基于X86平台的简单多任务内核的分析与实现
扫描二维码
随时随地手机看文章
引言
当提到多任务时,人们便会联想到MacOS、Linux、Windows等操作系统。通常情况下,若在操作系统下运行多任务,是由操作系统负责管理和调度各个任务的。本文通过分析一个简单的多任务内核,能够便于更容易地理解操作系统的任务管理机制,以及可以理解计算机系统是如何启动的。
1多任务程序的结构
本文实现的简单多任务内核,主要由两个文件构成:一个是使用as86语言编制的引导启动程序,主要用于在计算机系统加电时,将内核代码从启动盘加载到内存中;另一个便是使用GNUas汇编语言编写的内核程序,其中实现两个运行在特权级2上的任务可在时钟中断控制下相互切换运行,并可通过系统调用在屏幕上实现字符显示。
2多任务内核工作的启动程序原理
计算机系统加电启动后,会把启动程序从启动盘的第一个扇区加载到物理内存0x7c00位置开始处,之后把执行权交给0x7c00初开始运行启动程序。
启动程序的主要功能是将软盘或者镜像文件中的内核程序加载到内存的某个指定位置,实现这个目的的方法是利用ROSBIOS中断int0x13,把软盘或者镜像中的内核代码读入到内存,然后再把这段内核代码移动到内存0开始处。最后设置控制寄存器CR0中的开启保护运行模式标志,并跳转到内存0处开始执行内核代码。启动程序在内存中移动内核代码的示意图如图1所示。
图1启动程序在内存中移动内核代码的示意图
将内核代码移动到物理内存0开始处的主要原因是这是GDT表时可以简单一点。但是,不能让启动程序把内核代码从软盘或映像文件中直接加载到内存0处,因为加载操作需要ROMBIOS提供中断过程,而BIOS使用的中断向量表正处于内存0开始处。若直接把内核代码加载到内存0处,那么,BIOS中断过程将不能正常运行。
3内核程序
3.1初始化任务
内核程序运行在32位保护模式下,初始化阶段主要包括重新设置GDT表,设置系统定时器芯片,重新设置IDT表并且设置时钟和系统调用中断门。内核示例中所有代码和数据段都对应到物理内存同一个区域上,即从物理内存0开始的区域。在虚拟地址空间中内核程序的内核代码和任务代码分配图如图2所示。
3.2启动第一个任务
特权级0的代码不能直接把控制权转移到特权级2的代码中执行,但可以使用中断返回操作来实现,因此当初始化GDT、IDT和定时芯片结束后,就利用中断返回指令IRET来启动第一个任务。
具体的实现方法是在初始堆栈init_stack中人工设置一个返回环境,即把任务0的TSS段选择符加载到任务寄存器TR中,LDT段选择符加载到LDTR中以后,把任务0的用户栈指针和代码指针以及标志寄存器值压入栈中,然后执行中断返回指令IRET。该指令会弹出堆栈上的堆栈指针作为任务0用户栈指针,恢复假设的任务0的标志寄存器内容,并且弹出栈中代码指针放入CS:EIP寄存器中,从而开始执行任务0的代码,以完成从特权级0到特权级3代码的控制转移。
3.3两个任务的切换
内核程序将定时器芯片的通道0设置成每经过10ms就向中断控制芯片发送一个时钟中断请求信号,这样,每个10ms将会切换运行的任务。PC的ROMBIOS开机时已经在定时器芯片中把时钟中断请求信号设置成中断向量8,因此需要在中断8的处理过程中执行任务切换操作。
每个任务在执行时,会首先把一个字符的ASCII码放入寄存器AL中,然后调用系统中断int0x80,而该系统调用处理过程会调用一个简单的字符写屏子程序。在显示过一个字符后,任务代码会使用循环语句延迟一段时间,然后又跳转到任务代码开始处继续循环执行,直到运行了10ms而发生了定时中断,从而代码会切换到另一个任务去运行。
目前,该内核示例已经在Bochs模拟软件中运行测试过,测试结果如图3所示。
4结语
本文分析了一个基于X86平台的简单多任务内核的基本结构和加载运行原理,描述了其被加载进机器RAM中的基本思路,同时给出了两个任务进行切换的运行方法。其主要目的是理解操作系统的启动加载过程。
附 :本文的启动代码及内核代码如下:
#############################################
# 名称 :引导程序 boot.s #
# 说明 :把镜像文件中的 head 内核代码加载到内存某个
指定位置。 #
# #
#############################################
BOOTSEG=0x07c0
SYSSEG=0x1000
SYSLEN=17
entry start
start :
jmpi go,#BOOTSEG
go :
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,#0x400
mov ax,#0x0600
mov cx,#0x0000
mov dx,#0xFFFF
int 0x10
mov cx,#10
mov dx,#0x0000
mov bx,#0x000c
mov bp,#msg
mov ax,#0x1301
int 0x10
load_system :
mov dx,#0x0000
mov cx,#0x0002
mov ax,#SYSSEG
mov es,ax
xor bx,bx
mov ax,#0x200+SYSLEN
int 0x13
jnc ok_load
mov dx,#0x0000
mov ax,#0x0000
int 0x13
jmp load_system
ok_load :
cli
mov ax,#SYSSEG
mov ds,ax
xor ax,ax
mov es,ax
mov cx,#0x1000
sub si,si
sub di,di
rep
movw
mov ax,cs
mov ds,ax
lidt idt_48
lgdt gdt_48
mov ax,#0x0001
lmsw ax
jmpi 0,8
msg :
.ascii "Loading..."
.byte 13,10
gdt :
.word 0,0,0,0
.word 0x07FF
.word 0x0000
.word 0x9A00
.word 0x00C0
.word 0x07FF
.word 0x0000
.word 0x9200
.word 0x00C0
idt_48 :
.word 0
.word 0,0
gdt_48 :
.word 0x7FF
.word 0x7c00+gdt,0
.org 510
.word 0xAA55
#############################################
# 名称 :内核程序 head.s
# 说明 :包含 32 位保护模式初始化设置代码,时钟中断代码,系统调用中断代码和两个任务的代码。在初始化完成之后程序移动到任务 0 开始执行,并在时钟中断控制下进行任务 0 和 1 之间的切换操作。
#############################################
LATCH = 11930 # 定时器初始计数值,即每隔 10ms 发送一次中断请求。
SCRN_SEL = 0x18 # 屏幕显示内存段选择符
TSS0_SEL = 0x20 # 任务 0 的 TSS 段选择符
LDT0_SEL = 0x28 # 任务 0 的 LDT 段选择符
TSS1_SEL = 0x30 # 任务 1 的 TSS 段选择符
LDT1_SEL = 0x38 # 任务 1 的 LDT 段选择符
.globl startup_32
.text
startup_32 :
# 首先加载数据段寄存器 DS、堆栈段寄存器 SS 和堆栈
指针 ESP。所有段的线性基地址都是 0.
movl $0x10,%eax # 0x10 是 GDT 中数据段选择符。
mov %ax,%ds
lss init_stack,%esp # 把 init_stack 地址处的内容既
init_stack 有效地址给 esp,同时把 0x10 给 ss 段寄存器
# 在新的位置重新设置 IDT 和 GDT 表。
call setup_idt # 设置 IDT,先把 256 个中断门都填
默认处理过程的描述符
call setup_gdt
movl $0x10,%eax # 在改变了 GDT 之后 重新加载
所有段寄存器 .
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss init_stack,%esp
# 设置 8253 定时芯片。把计数器通道 0 设置成每个 10ms
向中断控制器发送一个中断请求信号。
movb $0x36,%al # 控制字:设置通道0工作在方式3、
计数处置采用二进制
movl $0x43,%edx #8253 芯片控制字寄存器写端口
outb %al,%dx
movl $LATCH,%eax # 初始计数值设置为 LATCH
(1193180/100),即频率 100HZ
movl $0x40,%edx # 通道 0 的端口
outb %al,%dx # 分两次把初始计数值写入通道 0
movb %ah,%al
outb %al,%dx
# 在 IDT 表第 8 和第 128(
0x80)项处分别设置定时中断
门描述符和系统调用陷阱门描述符
movl $0x00080000,%eax # 中断处 理 属内核,即
EAX 高字节是内核代码段选择符 0x0008.
movw $timer_interrupt,%ax # 设置定时中断门描述
符。取定时中断处理程序地址。
movw $0x8E00,%dx # 中断门类型是 14(屏蔽中断),
特权级 0 或硬件使用。
movl $0x08,%ecx # 开机时 BIOS 设置的时钟中断
向量号 8. 这里直接使用它。
lea idt(,%ecx,8),%esi # 把 IDT 描 述 符 0x08 地
址放入 ESI 中,然后设置该描述符
movl %eax,(%esi)
movl %edx,4(%esi)
movw $system_interrupt,%ax # 设置系统调用陷阱
门描述符。取系统调用处理程序地址。
movw $0xef00,%dx # 陷阱门类型是 15,特权级 3
的程序可执行。
movl $0x80,%ecx # 系统调用向量号是 0x80.
lea idt(,%ecx,8),%esi # 把 IDT 描 述 符 项 0x80
地址放入 ESI 中,然后设置该描述符.
movl %eax,(%esi)
movl %edx,4(%esi)
# 现在,为移动到任务 0 中执行来操作堆栈内容,在堆
栈中人工建立中断返回时的场景
pushfl
andl $0xffffbfff,(%esp)
popfl
movl $TSS0_SEL,%eax # 把任务 0 的 TSS 段选择
符加载到任务寄存器 TR
ltr %ax
movl $LDT0_SEL,%eax # 把任务 0 的 LDT 段选择
符加载到局部描述符表寄存器 LDTR
lldt %ax
movl $0,current
sti
pushl $0x17 # 任务 0 当前局部空间数据段选择符入栈
pushl $init_stack # 堆栈指针入栈
pushfl # 标志寄存器值入栈
pushl $0x0f # 任务 0 局部空间代码段选择符入栈
pushl $task0 # 把代码指针入栈
iret # 执行中断返回指令,从而切换到特权级 3 的任
务 0 中执行.
# 一下是设置 GDT 和 IDT 中描述符项的子程序
setup_gdt :
lgdt lgdt_opcode
ret
# 设置 IDT 表中所有 256 个中断们描述符都为统一个默
认值,均使用默认的中断处理过程 ignore_int。
setup_idt :
lea ignore_int,%edx
movl $0x0008000,%eax
movw %dx,%ax
movw $0x8E00,%dx
lea idt,%edi
mov $256,%ecx
rp_idt :
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_idt
lidt lidt_opcode
ret
# 显示字符子程序
write_char :
push %gs
pushl %ebx
mov $SCRN_SEL,%ebx
mov %bx,%gs
mov src_loc,%bx
shl $1,%ebx
mov %ax,%gs :
(%ebx)
shr $1,%ebx
incl %ebx
cmpl $2000,%ebx
jb 1f
movl $0,%ebx
1 :
movl %ebx,src_loc
popl %ebx
pop %gs
ret
# 以下是 3 个中断处理程序 :默认中断、定时中断和系统
调用中断
#ignore_int 是默认的中断处理程序,若系统产生了其他
中断,会在屏幕上显示一个字符“C”
.align 2
ignore_int :
push %ds
pushl %eax
movl $0x10,
%eax
mov %ax,
%ds
mov $0x0c98,
%ax /* print 'C' */
call write_char
popl %eax
pop %ds
iret
# 这是定时中断处理程序,主要执行任务切换操作 .
.align 2
timer_interrupt :
push %ds
pushl %eax
movl $0x10,
%eax
mov %ax,
%ds
movb $0x20,
%al
outb %al,
$0x20
movl $1,%eax
cmpl %eax,current
je 1f
movl %eax,current
ljmp $TSS1_SEL,$0
jmp 2f
1 :
movl $0,current
ljmp $TSS0_SEL,$0
2 :
popl %eax
pop %ds
iret
# 系统调用中断 int 0x80 处理程序 . 显示字符功能
.align 2
system_interrupt :
push %ds
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax
movl $0x10,
%edx
mov %dx,
%ds
call write_char
popl %eax
popl %ebx
popl %ecx
popl %edx
pop %ds
iret
/*************************************************/
current :
.long 0
src_loc :
.long 0
.align 2
lidt_opcode :
.word 256*8-1
.long idt
lgdt_opcode :
.word (end_gdt-gdt)-1
.long gdt
.align 2
idt :
.fill 256,8,0
gdt :
.quad 0x0000000000000000 #GDT 表, 第
一个描述符不用
.quad 0x00c09a00000007ff # 第二个是内核代码段描
述符,其选择符是 0x08.
.quad 0x00c09200000007ff # 第三个是内核数据段描
述符,其选择符是 0x10.
.quad 0x00c0920b80000002 # 第 4 个是显示内存段
描述符,其选择符是 0x18.
.word 0x68,tss0,0xe900,0x0 # 第 5 个是 TSS0 段
的描述符,其选择符是 0x20.
.word 0x40,ldt0,0xe200,0x0 # 第 6 个 是 LDT0
段的描述符,其选择符是 0x28
.word 0x68,tss1,0xe900,0x0 # 第 7 个是 TSS1 段
的描述符,其选择符是 0x30
.word 0x40,ldt1,0xe200,0x0 # 第 8 个是 LDT1 段
的描述符,其选择符是 0x38
end_gdt :
.fill 128,4,0 # 初始内核堆栈空间
init_stack :
# 刚进入保护
模式时用于加载 SS :ESP 堆栈指针值
.long init_stack
.word 0x10
# 下面是任务 0 的 LDT 表段中的局部段描述符
.align 2
ldt0 :
.quad 0x0000000000000000
.quad 0x00c0fa00000003ff # 第 2 个是局部代码段
描述符,对应选择符是 0x0f.
.quad 0x00c0f200000003ff # 第 3 个是局部数度段
描述符,
对应选择符是 0x17.
# 下面是任务 0 的 TSS 段的内容
tss0 :
.long 0
.long krn_stk0,0x10 /*esp0,ss0*/
.long 0,0,0,0,0
/*esp1,ss1,esp2,ss2,
cr3*/
.long 0,0,0,0,0
/ *e i p,e f l a g s,e a x,
ecx,edx*/
.long 0,0,0,0,0
/*ebx,esp,ebp,esi,
edi*/
.long 0,0,0,0,0,0
/*es,cs,ss,ds,fs,
gs*/
.long LDT0_SEL,0x8000000
/ * l d t,t r a c e
bitmap*/
.fill 128,4,0
# 这是任务 0 的内核栈
空间
krn_stk0 :
# 下面是任务 1 的 LDT 表段内容和 TSS 段内容
.align 2
ldt1 :
.quad 0x0000000000000000# 第 1 个 描 述
符,不用
.quad 0x00c0fa00000003ff # 选择符是 0x0f,基地
址 = 0x00000
.quad 0x00c0f200000003ff # 选择符是 0x17,基地
值 = 0x00000
tss1 :
.long 0
.long krn_stk1,0x10
.long 0,0,0,0,0
.long task1,0x200
.long 0,0,0,0
.long usr_stk1,0,0,0
.long 0x17,0x0f,0x17,0x17,0x17,0x17
.long LDT1_SEL,0x8000000
.fill 128,4,0
# 这是任务 1 的内核栈
空间,其用户栈直接使用初始栈空间
krn_stk1 :
# 下面是任务 0 和任务 1 的程序,它们分别循环显示字
符“A”和“B”
task0 :
movw $0x0c61,
%ax
# 把需要显示
的字符 A 放入 AL 寄存器中
int $0x80
movl $0xfff,%ecx # 执行循环,起延时作用
1 :
loop 1b
jmp task0
task1 :
movw $0x0d62,
%ax
int $0x80
movl $0xfff,%ecx
1 :
loop 1b
jmp task1
.fill 128,4,0
# 这是任务 1 的用户栈
空间
usr_stk1 :
.long 0