第四章第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=0:0. 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