4.9一个简单的多任务内核实例

第四章第9节

  本节描述了一个简单多任务内核的设计和实现方法,这个内核包括两个特权级3的用户任务和一个系统调用中断过程。

本节给出的内核实例由两个文件构成。一个是使用as86语言编制的引导启动程序boot.s,用于在计算机加电时从启动盘上把内核代码加载到内存中;另一个是使用GUN as汇编语言编制的内核程序head.s,其中实现了2个运行在特权级3上的任务在时钟中断控制下相互切换运行,并且还实现了在屏幕上显示字符的一个系统调用。我们把这两个任务分别称为任务A和任务B,它们会分别调用这个系统调用在屏幕上输出字符'A'和'B',直到每隔10毫秒切换至另一个任务,任务A连续循环的调用系统调用在屏幕上输出'A',而任务B一直显示'B'。如要终止这个内核实例程序,则要重新启动机器,或者关闭运行的模拟PC运行环境软件。

  boot.s程序编绎出的代码共512字节,将被存放在软盘映像文件的第一个扇区中,PC在加电启动时,ROM BIOS中的程序会把启动盘第一个扇区加载到物理内存0X7C00(31kb)位置开始出,并把执行权限转移到0X7C00处开始执行boot程序代码。head.s程序运行在32位保护模式下,其功能主要包括:初始化设置代码、时钟中断0X08的过程代码、系统调用中断0X08的过程代码以及任务A和任务B等的代码和数据。初始化设置工作主要包括:1.重新设置GDT表  2.设置系统定时器芯片  3.重新设置IDT表并且设置时钟和系统调用中断门  4.移动到任务A中执行。

  由于特权级0的代码不能直接把控制权转移到特权级3的代码中去,但是中断操作是可以的,因此当初始化GDT,IDT和定时芯片结束后,我们就利用中断返回指令IRET来启动运行第一个任务。具体实现方法是在初始堆栈init_stack中人工设置一个返回环境,即把任务0的TSS段选择符加载到任务寄存器LTR中、LDT段选择符加载到LDTR中以后,把任务0的用户栈指针(0x17:init_stack)和代码指针(0x0f:task0)以及标志寄存器的值压入栈中,然后执行中断返回指令IRET。该指令会弹出堆栈上的堆栈指针作为任务0的用户栈指针恢复假设的任务0的标志寄存器的内容,并且弹出堆栈中的代码指针放入CS:EIP寄存器中,从而开始执行任务0的代码,完成了从特权级0到特权级3代码的控制转移。

  为了每隔10毫秒切换运行的任务,head.s程序中把定时器芯片8253的通道0设置成每隔10毫秒就向中断控制器8259A发送一个时钟中断请求信号,PC机的ROM BIOS开机时已经在8259A中把时钟中断请求信号设置成中断向量8,因此需要在中断8的处理过程中执行任务切换操作,任务切换的方法是查看current变量中当前运行的任务号,如果current是0,就利用任务1的TSS选择符作为操作数执行远跳转指令,从而切换到任务1中执行,否则反之。

  每个任务在执行时,会首先把一个字符的ACII码放入寄存器AL中,然后调用系统中断调用int 0x80,该系统调用处理过程则会调用一个简单的字符写屏子程序,把AL中的字符显示在屏幕上,同时把字符显示的屏幕的下一个位置记录下来,用于下一次显示字符。在显示过一个字符后,任务代码会使用循环语句延迟一段时间,然后又跳转到任务代码开始处继续循环执行,知道运行了10毫秒而发生了定时中断,切换到另一个任务中去执行。对于任务A,寄存器AL中始终存放字符'A',而任务B运行时AL中始终存放字符'B',因此在程序运行时我们会看到一连串的字符'A'和一连串的字符'B'不断的显示在屏幕上。

  下面给出boot.s和head.s程序的详细注释。有关这个简单内核实例的编译和运行方法参考最后一章“编译运行简单内核实例程序”一节的内容。

