Linux 0.00 代码解析(一)
《Linux內核完全剖析》這本書在第4章給出了一個簡單多任務內核示例程序,作者稱之為Linux 0.00系統。
源碼的下載地址和實驗方法可以參考我的博文
http://blog.csdn.net/longintchar/article/details/78757065
本文想分析一下啟動代碼boot.s.
boot.s,采用as86語言編寫,是引導啟動程序,其作用是把內核代碼加載到內存0x10000處,之后設置好臨時GDT表等信息,再把處理器設置成保護模式,最后跳轉到內核代碼處運行。
我打算邊貼代碼邊分析。如有紕繆,還請各位看客拍磚指教。
BOOTSEG = 0x07c0 SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). SYSLEN = 17 ! sectors occupied.以上三行都是在定義符號常量,也就是說BOOTSEG 就是0x07c0. 屬于偽指令,并不會分配內存。
entry start start:jmpi go,#BOOTSEG go: mov ax,csmov ds,axmov ss,axmov sp,#0x400標識符entry是保留關鍵字,用于迫使鏈接器ld86在生成的可執行文件中包括其后指定的標號。通常在鏈接多個目標文件生成一個可執行文件時應該在其中一個匯編程序中用關鍵詞entry指定一個入口標號,以便調試。但是這里可以省略entry start,因為我們不希望在生成的純二進制文件中包括任何符號信息。
jmpi go,#BOOTSEG,是一個段間跳轉語句,跳轉到0x07c0:go處。當BIOS把主引導扇區(也就是boot.s生成的二進制鏡像)加載到物理內存0x7c00處并跳轉到該處時,所有段寄存器(包括CS)的默認值均為0,即此時CS:IP=0x0000:0x7c00。因此這里使用段間跳轉語句就是為了給CS賦值0x07c0。該語句執行后CS:IP = 0x07C0:go。
load_system:mov dx,#0x0000mov cx,#0x0002mov ax,#SYSSEGmov es,axxor bx,bxmov ax,#0x200+SYSLENint 0x13jnc ok_load die: jmp dieINT 13H,AH=02H 讀扇區
此功能從磁盤上把一個或更多的扇區內容讀進內存。因為這是一個低級功能,在一個操作中讀取的全部扇區必須在同一條磁道上。
| AH | =02H ,指明調用讀扇區功能。 |
| AL | 要讀的扇區數目,不允許使用讀磁道末端以外的數值,也不允許使該寄存器為0。 |
| DL | 需要進行讀操作的驅動器號,0表示軟盤,80H表示硬盤。 |
| DH | 所讀磁盤的磁頭號。 |
| CH | 磁道號的低8位數(磁道號共10位)。 |
| CL | 低5位放入所讀起始扇區號,位7-6表示磁道號的高2位。 |
| ES:BX | 讀出數據的緩沖區地址。 |
| 返回參數 | |
| CF | =1,則操作失敗;=0,操作成功。 |
| AH | 錯誤返回碼。 |
| AL | 實際讀到的扇區數。 |
所以,對照代碼,可以得出從軟盤的0磁頭,0磁道,從第2個扇區起,連續讀17個扇區到0x1000:0x0000(即0x10000)處。最后兩行表示判斷CF標志,如果標志置位說明出錯,則陷入死循環。否則跳轉到ok_load處。
ok_load:cli ! no interrupts allowed !mov ax, #SYSSEGmov ds, ax !ds=0x1000xor ax, axmov es, ax !es=0mov cx, #0x2000 !書上是0x1000sub si,si !si=0sub di,di !di=0repmovw !每次移動一個字上面的代碼表示把 DS:SI(0x1000:0x0)處的內容移動到ES:DI(0x0:0x0); CX 中是重復的次數(按0x1000算,就是4K),每次移動一個字(2B),所以一共移動了8KB(內核的長度不超過8KB)的代碼。
lidt idt_48 ! load idt with 0,0lgdt gdt_48 ! load gdt with whatever appropriate! absolute address 0x00000, in 32-bit protected mode.mov ax,#0x0001 ! protected mode (PE) bitlmsw ax ! This is it!jmpi 0,8 ! jmp offset 0 of segment 8 (cs)gdt: .word 0,0,0,0 ! dummy.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0x00000.word 0x9A00 ! code read/exec.word 0x00C0 ! granularity=4096, 386.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb).word 0x0000 ! base address=0x00000.word 0x9200 ! data read/write.word 0x00C0 ! granularity=4096, 386idt_48: .word 0 ! idt limit=0.word 0,0 ! idt base=0L gdt_48: .word 0x7ff ! gdt limit=2048, 256 GDT entries.word 0x7c00+gdt,0 ! gdt base = 07xxxlidt是加載IDTR(中斷描述符表寄存器),其后面要跟一個內存地址。在16位模式下,該地址是16位的;在32位模式下,該地址是32位的。該指令在實模式和保護模式下都可以用。地址指向一個6字節的內存區域,前16位是IDT的界限值,后32位是IDT的線性地址。
lgdt是加載GDTR(全局描述符表寄存器),用法與lidt類似。gdt_48處定義了6個字節,前兩個字節表示表的界限,0x7FF+1=2048(十進制),2048/8 = 256,也就是最多可以容納256個段描述符;后四個字節是GDT的線性基地址,0x7c00+gdt,因為主引導扇區被BIOS加載到了0x7c00處,所以要加個偏移。
9到19行定義了3個段描述符,第0個不用;
關于數據段描述符和代碼段描述符,可以參考我的博文:
http://blog.csdn.net/longintchar/article/details/50489889
第1個描述符定義了一個代碼段,其基地址為0,界限值是0x7FF(10進制2047),粒度4KB,DPL=0,非一致性,可讀可執行。因為粒度是4KB,所以段長度是(2047+1)*4KB=8MB。
第2個描述符定義了一個數據段,其基地址為0,界限值是0x7FF(10進制2047),粒度4KB,DPL=0,向上擴展,可讀可寫。同上,段長度是8MB。
再來解釋5~7行。
lmsw是加載機器狀態字指令,后接16位寄存器或者內存地址。其功能是用源操作數的低4位加載CR0,也就是說僅會影響CR0的低4位——PE, MP, EM, TS。
第6行執行完成后,保護模式就開啟了。
即使在實模式下,段寄存器的高速緩存寄存器也被用于訪問內存。當處理器進入保護模式后,高速緩存寄存器的內容依然殘留,但是這些內容在保護模式下是無效的。因此,比較安全的做法是盡快刷新段選擇器,包括描述符高速緩存寄存器。
另外,在進入保護模式之前,很多指令已經進入了流水線。因為處理器工作在實模式下,所以它們都是按照16位操作數和地址長度進行譯碼的,即使是那些用bits32編譯的指令,為了防止執行結果不正確,所以必須清空流水線。還用,那些通過亂序執行得到的中間結果也是無效的,所以必須清理掉,讓處理器串化執行。
為了達到上述目的,我們可以采用段間跳轉指令。jmpi 0,8執行后,處理器一般會清空流水線并且串化執行;另一方面,會重新加載CS,并刷新描述符高速緩存寄存器的內容。
jmpi 0,8,這里的0是偏移地址,8是段選擇子。段選擇子的結構如下圖:
TI=0表示描述符在GDT中,TI=1表示描述符在LDT中。描述符索引則表示第幾個描述符(從0開始)。
8即二進制的1000,也就是說是GDT表的第1個描述符,即基地址為0的代碼段。基地址0+偏移地址0=0,所以jmpi 0,8表示跳轉到物理地址0處,這正是內核代碼的起始位置,此后內核開始執行了。
源碼的最后兩行是
.org 510.word 0xAA55偽指令.org 510表示在它之后的指令從地址510開始存放。遇到.org,編譯器會把其后的指令代碼放到org偽指令指定的偏移地址。如org指定的地址和之前的指令地址有空洞,則用0填充。
.word 0xAA55是有效引導扇區的標志,第510字節必須是0x55,第511字節必須是0xAA.
【參考資料】
《Linux內核完全剖析》
總結
以上是生活随笔為你收集整理的Linux 0.00 代码解析(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux 0.00 的编译和运行
- 下一篇: oracle中synonym是什么,请问