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

你可能感兴趣的:(实例)