4.9.2 引导启动程序boot.s

  为了让程序尽量简单,这个引导扇区启动程序仅能够加载长度不超过16个扇区的head代码,并且直接使用了ROM BIOS默认设置的中断向量号,即定时中断请求处理的中断号仍然是8,这与linux系统中使用的不同。linux系统会在内核初始化时重新设置8259A中断控制芯片,并把时钟中断请求信号对应到中断0x20上,详细说明见“内核引导启动程序”一章内容。

 

! boot.s程序
! 首先利用BIOS中断把内核代码(head.s)加载到内存0x10000处,然后移动到内存0处
! 最后进入保护模式,并跳转到内存0(head.s)开始出继续运行。
BOOTSEG = 0X07C0            !引导扇区(本程序)被BIOS加载到内存0X7C00处
SYSSEG = 0X1000                !内核(head)先加载到0X10000处,然后移动到0X0处
SYSLEN = 17                    !内核占用的最大磁盘扇区数
entry start
start:
    jmpi    go,#BOOTSEG        !段间跳转至0x7c0:go处。当本程序刚运行时所有段寄存器的值均为0.该
                            !跳转语句会把CS寄存器加载为0x7c0
go: mov        ax,cs             !让DS和SS都指向0X7C0段
    mov        ds,ax
    mov        ss,ax
    mov        sp,#0x400        !设置临时栈指针,其值需大于程序末端并有一定的空间即可

!加载内核代码到内存0x10000开始处
load_system:
    mov     dx,#0x0000           !利用BIOS中断int 0x13功能2从启动盘读取head代码。
    mov        cx,#0x0002           ! DH - 磁头号;DL - 驱动器号; CH - 10位磁道号低8位;
    mov        ax,#SYSSEG           !CL - 位7,6是磁道号高2位,位5-0是起始扇区号(从1记).
    mov     es,ax               !ES:BX - 读入缓冲区位置(0x1000:0x0000)。
    xor        bx,bx             
    mov     ax,#0x200+SYSLEN   !AH - 读扇区功能号;AL - 需读的扇区数(17)
    int     0x13 
    jnc        ok_load                !若没有发生错误则跳转继续运行,否则死循环
die:
    jmp     die
!把内核代码移动到内存0开始出,共移动8KB字节(内核长度不超过8KB)
ok_load:
    cli                            ! 关中断
    mov     ax, #SYSSEG            !移动开始位置 DS:SI = 0X1000:0 目的位置ES:DI=00.
    mov        ds, ax
    xor        ax, ax
    mov        es, ax
    mov     cx, #0X1000
    sub        si, si
    sub     di, di
    rep        movw                ! 执行重复移动指令
! 加载 IDT 和 GDT基地址寄存器 IDTR 和 GDTR
    mov     ax, #BOOTSEG
    mov     ds, ax                 ! 让DS重新指向 0x7c0段
    lidt    idt_48                ! 加载IDTR.6字节操作数,2字节表长度,4字节线性基地址
    lgdt    gdt_48                ! 加载GDTR.6字节操作数,2字节表长度,4字节线性基地址。

! 设置控制寄存器CR0(即及其状态字),进入保护模式。段选择符8对应GDT表中第2个段描述符
    mov     ax, #0x0001            ! 在CR0中设置保护模式标志PE(位0)
    lmsw    ax                    
    jmpi    0,8                    ! 然后跳转至段选择符指定的段中,偏移0处。
                                ! 注意此时段值已是段选择符,该段的线性基地址是0

! 下面是全局描述符表GDT的内容,其中包含3个段描述符。第一个不用,第二个是代码和数据段描述
! 符 
gdt:
    .word    0,0,0,0             ! 段描述符0,不用,每个描述符占8个字节

    .word    0x07FF                ! 段描述符1. 8MB  段限长=2047(2048*4096=8MB)
    .word     0X0000                 ! 段基地址=0x00000
    .word     0X9A00                ! 是代码段,可读/执行
    .word     0X00C0                ! 段属性颗粒度=4KB, 80386

    .word    0x07FF                 !段描述符2.8MB  段限长值=2047 (2048*4096=8MB)
    .word     0x0000                 ! 段基地址=0x00000
    .word     0x9200                 ! 是数据段,可读写
    .word     0x00c0                 ! 段属性科类度=4KB,80386

