linux0.00 "head.s"程式詳解
阿新 • • 發佈:2019-01-22
// head.s包含32位保護模式初始化設定程式碼、時鐘中斷程式碼、系統呼叫中斷程式碼和兩個任務的程式碼。 // 在初始化完成之後程式移動到任務0開始執行,並在時鐘中斷控制下進行任務0和1之間的切換操作。 LATCH = 11930 // 定時器初始計數值,即每隔10毫秒傳送一次中斷請求。 問:為何是這個值? 因為8253晶片的時鐘輸入頻率是1.193180mhz SCRN_SEL = 0x18 // 螢幕顯示記憶體段選擇符。 問:以下這些選擇符是怎麼定的值?根據段選擇符的定義:位bit[15-3]為段索引,bit2為0表示GDT,1表示LDT,bit[1:0]表示RPL。所以0x18二進位制為[00011 0 00]表示GDT表中的第三個描述符。 TSS0_SEL = 0x20 // 任務0的TSS段選擇符。 0x20二進位制為[00100 0 00],表示選擇GDT表中的第四個描述符 LDT0_SEL = 0x28 // 任務0的LDT段選擇符。 0x28二進位制為[00101 0 00],表示選擇GDT表中的第五個描述符 TSS1_SEL = 0x30 // 任務1的TSS段選擇符。 0x30二進位制為[00110 0 00],表示選擇GDT表中的第六個描述符 LDT1_SEL = 0x38 // 任務1的LDT段選擇符。 0x38二進位制為[00111 0 00],表示選擇GDT表中的第七個描述符 .text // 表示可執行程式碼段(問:實際在編譯時有什麼影響嗎?.text,.data,.bss,用於分別定義當前程式碼段,資料段和未初始化資料段,在連結多個目標模組時,連結程式會根據它們的類別把各個目標模組中的相應段分別組合在一起。) startup_32: // 首先載入資料段暫存器DS、堆疊段暫存器SS和堆疊指標ESP。所有段的線性基地址都是0. movl $0x10, %eax // 0x10是GDT中資料段選擇符。 0x10二進位制為[00010 0 00],表示選擇GDT表中的第二個描述符 mov %ax, %ds // ds就是存放資料段選擇符的段暫存器,這條指令將ax的值0x0010傳遞給ds暫存器,ax是eax的低16位 lss init_stack, %esp // LSS:載入堆疊段 /* 裝入全指標指令LDS,LES,LFS,LGS,LSS 這些指令有兩個運算元。第一個運算元是一個有效地址,第二個是一個通用暫存器。指令從該地址 取得48位全指標,將選擇符裝入相應的段暫存器,而將32位偏移量裝入指定的通用暫存器。注意在 記憶體中,指標的存放形式總是32位偏移量在前面,16位選擇符在後面。LDS/LSS等裝入指標以後, 就可以用DS:[ESI]/SS:[ESP]等這樣的形式來訪問指標指向的資料了。 比如這裡: lss init_stack,%esp 而: init_stack: .long init_stack ;四位元組地址 .word 0x10 ;段選擇符,同資料段選擇符 這樣執行後SS中裝入段選擇符0x10,ESP中裝入init_stack的地址,棧頂在init_stack標號處。 */ // 在新的位置重新設定IDT和GDT表。 call setup_idt call setup_gdt movl $0x10, %eax mov %ax, %ds // 重新設定GDT表後,雖然段選擇符沒變,但是實際的描述表位置已經改變(setup_gdt子程式中用lgdt指令更新了GDTR暫存器中gdt表的位置和長度),所以要重新載入段暫存器 mov %ax, %es mov %ax, %fs mov %ax, %gs lss init_stack, %esp // 重新載入SS和ESP // 設定8253定時晶片。把計數器通道0設定成每隔10毫秒向中斷控制器傳送一箇中斷請求訊號。 // 下面介紹一下8253定時晶片: // 8253具有3個獨立的計數通道,採用減1計數方式。在門控訊號有效時,每輸入1個計數脈衝,通道作1次計數操作。當計數脈衝是已知週期的時鐘訊號時,計數就成為定時。 // 方式3為:方波發生器,最適合計算機。 movb $0x36, %al // 控制字:設定通道0工作在方式3、計數器初值採用二進位制。 movl $0x43, %edx // 8253晶片控制字暫存器寫埠。 outb %al, %dx // 向I/O埠寫入一個位元組,這裡是向埠0x43寫入0x36 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)項處分別設定定時中斷門描述符和系統呼叫陷阱門描述符。 // 這裡先解釋一下int $0x80: // int $0x80是一條AT&T語法的中斷指令,用於Linux的系統呼叫。 // Linux系統下的組合語言比較喜歡用AT&T的語法,如果翻譯成Intel的語法就是int 80h,就像我們在Intel的語法下的DOS彙編中經常用的int 21h呼叫DOS中斷,同樣如果換成AT&T語法就是int $0x80。 // 不過無論使用那一種語法,int $0x80或者int 80h都是針對Linux的,在DOS或者Windows下不起相應作用。反之亦然。 movl $0x00080000, %eax // 中斷程式屬核心,即EAX高字是核心程式碼段選擇符0x0008(即索引為1,TI=0,RPL=00) movw $timer_interrupt, %ax // 設定定時中斷門描述符。取定時中斷處理程式地址。eax低字(低16位元組) movw $0x8E00, %dx // 中斷門型別是14(遮蔽中斷),特權級0或硬體使用。 movl $0x08, %ecx // 開機時BIOS設定的時鐘中斷向量號8.這裡直接使用它。 lea idt(0, %ecx, 8), %esi // 把IDT描述符0x08地址放入ESI中,idt+0+ecx*8 => esi,標號"idt"是IDT表的地址,ecx這裡是0x08,所以此時esi指向idt表的第64位元組處,每個描述符佔8位元組,即第8箇中斷門描述符處,然後設定該描述符 movl %eax, (%esi) // 將相應數值填入門描述符的低4位元組,填段選擇符和中斷函式地址低16位 movl %edx, 4(%esi) // 填充門描述符的高4位元組,描述符屬性和中斷函式地址的高16位 /* 定時中斷呼叫過程(為簡便省去特權級檢查等):定時中斷的向量號為8,所以發生中斷的時候 ,CPU會根據IDTR暫存器(上面call setup_idt已經設定好了IDTR)中提供的IDT表的基地址 ,找到第8箇中斷門描述符,也即這裡設定的門描述符。根據門描述符中的段選擇符找到相應段 描述符,這裡是找到核心程式碼段,這裡的核心程式碼段的基地址是0.該基地址加上 timer_interrupt就是中斷函式入口。所以最終可以呼叫timer_interrupt函式。 */ movw $system_interrupt, %ax // 設定系統呼叫陷阱門描述符。取系統通呼叫處理程式地址。 movw $0xef00, %dx // 陷阱門型別是15,特權級3的程式可執行。 movl $0x80, %ecx // 系統呼叫向量號是0x80。 lea idt(, %ecx, 8), %esi // 把IDT描述符項0x80地址放入ESI中,然後設定該描述符。 movl %eax, (%esi) // 將相應數值填入描述符的低4位元組,填段選擇符(沒有改動還是0x08)和中斷函式地址低16位 movl %edx, 4(%esi) // 填充門描述符的高4位元組,描述符屬性和中斷函式地址的高16位 // 好了,現在我們為移動到任務0(任務A)中執行來操作堆疊內容,在堆疊中人工建立中斷返回時的場景。 // 注: 由於處於特權級0的程式碼不能直接把控制權轉移到特權級3的程式碼中執行,但中斷返回操作是可以的,因此當初始化GDT、IDT和定時晶片結束後,我們就利用中斷返回指令IRET來啟動執行第1個任務。 // 具體實現方法是在初始堆疊init_stack中人工設定一個返回環境。即把任務0的TSS段選擇符載入到任務暫存器LTR中、LDT段選擇符載入到LDTR中以後, // 把任務0的使用者棧指標(0x17:init_stack)和程式碼指標(0x0f:task0)以及標誌暫存器壓入棧中,然後執行中斷返回指令IRET。 // 該指令會彈出堆疊上的堆疊指標作為任務0的使用者棧指標,恢復假設的任務0的標誌暫存器內容,並且彈出棧中程式碼指標放入CS:EIP暫存器中,從而開始執行任務0的程式碼, // 完成了從特權級0到特權級3的控制轉移。 pushfl // EFLAGS入棧 andl $0xffffbfff, (%esp) // 復位標誌暫存器EFLAGS中的巢狀任務標誌。 popfl // EFLAGS出棧 // 解釋一下EFLAGS暫存器中的NT標誌: // 位14是巢狀任務標誌(Nested Task)。它控制這被中斷任務和呼叫任務之間的連結關係。在使用CALL指令、中斷或異常執行任務呼叫時,處理器會設定該標誌。在通過使用IRET指令從一個任務返回時,處理器會檢查並修改這個NT標誌。 // 使用POPF/POPFD指令也可以修改這個標誌,但是在應用程式中改變這個標誌的狀態會產生不可意料的異常。 // 巢狀任務標誌NT用來控制中斷返回指令IRET的執行。具體規定如下: // (1) 當NT=0,用堆疊中儲存的值恢復EFLAGS、CS和EIP,執行常規的中斷返回操作; // (2) 當NT=1,通過任務轉換實現中斷返回。 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當前區域性空間資料段(堆疊段)選擇符入棧。 /* 問:0x17是怎麼來的? 答:0x17是任務0的區域性資料段選擇符,0x17的二進位制為[00010 1 11]故為選擇Index=2, TI=1(表示在LDT中),RPL=3的段描述符,根據該段描述符可知為資料段,但是我們又看 到下面有ldt0和ldt1二個區域性描述符表中都有0x17這個段選擇符,那這裡怎麼區分選擇 的是哪個區域性段描述符表裡的資料段呢,很簡單因為TI=1,直接根據LDTR中的內容 來尋找區域性段描述符表。上面已經用lldt指令載入了ldt0段描述符在GDT表中的段選擇符到 LDTR中,所以這裡的0x17段選擇符選擇的是LDT0的第二個段(資料段)。注:lgdt載入的 是6位元組的運算元,表示GDT表的基地址和長度。而lldt載入的是相應LDT表段描述符在GDT 表中的段選擇符,根據該段選擇符就能找到LDT表的基地址和長度。如根據LDT0_SEL段選擇 符就能找到GDT表中ldt0的段描述符,然後根據LDT0段的描述符找到ldt0表的地址, 最終找到ldt0的資料段。這就是為什麼每個LDT都必須在GDT中有一個段描述符和段選擇符。 */ pushl $init_stack // 把堆疊指標入棧(也可以直接把ESP入棧),在任務切換到任務1時,該值被彈出堆疊做為任務0的使用者棧在TSS中儲存,詳見任務狀態段有關任務切換的描述。 pushfl // 把標誌暫存器入棧。 pushl $0x0f // 把當前區域性空間程式碼段選擇符入棧。 pushl $task0 // 把程式碼指標入棧。注意!pushl和push也是有區別的! iret // 執行中斷返回指令,從而切換到特權級3的任務0中執行。 /* 出棧時的內容為:任務0的程式碼指標task0,區域性空間程式碼段選擇符,標誌暫存器,堆疊指標, 區域性空間資料段(堆疊段)選擇符,ldt0段選擇符,tss0段選擇符. */ // 以下是設定GDT和IDT中描述符項的子程式。 setup_gdt: // 使用6位元組運算元lgdt_opcode設定GDT表位置和長度。 lgdt lgdt_opcode // lgdt指令載入GDT的入口地址(這裡由lgdt_opcode指出)到GDTR中 ret setup_idt: //首先在256個門描述符中都設定中斷處理函式為ignore_int,然後用lidt載入IDT表。 lea ignore_int, %edx // 設定方法與設定定時中斷門描述符的方法一樣。 movl $0x00080000, %eax // 選擇符位0x0008,即同核心程式碼段。 movw %dx, %ax // (注:ax為eax的低16位),設定中斷函式地址 movw $0x8E00, %dx // 門描述符屬性 lea idt, %edi // 取idt表地址 mov $256, %ecx // 迴圈設定所有256個門描述符項。 rp_idt: movl %eax, (%edi) // 填IDT表 movl %edx, 4(%edi) addl $8, %edi // IDT表地址加8位元組,即跳到下一門描述符 dec %ecx // 重複256次 jne rp_idt lidt lidt_opcode // 載入LDTR ret // 顯示字元子程式。取當前游標位置並把AL中的字元顯示在螢幕上。整屏可顯示80X25個字元。 write_char: push %gs // 首先儲存要用到的暫存器,eax由呼叫者負責儲存 pushl %ebx mov $SCRN_SEL, %ebx // 然後讓GS指向顯示記憶體段(0xb8000) mov %bx, %gs movl scr_loc, %ebx // 再從變數scr_loc中取目前字元顯示位置值 shl $1, %ebx // 因為螢幕上每個字元還有一個屬性位元組,因此字元實際 movb %al, %gs:(%ebx) // 顯示位置對應的顯示記憶體偏移地址要乘2 shr $1, %ebx // 把字元放在顯示記憶體後把位置值除2加1,此時位置值對應 incl %ebx // 下一個顯示位置,如果該位置大於2000,則復位為0。 cmpl $2000, %ebx jb 1f // 沒有大於2000,跳到標號1f movl $0, %ebx // 復位為0 1: movl %ebx, scr_loc // 最後把這個位置值儲存起來(scr_loc) popl %ebx // 並彈出儲存的暫存器內容(恢復呼叫該子程式前ebx,gs的值),返回。 pop %gs ret // 以下是3箇中斷處理程式:預設中斷、定時中斷和系統呼叫中斷。 // ignore_int是預設的中斷處理程式,若系統產生了其他中斷,則會載螢幕顯示一個字元‘C’。 .align 2 // align是對齊的指令 (注意:之後來好好研究一下關於對齊這個問題) ignore_int: push %ds pushl %eax movl $0x10, %eax // 首先讓DS指向核心資料段,因為中斷程式屬於核心。 mov %ax, %ds movl $67, %eax // 在AL中存放字元'C'的程式碼,呼叫顯示程式顯示在螢幕上。 call write_char popl %eax pop %ds iret // 這是定時中斷處理程式。其中主要執行任務切換操作。 .align 2 timer_interrupt: push %ds pushl %eax movl $0x10, %eax // 首先讓DS指向核心資料段。 mov %ax, %ds movb $0x20, %al // 然後立刻允許其他硬體中斷,則向8253傳送EOI命令。 outb %al, $0x20 movl $1, %eax cmpl %eax, current je 1f movl %eax, current // 若當前任務是0,則把1存入current,並跳轉到任務1 ljmp $TSS1_SEL, $0 // 去執行。對於造成任務切換的長跳轉偏移值無用,但需要寫上。 jmp 2f 1: movl $0, current // 若當前任務是1,則把0存入current,並跳轉到任務0 ljmp $TSS0_SEL, $0 2: popl %eax pop %ds iret // 系統呼叫中斷int0x80處理程式。該示例只有一個顯示字元功能。 // 說明:system_interrup這個中斷處理程式將由兩個任務來呼叫。 .align 2 system_interrupt: push %ds pushl %edx push %ecx pushl %ebx pushl %eax movl $0x10, %edx // 首先讓DS指向核心資料段 mov %dx, %ds call write_char // 然後呼叫顯示字元子程式write_char, 顯示AL中的字元 popl %eax popl %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 // 這個16位數表示GDT的段限長 .long gdt // 這個32位數表示GDT的基地址 .align 8 idt: .fill 256,8,0 // IDT表空間。每個門描述符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: .long init_stack // 堆疊段偏移位置。 .word 0x10 // 堆疊段同核心資料段 // 下面是任務0的LDT表段中的區域性段描述符。 .align 8 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 ,krn_stk0為任務0核心棧頂指標*/ .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的核心棧空間。 任務0的使用者棧其實是init_stack,在IRET之前已經手工的把任務0的使用者棧基址入棧 krn_stk0: // 下面是任務1的LDT表段內容和TSS段內容 .align 8 ldt1: .quad 0x0000000000000000 // 第1個描述符,不用 .quad 0x00c0fa00000003ff // 選擇符是0x0f,基地址=0x00000。 .quad 0x00c0f200000003ff // 選擇符是0x17,基地址=0x00000。 tss1: .long 0 /* back link */ .long krn_stk1, 0x10 /* esp0, ss0 */ .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, trace bitmap */ .fill 128, 4, 0 // 這是任務1的核心棧空間。其使用者棧直接使用初始棧空間。 krn_stk1: // 下面是任務0和任務1的程式,他們分別迴圈顯示字元'A'和'B'。 task0: movl $0x17, %eax // 首先讓DS指向任務的區域性資料段。 movw %ax, %ds // 因為任務沒有使用區域性資料,所以這兩句可省略。 movb $65, %al // 把需要顯示的字元'A'放入暫存器中。 int $0x80 // 執行系統呼叫,顯示字元。 movl $0xfff, %ecx // 執行迴圈,起延時作用。 1: loop 1b // loop指令:每次迴圈CX遞減1。等到CX為0的時候就退出迴圈 jmp task0 // 跳轉到任務程式碼開始處繼續顯示字元。 task1: movb $66, %al // 把需要顯示的字元'B'放入暫存器中。 int $0x80 // 執行系統呼叫,顯示字元。 movl $0xfff, %ecx // 執行迴圈,起延時作用。 1: loop 1b jmp task1 // 跳轉到任務程式碼開始處繼續顯示字元。 .fill 128,4,0 // 這是任務1的使用者棧空間。 usr_stk1: