4.9一個簡單的多任務內核實例

第四章第9節linux

  本節描述了一個簡單多任務內核的設計和實現方法,這個內核包括兩個特權級3的用戶任務和一個系統調用中斷過程。oop

本節給出的內核實例由兩個文件構成。一個是使用as86語言編制的引導啓動程序boot.s,用於在計算機加電時從啓動盤上把內核代碼加載到內存中;另外一個是使用GUN as彙編語言編制的內核程序head.s,其中實現了2個運行在特權級3上的任務在時鐘中斷控制下相互切換運行,而且還實現了在屏幕上顯示字符的一個系統調用。咱們把這兩個任務分別稱爲任務A和任務B,它們會分別調用這個系統調用在屏幕上輸出字符'A'和'B',直到每隔10毫秒切換至另外一個任務,任務A連續循環的調用系統調用在屏幕上輸出'A',而任務B一直顯示'B'。如要終止這個內核實例程序,則要從新啓動機器,或者關閉運行的模擬PC運行環境軟件。url

  boot.s程序編繹出的代碼共512字節,將被存放在軟盤映像文件的第一個扇區中,PC在加電啓動時,ROM BIOS中的程序會把啓動盤第一個扇區加載到物理內存0X7C00(31kb)位置開始出,並把執行權限轉移到0X7C00處開始執行boot程序代碼。head.s程序運行在32位保護模式下,其功能主要包括:初始化設置代碼、時鐘中斷0X08的過程代碼、系統調用中斷0X08的過程代碼以及任務A和任務B等的代碼和數據。初始化設置工做主要包括:1.從新設置GDT表  2.設置系統定時器芯片  3.從新設置IDT表而且設置時鐘和系統調用中斷門  4.移動到任務A中執行。spa

  因爲特權級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'不斷的顯示在屏幕上。code

  下面給出boot.s和head.s程序的詳細註釋。有關這個簡單內核實例的編譯和運行方法參考最後一章「編譯運行簡單內核實例程序」一節的內容。blog

4.9.2 引導啓動程序boot.sip

  爲了讓程序儘可能簡單,這個引導扇區啓動程序僅可以加載長度不超過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

相關文章
相關標籤/搜索