! 下面分别是LIDT和LGDT指令的6字节操作数
idt_48:
    .word    0                     ! IDT表长度是0
    .word     0,0                 ! IDT表的线性基地址也是0
gdt_48:
    .word     0x7ff                 ! GDT 表长度是2048字节,可容纳256个描述符项
    .word     0x7c00+gdt, 0       ! GDT 表的线性基地址在0x7c0段的偏移gdt处
.org 510                !.org命令的作用等同于给'.'赋值,即是使当前程序定位在510字节处
    .word     0XAA55                 ! 引导扇区有效标志,必须处于引导扇区最后2字节处

 

 4.9.3 多任务内核程序 head.s

   在进入保护模式后,head.s重新建立和设置IDT、GDT表的主要原因是为了让程序在结构上比较清晰,也为了与后面linux 0.11内核源代码中这两个表的设置方式保持一致。

 

#head.s 包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码
#在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和任务1之间的切换操作
LATCH  = 11930                #定时器出事计数值,即每隔10毫秒发送一次中断请求
SCRN_SEL = 0X18                #屏幕显示内存段选择符
TSS0_SEL = 0X20                #任务0的TSS段选择符
LDT0_SEL = 0X28                #任务0的LDT段选择符
TSS1_SEL = 0X30                #任务1的TSS段选择符
LDT1_SEL = 0X38                #任务1的LDT段选择符
.text
startup_32:
#首先加载数据段寄存器DS、堆栈寄存器SS和堆栈指针ESP。所有段的线性基地址都是0
    movl $0x10, %eax        #0x10是GDT中数据段选择符
    mov  %ax, %ds
    lss  init_stack, %esp    #lss命令同时给SS和ESP赋值,高16位赋给SS,低16位赋给ESP
#在新的位置重新设置IDT和GDT表
    call setup_idt            #设置IDT,先把256个中断门都填默认处理过程的描述符
    call setup_gdt            #设置GDT
    movl $0x10, %eax        #在改变了GDT之后重新加载所有段寄存器
    mov  %ax,%ds
    mov  %ax,%es
    mov  %ax,%fs
    mov  %ax,%gs
    lss  init_stack,%esp
#设置8253定时芯片,把计数器通道0设置成每隔10户毫秒向中断控制器发送一个中断请信号
    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项处分别设置定时中断门描述符和系统调用陷阱门描述符
    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(任务A)中执行来操作堆栈内容,在堆栈中人工建立中断返回时的场景
    pushfl                        #复位标志寄存器EFLAGS中的嵌套任务标志
    andl  $0xffffbfff, (%esp)
    popf1
    movl $TSS0_SEL, %eax         #把任务0的TSS段选择符加载到任务寄存器TR
    ltr  %ax
    movl $LDT0_SEL, %eax         #把任务0的LDT段选择符加载到局部描述符表寄存器LDTR
    lldt %ax                    #TR和LDTR只需人工加载一次,以后CPU会自动处理
    movl $0, current            #把当前任务号0保存在current变量中
    sti                         #现在开启中断,并在栈中营造中断返回时的场景
    pushl $0x17                    #把任务0当前局部空间数据段(堆栈段)选择符入栈
    pushl $init_stack            #把堆栈指针入栈(也可以直接把ESP入栈)
    pushfl                        #把标志寄存器入栈
    pushl $0x0f                 #把当前局部空间代码选择符入栈
    pushl $task0                #把代码指针入栈
    iret                         #执行中断返回指令,从而切换到特权级3的任务0中执行

#以下是设置GDT和IDT中描述符项的子程序
setup_gdt:                        #使用6字节操作数lgdt_opcode设置GDT表位置和长度
    lgdt lgdt_opcode
    ret

#这段代码暂时设置IDT表中所有256个中断门描述符都为同一个默认值,均使用默认的中断处理过程ignore_int。
#设置的具体方法是:首先在EAX和EDX寄存器中分别设置好默认中断门描述符的0-3字节和4-7字节的内容,然后
#利用该寄存器对循环往IDT表中填充默认中断门描述符的内容
setup_idt:                    #把所有256个中断门描述符设置为使用默认处理过程
    lea ignore_int , %eax    #设置方法与设置定时中断门描述符的方法一样
    movl $0x00080000, %eax    #选择符为0x0008
    movw %dx,%ax
    movw $0x8e00, %dx        #中断门类型,特权级为0
    lea idt, %edi
    mov $256, %ecx            #循环设置所有256个门描述符项
rp_idt:
    movl %eax, (%edi)
    movl %edx, 4(%edi)
    addl $8, %edi
    dec %ecx
    jne rp_idt
    lidt lidt_opcode        #最后用6字节操作数加载IDTR寄存器
    ret

#显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上,整屏可显示80x25个字符
write_char:
    push  %gs                #首先保存要用到的寄存器,EAX由调用者负责保存
    pushl %ebx                
    mov  $SCRN_SEL, %ebx    #然后让GS指向显示内存段(0xb8000)
    mov  %bx, %gs
    movl scr_loc, %bx        #再从变量scr_loc中取目前字符显示位置值
    shl  $1, %ebx            #因为在屏幕上每个字符还有一个属性字节,因此字符
    movb %al, %gs:(%ebx)     #实际显示位置对应的显示内存偏移地址要乘2
    shr  $1, %ebx             #把字符放到显示内存后把位置值除2加1,此时位置值对
    incl  %ebx                #应下一个显示位置,如果该位置大于2000,则复位成0
    cmpl  $2000, %ebx
    jb    lf
    movl  $0, %ebx
l:
    movl  %ebx, scr_loc        #最后把这个位置值保存起来(scr_loc)
    popl  %ebx                #并弹出保存的寄存器内容,返回
    pop   %gs
    ret

#以下是3个中断处理程序:默认中断、定时中断和系统调用中断
#ignore_int是默认的中断处理程序,若系统产生了其它中断,则会在屏幕上显示一个字符“C”
.align 2
ignore_int:
    push %ds
    pushl %eax     
    movl $0x10, %eax         #首先让DS指向内核数据段,因为中断程序属于内核
    mov %ax,  %ds
    movl $67, %eax             #在AL中存放字符C的代码,调用显示程序显示在屏幕上
    call  write_char
    popl  %eax
    popl  %ds
    iret

#这是定时中断处理程序。其中主要执行任务切换操作
.align 2
timer_interrupt:
    push %ds
    pushl %eax
    movl $0x10, %eax         #首先让DS指向内核数据段
    mov  %ax, %ds
    movb $0x20, %al         #然后立刻允许其他硬件中断,即向8259A发送EOI命令
    outb %al, $0x20
    movl $1, %eax             #接着判断当前任务,若是任务1则去执行任务0,或反之
    cmpl %eax, current
    je 1f
    movl %eax, current         #若当前任务是1,则把0存入current,并跳转到任务0
    ljmp $TSS0_SEL, $0         #去执行
    popl %eax 
    pop  %ds 
    iret

#系统调用中断int 0x80处理程序。该示例只有一个显示字符功能
.align 2
system_interrupt:
    push  %ds
    pushl %edx
    pushl %ecx
    pushl %ebx
    pushl %eax

    movl $0x10, %edx        #首先让DS指向内核数据段
    mov  %dx, %ds
    call write_char         #然后调用显示字符子程序write_char,显示AL中的字符。
    popl %eax 
    pop1 %ebx
    popl %ecx
    popl %edx
    pop %ds
    iret

##############****************************************###############
current:.long 0                         #当前任务号(0或1)
scr_loc:.long 0                         #屏幕当前显示位置。从左上角到右下角顺序显示

.align 2
lidt_opcode:
    .word 256 * 8 - 1                     #加载IDTR寄存器的6字节操作数:表长度和基地址
    .long idt
lgdt_opcode:
    .word (end_gdt-gdt)-1                 #加载GDTR寄存器的6字节操作数:表长度和基地址
    .long gdt

.align 3
idt:
    .fill 256,8,0                         #IDT空间。共256个门描述符,每个8字节,共占用2KB

gdt:
    .quad 0x0000000000000000             #GDT表,第1个描述符不用
    .quad 0x00c09a00000007ff             #第2个是内核代码段描述符,其选择符是0x08
    .quad 0x00c09200000007ff             #第3个是内核数据段描述符,其选择符是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 3
ldt0:
    .quad 0x0000000000000000             #第1个描述符,不用。
    .quad 0x00c0fa00000003ff             #第2个局部代码段描述符,对应选择符是0x0f
    .quad 0x00c0f200000003ff             #第3个局部数据段描述符,对应选择符是0x17

#下面是任务0的TSS段的内容。注意其中标号等字段在任务切换时不会改变。
tss0:
    .long 0                     /*back link*/
    .long krn_stk0, 0x10         /*esp0,ss0*/
    .long 0, 0, 0, 0, 0         /*esp1, ss1, esp2, ss2, cr3*/
    .long 0, 0, 0, 0, 0         /*eip, eflags, eax, 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     /*ldt, trace bitmap*/
    .fill 128,4,0                 #这是任务0的内核栈空间
krn_stk0:
#下面是任务1的LDT表段内容和TSS段内容
.align 3
ldt1:
    .quad 0x0000000000000000     #第1个描述符,不用。
    .quad 0x00c0fa00000003ff     #选择符是0x0f,基地址=0x00000
    .quad 0x00c0f200000003ff     #选择符是0x17, 基地址=0x00000

tss1:
    .long 0                                 /*back link */
    .long krn_stk1, 0x10                     /*esp0, sss0*/
    .long 0,0,0,0,0                         /*esp1, ss1,esp2,ss2,cr3*/
    .long task1, 0x200                         /*eip, eflags */
    .long 0,0,0,0                             /* eax, ecx , edx, ebx */
    .long usr_stk1, 0, 0, 0                 /* esp, ebp, esi, edi */
    .long 0x17,0x0f,0x17,0x17,0x17,0x17        /* es,cs,ss,ds,fs,gs*/
    .long LDT1_SEL, 0X8000000                 /* ldt, tarce bitmap */

    .fill 128,4,0                     #这是任务1的内核空间。其用户栈直接使用初始栈空间
krn_stk1:

#下面是任务0和任务1的程序,它们分别循环显示字符'A''B'task0:
    movl $0x17, %eax             #首先让DS指向任务的局部数据,所以这两句可省略
    movw %ax, %ds                 #因为任务没有使用局部数据,所以这两句可省略
    movl $65, %al                 #把需要显示的字符'A'放入AL寄存器中
    int $0x80                     #执行系统调用,显示字符
    movl $0xfff, %ecx            #执行循环,起延时作用
1:
    loop 1b
    jmp  task0                     #跳转到任务代码开始处继续显示字符
task1:
    movl $66, %al                 #把需要显示的字符'B'放入AL寄存器中
    int  $0x80                     #执行系统调用,显示字符
    movl $0xfff, %ecx             #延时一段时间,并跳转到开始处继续循环显示
1:
    loop 1b
    jmp  task1

    .fill 128,4,0                 #这是任务1的用户栈空间
usr_stk1:

 

  保护模式详解------http://baike.baidu.com/link?url=BwqoEM95JB15Q2Xl3-UEuEozXNToviyZ66qtEZFKSMU-XZDX-mNXO8L2mW4JwPqV

posted @ 2014-03-15 10:48  萧瑟秋风_cyz  阅读(934)  评论(0编辑  收藏  举报