bootloader詳解(轉載)
一。bootloader介紹
bootloader是硬體在加電開機後,除BIOS固化程式外最先執行的軟體,負責載入真正的作業系統,可以理解為一個超小型的os。目前在Linux平臺中主要有lilo、grub等,在Windows平臺上主要有ntldr、bootmgr、grldr等。這裡以grub-0.97為基礎描述bootloader的啟動過程。
一般grub主要分為stage1和stage2兩個階段。stage1作為啟動裝置的MBR存在於第一扇區,大小隻有512位元組。stage1載入位於第二扇區的start程式,然後start以磁碟扇區形式而非檔案系統形式載入stage2。stage2中包含了可以進行使用者互動的處理流程,實際上就是一個小型的os。通過stage2可以選擇決定載入的作業系統版本和相關引數,另外stage2還提供一些特殊功能,如加密、網路以及光碟啟動等。
如果grub支援stage1_5,則stage1載入的start不是直接去載入stage2,而是先載入stage1_5,然後通過stage1_5支援的檔案系統驅動,通過檔案系統載入stage2。
要特別指出的是,start.S即是stage1_5的開頭512位元組即start程式的原始碼,同時也是 stage2的開頭512位元組的原始碼,只是裡面一些具體的過程和引數因為條件編譯不同而不同,比如在編譯stage1_5的start時使用了 -DSTAGE1_5,而編譯stage2時則沒有。
stage1位於MBR扇區,即0面0磁軌的第1扇區,大小為512位元組(388位元組程式碼+58位元組BIOS引數塊BPB資訊+64位元組分割槽表+2位元組標誌55AA)。start程式位於0面0道第2扇區。系統如果支援stage1_5,stage1_5一般從0面 0磁軌的第3扇區開始,這時候stage2就可以以檔案方式載入,否則stage2一般就從0面0磁軌的第3扇區開始。這些都是grub在安裝到系統的時候就準備就緒的。
二。grub的啟動過程
1。系統加電,BIOS自檢硬體狀態,如CPU、記憶體、硬碟等資訊。
2。BIOS執行INT 0x19,讀取啟動裝置的MBR,即起始扇區的512位元組,實際上就是grub的stage1,將其載入至記憶體地址0x7c00處並跳轉執行。注意當最初的系統安裝時,在安裝grub的stage1到啟動裝置起始扇區的時候,grub的安裝程式會在stage1中嵌入stage1_5或者stage2的磁碟位置資訊,有了這個準備,stage1才可以在沒有檔案系統支援的情況下載入stage2。
3。stage1開始執行,載入位於第二扇區的start程式到0x2000(若支援stage1_5)或0x8000(不支援stage1_5),並跳轉執行。
4。執行_start(在檔案start.S中),若支援stage1_5,則載入stage1_5到0x2200,否則載入stage2到0x8200,並跳轉執行。
5。執行EXT_C(main) (在檔案asm.S中) ,通過EXT_C(init_bios_info)進入cmain。
6。執行cmain(在檔案stage1_5.c或stage2.c中都有)。若是支援stage1_5,則先進入stage1_5中的cmain,通過檔案系統載入stage2,然後執行chain_stage2,跳轉到stage2中的EXT_C(main) ,再進入EXT_C(init_bios_info),然後是stage2中的cmain,否則直接進入stage2中的cmain。
7。呼叫run_menu,可進行使用者互動選擇啟動核心。
8。執行run_script(在檔案cmdline.c中),依次執行menu.lst(grub.conf) 中的builtin->func,如root_func、kernel_func及initrd_func等(詳見檔案builtins.c中),最後執行boot_func啟動核心 。
三。硬碟工作模式和相關BIOS呼叫介紹
1。硬碟工作模式
現在的硬碟一般都支援邏輯塊定址(LBA)和柱面磁頭扇區定址(CHS)模式,CHS模式是指柱面/磁頭/扇區 (Cylinder/Head/Sector) 組成的3D定址方式。在磁碟的CHS定址方式中,資料傳輸的地址是寫到4個8位暫存器裡的,分別是:柱面低位暫存器、柱面高位暫存器、扇區暫存器和裝置/ 磁頭暫存器。
柱面地址是16位,即柱面低位暫存器(8位)加上柱面高位暫存器(8位)。扇區地址是8位(注意:扇區暫存器裡第一個扇區是1扇區,而不是0扇區)。而磁頭地址是4位(沒有完全佔用8位)。因此,硬碟柱面的最大數是65,536(2的16次方),磁頭的最大數是 16(2的4次方),扇區的最大數是255(2的8次方-1,注意剛剛我們提到的扇區暫存器問題)。所以,能定址的最大扇區數是267,386,880 (65,536x16x255)。一個扇區的大小是512位元組,也就是說如果以CHS定址方式,IDE硬碟的最大容量為136.9GB。
在LBA定址方式中,上述的總共28位可用的暫存器空間(16+8+4)被看作一個完整的LBA地址,因為包括了位0(在CHS模式中扇區不能從0開始計算),其能定址的扇區數是268,435,456 (65,536x16x256),這時IDE硬碟的最大容量為137.4GB。
特別要指出的是由於BIOS中int13缺陷所導致的528MB和8.4GB限制
早先的硬碟容量比較小,所以在設計BIOS時,在把定址地址從Int 13的地址暫存器轉換為IDE(ATA)的地址暫存器時,僅僅把int13中10位的柱面地址對應IDE(ATA)介面中的16位柱面暫存器,而把沒有用到的6位(高位暫存器)地址都設定為0。同時僅把6位的扇區地址來對應IDE(ATA)介面的8位扇區暫存器,其中沒有用到的2位設定為0。並且僅使用 int13中的磁頭暫存器4位(又去掉了4位)來對應IDE(ATA)。因此,此時的磁碟柱面最大數為1024(2的10次方),磁頭的最大數是16(2 的4次方),扇區的最大數是63(2的6次方-1)。所以能定址的扇區數就成了1,032,192(1,024x16x63)。一個扇區的容量是512位元組,也就是說如果以CHS定址方式,IDE硬碟的最大容量為528.4MB。因此528MB的硬碟容量限制就出現了。
後來儘管EIDE介面對普通IDE介面進行了擴充套件,支援了LBA存取方式,突破了528MB的容量限制,理論上可以支援到128G的硬碟容量。但老式的BOIS卻繼續使用10bit表示柱面數,8bit表示磁頭數,6bit表示扇區數,因此老式BOIS最多可以支援 8.4GB的容量(512×63×255×1024=8.4GB)。
在目前新設計的BIOS中,新的int13不使用原有的暫存器傳遞硬碟的定址引數,它使用的是儲存在作業系統記憶體裡的地址包(沒有作業系統支援仍然有問題)。地址包裡儲存的是64位LBA地址,如果硬碟支援LBA定址,就把低28位直接傳遞給ATA介面,如果不支援,作業系統就先把LBA地址轉換為CHS地址,再傳遞給ATA介面。通過這種方式,能實現在ATA匯流排基礎上CHS定址最大容量是136.9GB,而 LBA定址最大容量是137.4GB。
同時隨著ATA-6規範以及48-Bit LBA Adress的規範的實施和發展,再加上ICH4以上南橋的支援,目前早已經突破硬碟所遇到的137.4GB 問題。
Maxtor是推出48-Bit LBA Address規範最早的公司,其中心思想就是增加CHS的位數,在48-Bit LBA Adress規範中,把扇區地址設定為16位的暫存器,磁頭的地址暫存器也設為16位,柱面地址暫存器不變。這樣在LBA定址中可用的暫存器空間就從28 位提高到了48位(16+16+16),可以定址的扇區數就為281,474,976,710,655(65,536x65,535x65,536),整個硬碟的容量就是281,474,976,710,655x512=144,115,188,075,855,872位元組,大約等於 144PB(1PB=1000,000,000,000,000位元組)。48位LBA定址基本上就可以支援非常大容量硬碟的定址了。
2。相關BIOS呼叫
這裡指的BIOS呼叫主要是int13的相關磁碟功能,有興趣可以參考中斷大全。
2.1。功能0x41
檢查磁碟是否支援LBA,例如:
movb $0x41, %ah
movw $0x55aa, %bx
int $0x13
2.2。功能0x42
從指定扇區讀資料到記憶體
%dl可以從功能0x41中獲得,是裝置號,磁碟為0x80
%ds:%si是指定的記憶體地址
2.3。功能0x8
獲取磁碟引數
2.4。功能0x2
讀取指定扇區資料到記憶體
%al是扇區個數
%ch是柱面號
%cl是扇區號,第6、7位是柱面號高位
%dh是磁頭
%dl是裝置,0x80是磁碟,0x0是軟碟機
%es:%bx是指定的記憶體地址
四。MBR(stage1)詳解
* MBR 獲取: dd if=/dev/sda of=mbr bs=512 count=1 * MBR 反彙編, nasm工具集中ndisam mbr :
於是利用bview將0x4a之前資料先刪掉再反彙編
一個實際啟動硬碟的MBR內容,大小為512位元組。
下面我們分析MBR的內容。我們使用AT&T組合語言,通過對MBR的反彙編來解釋MBR啟動。注意在系統啟動時MBR會被BIOS載入到記憶體的0x7c00位置。
1。啟動跳轉
00000000h:EB 48 :jmp $0x0000004a ;跳轉到0x0000004a位置執行,實際是0x00000048+2(EB 48所佔的兩個位元組)
00000003h:90:nop
2。引數資訊
00000004h至0000003dh是BIOS引數塊BPB。
0000003eh:03:COMPAT_VERSION_MAJOR版本號
0000003fh:02:COMPAT_VERSION_MINOR版本號
00000040h:FF:GRUB_INVALID_DRIVE,載入stage2標記
00000042h:00 20:start程式載入到的地址0x2000,實際上從這裡就可以看出來,此bootloader是支援stage1_5的
00000044h:01 00 00 00:start程式的扇區位置
00000048h:00 02:start程式的段地址0x0200
3。啟動磁碟的檢查以及載入start程式前的準備
0000004ah:FA :cli ;清中斷標記
0000004bh:90 90 :nop nop
0000004dh:F6 C2 80 :testb $0x80, %dl :這是為了避免一些有問題的BIOS沒有將啟動裝置放到%dl中
00000050h:75 02 :jnz $0x00000054 :如果測試為非0,則認為GRUB被安裝到軟碟機上,直接跳轉
00000052h:B2 80 :movb $0x80, %dl :如果%dl沒有被設定,就將其設定為0x80
00000054h:EA 59 7C 00 00 :ljmp $0x00007c59 ;長跳轉到0x7c59,實際上就是這裡的0x0059,因為磁碟上的0x0000就對應記憶體中的0x7c00,使用長跳轉是為了避免有問題的BIOS跳轉到07c0:0000而不是0000:7c00
00000059h:31 C0 :xorw %ax, %ax
0000005bh:8E D8:movw %ax, %ds
0000005dh:8E D0:movw %ax, %ss;設定%ds和%ss為0
0000005fh:BC 00 20:movw $0x2000, %sp; 設定棧啟始從0x2000開始
00000062h:FB:sti;設定中斷標誌
00000063h:A0 40 7C:movb $(0x7c40), %al ;實際就是0x40處的內容,0x7c40-0x7c00,這裡就是0xFF
00000066h:3C FF:cmpb $0xFF, %al ;檢查是否有設定了GRUB_INVALID_DRIVE標記,確認%al中是否0xFF
00000068h:74 02:je $0x0000006c :相等的話跳到0x0000006c
0000006ah:88 C2:movb %al, %dl ,將0xFF儲存到%dl
0000006ch:52:pushw %dx
0000006dh:BE 7F 7D:movw $(0x7d7f), %si;取0x7d7f-0x7c00=0x17f處的內容 ,當前為GRUB
00000070h:E8 34 01:call $0x01a7;即0x0134+0x70+0x03=0x01a7 ,實際上是呼叫message過程在螢幕上列印GRUB字樣
4。判斷磁碟模式,CHS還是LBA
00000073h:F6 C2 80:testb $0x80, %dl ;如果是軟碟機(0x80)的話就不進行LBA判斷
00000076h:74 54:jz $0x00cc;0x76+0x54+0x2=0xcc ,如果比較結果為0,是軟碟機,直接跳轉到CHS模式
00000078h:B4 41:movb $0x41, %ah
0000007ah:BB AA 55:movw $0x55aa, %bx
0000007dh:CD 13:int $0x13 ;呼叫int13的0x41檢查磁碟是否支援LBA模式
0000007fh:5A:popw %dx
00000080h:52:pushw %dx
00000081h:72 49:jc $0x00cc ;出錯跳轉到CHS模式
00000083h:81 FB 55 AA:cmpw $0xaa55, %bx
00000087h:75 43:jne $0x00cc ;不相等跳轉到CHS模式
00000089h:A0 41 7C:$(0x7c41), %al ;取0x0041處內容,是否強制為LBA(grub安裝時可以強制LBA),目前為0,不是強制LBA
0000008ch:84 C0:testb %al, %al
0000008eh:75 05:jnz $0x0095 ;若不為0,是強制LBA,跳轉到LBA模式
00000090h:83 E1 01:andw $1, %cx
00000093h:74 37:jz $0x00cc ;若為0,跳轉到CHS模式,顯然在這裡為0,所以實際上是進入CHS模式
5。使用LBA模式讀取start程式,讀取到記憶體0x7000處
00000095h:66 8B 4C 10:movl 0x10(%si), %ecx ;這裡是LBA模式的入口,儲存扇區數目到%ecx
00000099h:BE 05 7C:movw $(0x7c05), %si
0000009ch:C6 44 FF 01:movb $1, -1(%si) :設定非零模式
000000a0h:66 8B 1E 44 7C:movl $(0x7c44), %ebx :儲存扇區位置到%ebx ,這裡為1,實際上就是第2扇區
000000a5h:C7 04 10 00:movw $0x0010, (%si)
000000a9h:C7 44 02 01 00:movw $1, 2(%si)
000000aeh:66 89 5c 08:movl %ebx, 8(%si) ;計算扇區的LBA絕對地址
000000b2h:C7 44 06 00 70:movw $0x7000, 6(%si)
000000b7h:66 31 C0:xorl %eax, %eax
000000bah:89 44 04:movw %ax, 4(%si)
000000bdh:66 89 44 0C:movl %eax, 12(%si)
000000c1h:B4 42:movb $0x42, %ah
000000c3h:CD 13:int $0x13 ;使用int13的功能42將LBA指定的磁碟資料拷貝到0x7000
000000c5h:72 05:jc $0x00cc ;如果出錯;則跳轉到CHS模式
000000c7h:BB 00 70:movw $0x7000, %bx
000000cah:EB 7D:jmp $0x0149 ;跳轉到移動資料到指定位置的呼叫入口
6。使用CHS模式讀取start程式,讀取到記憶體0x7000處
000000cch:B4 08:movb $8, %ah ;這裡是CHS模式的入口,int13功能8為獲取驅動器引數
000000ceh:CD 13:int $0x13 ;呼叫BIOS決定磁碟的geometry
000000d0h:74 0A:jnc $0x00dc ;情況正常進入處始化過程
000000d2h:F6 C2 80:testb $0x80, %dl
000000d5h:0F 84 EA 00:jz $0x01c3 ;呼叫失敗,如果%dl為0x80則探測軟盤
000000d9h:E9 8D 00:jmp $0x0169 ;否則列印硬碟錯誤
000000dch:BE 05 7C:movw $(0x7c05), %si ;CHS初始化過程開始
000000dfh:C6 44 FF 00:movb $0, -1(%si) ;設定模式為0
000000e3h:66 31 C0:xorl %eax, %eax ;儲存磁頭數開始
000000e6h:88 F0:movb %dh, %al
000000e8h:40:incw %ax
000000e9h:66 89 44 04:movl %eax, 4(%si)
000000edh:31 D2:xorw %dx, %dx
000000efh:88 CA:movb %cl, %dl
000000f1h:C1 E2 02:shlw $2, %dx
000000f4h:88 E8:movb %ch, %al
000000f6h:88 F4:movb %dh, %ah ;儲存磁頭數結束
000000f8h:40:incw %ax ;儲存柱面數開始
000000f9h:89 44 08:movw %ax, 8(%si)
000000fch:31 C0:xorw %ax, %ax
000000feh:88 D0:movb %dl, %al
00000100h:C0 E8 02:shrb $2, %al ;儲存柱面數結束
00000103h:66 89 04:movl %eax, (%si);儲存扇區數
00000106h:66 A1 44 7C:movl $(0x7c44), %eax ;從0x44位置載入邏輯啟始扇區地址,這裡為1,實際上是就第2扇區
0000010ah:66 31 D2:xorl %edx, %edx ;清0
0000010dh:66 F7 34:divl (%si) ;除以扇區數
00000110h:88 54 0A:movb %dl, 10(%si) ;儲存啟始扇區
00000113h:66 31 D2:xorl %edx, %edx ;清0
00000116h:66 F7 74 04:divl 4(%si) ;除以磁頭數
0000011ah:88 54 0B:movb %dl, 11(%si) ;儲存啟始磁頭
0000011dh:89 44 0C:movw %ax, 12(%si) ;儲存啟始柱面
00000120h:3B 44 08:cmpw 8(%si), %ax ;柱面是否超出
00000123h:7D 3C:jge $0x0161 ;若大於等於則出Geom錯誤
00000125h:8A 54 0D:movb 13(%si), %dl ;獲取柱面的高位
00000128h:C0 E2 06:shlb $6, %dl ;平移6位
0000012bh:8A 4C 0A:movb 10(%si), %cl ;獲取扇區
0000012eh:FE C1:incb %cl
00000130h:08 D1:orb %dl, %cl
00000132h:8A 6C 0C:movb 12(%si), %ch ;將扇區+柱面高位放到cl,將柱面放到ch
00000135h:5A:popw %dx
00000136h:8A 74 0B:movb 11(%si), %dh ;磁頭號
00000139h:BB 00 70:movw $0x7000, %bx
0000013ch:8E C3:movw %bx, %es
0000013eh:31 DB:xorw %bx, %bx
00000140h:B8 01 02:movw $0x0201, %ax
00000143h:CD 13:int $0x13 ;int13功能0x2,將指定扇區內容讀到0x7000
00000145h:72 24:jc $0x0171 ;磁碟讀錯誤則跳轉
7。將start程式從0x7000移動到指定的啟始地址位置,在這裡是0x2000,並跳轉到start程式
00000147h:8C C3:movw %es, %bx
00000149h:8E 06 48 7C:movw $(0x7c48), %es;將0x7000的內容拷貝到0x0048指定的地址,這裡是0x0200:0x0000
0000014dh:60:pusha
0000014eh:1E:pushw %ds
0000014fh:B9 00 01:movw $0x100, %cx
00000152h:8E DB:movw %bx, %ds
00000154h:31 F6:xorw %si, %si
00000156h:31 FF:xorw %di, %di
00000158h:FC:cld
00000159h:F3 A5:rep movsw ;串移動
0000015bh:1F:popw %ds
0000015ch:61:popa
0000015dh:FF 26 42 7C:jmp $(0x7c42) ;跳轉到0x2000處執行,進入start階段
8。一些基本的函式呼叫
00000161h:BE 85 7D:movw $0x7d85, %si ;geometry_error呼叫
00000164h:E8 40 00:call $0x01a7
00000167h:EB 0E:jmp $0x0177
00000169h:BE 8A 7D:movw $0x7d8a, %si ;hd_probe_error呼叫
0000016ch:E8 38 00:call $0x01a7
0000016fh:EB 06:jmp $0x0177
00000171h:BE 94 7D:movw $0x7d94, %si ;read_error呼叫
00000174h:E8 30 00:call $0x01a7
00000177h:BE 99 7D:movw $0x7d99, %si ;general_error呼叫
0000017ah:E8 2A 00:call $0x01a7
0000017dh:EB FE:jmp $0x017d ;進入死迴圈
。。。
000001a0h:BB 01 00:movw $0x0001, %bx
000001a3h:B4 0E:movb $0xe, %ah
000001a5h:CD 10:int $0x10
000001a7h:AC:lodsb ;在螢幕上顯示訊息的呼叫
000001a8h:3C 00:cmpb $0, %al
000001aah:75 F4:jne $0x01a0
000001ach:C3:ret
下圖是對應的未安裝到啟動硬碟前的原始stage1內容,大小也為512位元組。
對比MBR,我們可以看到被修改的地址有:
0x43:80 -> 20 實際上就是stage2的啟始原先是0x8000的,在這個例項中,由於支援stage1_5,在安裝grub時被setup_func修改成0x2000了。
0x49:08 -> 02 原來是0x0800,現在是0x0200,成為stage1_5的段地址,。
0x4b至0x4c:EB 07 -> 90 90 這是將原來的一個jmp指令改為nop 。
0x1be至0x1fc:實際上包含了分割槽表資訊。
最後大家可以通過直接分析stage1.S檔案進一步理解stage1的工作過程。
五。start程式的作用
start位於第2個扇區,在此例項中實際資料如下所示:
在此扇區中,0x01fe開始的內容0x0220是下一次轉載到的段地址,0x01fc開始的內容0x000e是 start需要讀取的扇區數目,從0x01f8開始的0x00000002是start讀取的啟始扇區,實際上是第三扇區。在這裡我們通過分析 start.S來解析處理過程。
_start:
pushw %dx pushw %si MSG(notification_string) ;列印"Loading stage1.5"資訊到螢幕 popw %si movw $ABS(firstlist - BOOTSEC_LISTSIZE), %di ;獲取接下來要讀取的磁碟扇區號,儲存在偏移位置0x01f8,這裡的firstlist是start的末尾地址,定義為8,這裡%di值為0x2,實際上就是第三扇區 movl (%di), %ebp bootloop:
cmpw $0, 4(%di) ;4(%di)是0x01fc,具體資料在這裡是0x000e,代表的是還要讀的扇區數目,這裡檢查讀完所有扇區沒有 je bootit ;讀完了就跳轉到bootit了 setup_sectors:
cmpb $0, -1(%si) ;檢查是LBA還是CHS模式 je chs_mode lba_mode:
movl (%di), %ebx ;獲取啟始扇區號 xorl %eax, %eax movb $0x7f, %al ;最大讀取扇區數目不超過0x7f,這是由於Phoenix EDD的限制 cmpw %ax, 4(%di) ;看看需要讀取的扇區數目是否大於0x7f jg 1f ;大於的話,跳到1: movw 4(%di), %ax ;小於就賦需要讀取的扇區數目給%ax 1:
subw %ax, 4(%di) ;將%ax內值減去需要讀取的扇區數目 addl %eax, (%di) ;加上啟始扇區號 movw $0x0010, (%si) ;保留空間 movw %ax, 2(%si) movl %ebx, 8(%si) ;計算絕對扇區地址(低32位) movw $BUFFERSEG, 6(%si) ;設定讀取到的記憶體地址 pushw %ax xorl %eax, %eax movw %ax, 4(%si) movl %eax, 12(%si) ;計算絕對扇區地址(高32位) movb $0x42, %ah int $0x13 ;使用int13的功能0x42讀取扇區資料 jc read_error ;讀取錯誤則報錯 movw $BUFFERSEG, %bx jmp copy_buffer ;跳轉到資料拷貝 chs_mode:
movl (%di), %eax xorl %edx, %edx divl (%si) movb %dl, 10(%si) xorl %edx, %edx divl 4(%si) movb %dl, 11(%si) movw %ax, 12(%si) cmpw 8(%si), %ax jge geometry_error ;geometry錯誤處理 movw (%si), %ax subb 10(%si), %al cmpw %ax, 4(%di) jg 2f movw 4(%di), %ax 2:
subw %ax, 4(%di) addl %eax, (%di) movb 13(%si), %dl shlb $6, %dl movb 10(%si), %cl incb %cl orb %dl, %cl movb 12(%si), %ch popw %dx pushw %dx movb 11(%si), %dh pushw %ax movw $BUFFERSEG, %bx ;要將扇區資料讀到的記憶體地址0x7000 movw %bx, %es xorw %bx, %bx movb $0x2, %ah int $0x13 ;讀取扇區資料到記憶體 jc read_error ;讀錯誤處理 movw %es, %bx copy_buffer:
movw 6(%di), %es ;6(%di)內容在0x01fe,值是0x0220,是要將資料載入到的段地址 popw %ax shlw $5, %ax addw %ax, 6(%di) pusha pushw %ds shlw $4, %ax movw %ax, %cx xorw %di, %di xorw %si, %si movw %bx, %ds cld rep movsb ;拷貝資料 popw %ds MSG(notification_step) ;列印資訊 popa cmpw $0, 4(%di) jne setup_sectors ;看是否讀取完所有扇區 subw $BOOTSEC_LISTSIZE, %di jmp bootloop bootit:
MSG(notification_done) ;列印結束資訊 popw %dx ljmp $0, $0x2200 ;跳轉到準備好的入口 geometry_error:
MSG(geometry_error_string) ;列印錯誤資訊"Geom" jmp general_error read_error:
MSG(read_error_string) ;列印錯誤資訊"Read" general_error:
MSG(general_error_string) ;列印錯誤資訊"Error" stop: jmp stop ;進入死迴圈
到此為止,start已經把第三扇區後的0x0e個扇區都讀入從0x2200開始的記憶體中了。
六。真正的入口 - EXT_C(main)
EXT_C(main) 在檔案asm.S中,就是從地址0x2200開始的入口呼叫。在這裡只做主要流程的分析。
ENTRY(main):
ljmp $0, $ABS(codestart) codestart::
cli ;清中斷 xorw %ax, %ax movw %ax, %ds movw %ax, %ss movw %ax, %es ;設定%ds、%ss和%es movl $STACKOFF, %ebp ;設定真實模式棧 movl %ebp, %esp sti ;開中斷 DATA32 call EXT_C(real_to_prot) ;轉換真實模式到保護模式 subl %edi, %ecx ;計算bss長度 xorb %al, %al cld rep stosb call EXT_C(init_bios_info) ;從這裡開始就進入到c語言的程式碼呼叫了 在init_bios_info 中呼叫了stage1_5的cmain,此時已經載入了檔案驅動,可以將stage2通過檔案系統方式讀入到地址0x8000處,然後執行ENTRY(chain_stage2)。下面看檔案stage2/stage1_5.c中的cmain。
grub_open (config_file);開啟stage2檔案
grub_read ((char *) 0x8000, SECTOR_SIZE * 2);讀取2個扇區的內容到地址0x8000
ret = grub_read ((char *) 0x8000 + SECTOR_SIZE * 2, -1);讀取其餘資料
chain_stage2 (0, 0x8200, saved_sector);具體函式在檔案stage2/asm.S中
在ENTRY(chain_stage2)中,首先是EXT_C(prot_to_real)退出保護模式,最後跳轉到從地址0x8200處開始執行,實際上就是跳過start,再次進入EXT_C(main)。
movl 0x8(%esp), %eax;取出棧中第一個引數(%esp+8)的內容放到%eax中
movl %eax, offset;實際上是將第一個引數0放到offset
movl %eax, %ebx
movw 0x4(%esp), %ax;取出棧中第二個引數(%esp+4)的內容放到%ax中
movw %ax, segment;實際上就是0x8200
shll $4, %eax;左移4位,得到0x0820
addl %eax, %ebx;產生線性地址0x0820:0000
movl 0xc(%esp), %ecx;將saved_sector賦給%ecx
call EXT_C(prot_to_real);從保護模式進入真實模式
DATA32 ADDR32 ljmp (offset);跳轉到0x0820:0000,即進入stage2的EXT_C(main)
第二次進入EXT_C(main),前面執行的內容和第一次進入一樣,只不過這一次cmain不是上一次stage1_5的cmain了,真正進入了stage2的cmain,即grub的互動處理迴圈了,主要步驟如下:
run_menu;處理使用者鍵盤指令和使用者選擇選單的命令,如游標上下移動、修改啟動引數、選擇啟動選項等。
run_script;處理使用者選擇的啟動選項中的命令,如root、kernel、initrd等命令,注意最後系統會自己加上boot命令。
builtin->func;具體執行root_func、kernel_func、initrd_func和boot_func命令。
七。kernel_func - 載入核心
在grub的stage2中的檔案builtins.c中有一個結構builtin_table,是所有grub 支援命令的函式對應表,其中設計核心啟動的主要有kernel_func和boot_func,另外setup_func是設計bootloader安裝的處理,在這裡也做介紹。
kernel_func 是將核心載入到記憶體指定地址的處理。
首先指定核心引數地址。
mb_cmdline = (char *) MB_CMDLINE_BUF;MB_CMDLINE_BUF=0x2000
grub_memmove (mb_cmdline, arg, len + 1);;將核心引數移動到0x2000
load_image (arg, mb_cmdline, suggested_type, load_flags);開始栽入核心
在load_image中使用了檔案系統讀取fsys_table,這裡就不詳細介紹了。
grub_open (kernel);開啟核心檔案,在這裡即bzImage檔案
grub_read (buffer, MULTIBOOT_SEARCH);讀取開頭MULTIBOOT_SEARCH=8192個位元組到buffer ,如下圖所示部分內容:
lh = (struct linux_kernel_header *) buffer;;在這裡lh是linux_kernel_header結構指標,具體可以參考Linux啟動協議的定義
這時候一定是lh->boot_flag == BOOTSEC_SIGNATURE && lh->setup_sects <= LINUX_MAX_SETUP_SECTS;BOOTSEC_SIGNATURE=0xAA55,LINUX_MAX_SETUP_SECTS=64,分別見偏移0x1fe和0x1f1,lh->setup_sects=0x0a
int setup_sects = lh->setup_sects;核心setup部分佔的扇區數目,在這裡就是0x0a
lh->type_of_loader = LINUX_BOOT_LOADER_TYPE;指定type_of_loader為LINUX_BOOT_LOADER_TYPE=0x71
linux_data_real_addr = (char *) ((mbi.mem_lower << 10) - LINUX_SETUP_MOVE_SIZE);LINUX_SETUP_MOVE_SIZE=0x9100,mbi.mem_lower是系統低位記憶體大小,一般為640k
if (linux_data_real_addr > (char *) LINUX_OLD_REAL_MODE_ADDR)
linux_data_real_addr = (char *) LINUX_OLD_REAL_MODE_ADDR;LINUX_OLD_REAL_MODE_ADDR=0x90000 ;如果linux_data_real_addr 大於0x90000,則實際資料地址不能超過0x90000
lh->heap_end_ptr = LINUX_HEAP_END_OFFSET;設定heap_end_ptr ,LINUX_HEAP_END_OFFSET=0x9000 - 0x200
lh->loadflags |= LINUX_FLAG_CAN_USE_HEAP;設定loadflags,LINUX_FLAG_CAN_USE_HEAP=0x80
lh->cmd_line_ptr = linux_data_real_addr + LINUX_CL_OFFSET;設定cmd_line_ptr,核心即引數位置,LINUX_CL_OFFSET=0x9000
data_len = setup_sects << 9;獲得bzImage中真實模式程式碼setup部分的大小,這裡是0x0a<<9,即0x1400位元組
text_len = filemax - data_len - SECTOR_SIZE;;獲得bzImage其餘部分,即保護模式程式碼的大小
linux_data_tmp_addr = (char *) LINUX_BZIMAGE_ADDR + text_len;設定臨時指標到地址0x100000+保護模式程式碼尺寸之後
grub_memmove (linux_data_tmp_addr, buffer, MULTIBOOT_SEARCH);將開始時候讀取buffer的內容放到0x100000+保護模式程式碼之後,即將bootsect和setup程式碼開頭部分放到0x100000+保護模式程式碼之後
grub_read (linux_data_tmp_addr + MULTIBOOT_SEARCH, data_len + SECTOR_SIZE - MULTIBOOT_SEARCH);將真實模式程式碼讀全了
char *src = skip_to (0, arg);
char *dest = linux_data_tmp_addr + LINUX_CL_OFFSET;將核心引數拷貝到0x100000+保護模式程式碼尺寸+0x9000後
while (dest < linux_data_tmp_addr + LINUX_CL_END_OFFSET && *src)
*(dest++) = *(src++);最多拷貝0xff個位元組,到0x90FF,所以bootsect+setup到核心引數結束總共為0x9100位元組
grub_seek (data_len + SECTOR_SIZE);重新將檔案指標定位到保護模式程式碼
grub_read ((char *) LINUX_BZIMAGE_ADDR, text_len);將保護模式程式碼拷貝到0x100000
到這裡,我們就可以瞭解到grub將核心載入後的內容地址分佈圖了:
0x100000開始,是核心保護模式以後程式碼
0x100000+保護模式程式碼尺寸開始,是核心bootsec和真實模式setup部分程式碼,在這裡bootsect為512位元組,setup為0x1400位元組
0x100000+保護模式程式碼尺寸+0x9000開始,是核心引數命令,一共0xff個位元組
八。boot_func - 啟動核心
boot_func是grub啟動核心時的操作,其對核心內容的資料又做了一些修改。
big_linux_boot 位於asm.S檔案中,主要操作如下:
1。調整核心bootsect和setup真實模式資料位置
將linux_data_tmp_addr(地址0x100000)處真實模式程式碼移到linux_data_real_addr(地址0x90000),移動尺寸大小為LINUX_SETUP_MOVE_SIZE=0x9100,這樣把引數也移過去了。
在load_image裡已經指出linux_data_real_addr最大為LINUX_OLD_REAL_MODE_ADDR=0x90000 ,這樣核心的實際內容地址分佈又成了:
0x90000開始,是核心bootsect和真實模式setup的執行程式碼
0x90000+0x9000開始,是核心引數,共0xff個位元組
0x100000開始,是核心保護模式程式碼
2。填寫要跳轉到的段地址
movl EXT_C(linux_data_real_addr), %ebx ;%ebx為0x90000
shrl $4, %ebx
movl %ebx, %eax
addl $0x20, %eax ;%eax為0x9020
movl %eax, linux_setup_seg;這樣下面要跳轉的linux_setup_seg地址是就是linux_data_real_addr+ 0x200的段地址,平移4位即段地址0x9020:0000,同時跳過了bootsect的0x200位元組,直接執行到setup真實模式程式碼
3。返回真實模式
call EXT_C(prot_to_real) ;在EXT_C(main) 中已經介紹,在stage2中進入保護模式,這裡又回到真實模式,因為核心啟始部分還是真實模式程式碼
4。設定核心棧,跳轉到核心setup真實模式
movw %bx, %ss;注意此時%bx是0x9000 movw $LINUX_SETUP_STACK, %sp;LINUX_SETUP_STACK=0x9000 movw %bx, %ds movw %bx, %es movw %bx, %fs movw %bx, %gs;將所有段地址賦值,0x9000:0000 byte 0xea word 0 linux_setup_seg:
word 0 可以看到linux_setup_seg是上面的0x9020段地址,這樣跳轉到的就是0x9020:0000即0x90200。
九。setup_func - 安裝grub
安裝grub時最關鍵的是修改了stage1和stage2裡的一些內容,具體操作在install_func中。修改的內容主要有:
1。修改stage1中一些引數
*((unsigned char *) (stage1_buffer + STAGE1_BOOT_DRIVE)) = new_drive;設定啟動裝置
*((unsigned char *) (stage1_buffer + STAGE1_FORCE_LBA)) = is_force_lba;設定是否強制LBA
*((unsigned long *) (stage1_buffer + STAGE1_STAGE2_SECTOR)) = stage2_first_sector;設定stage1_5或stage2啟始扇區號
*((unsigned short *) (stage1_buffer + STAGE1_STAGE2_ADDRESS)) = installaddr;設定stage1_5或stage2的載入地址,前者0x2000,後者為0x8000
*((unsigned short *) (stage1_buffer + STAGE1_STAGE2_SEGMENT)) = installaddr >> 4;載入的段地址
2。修改stage2中一些引數
*((unsigned char *) (stage2_second_buffer + STAGE2_FORCE_LBA)) = is_force_lba;設定是否強制LBA
十。grub在記憶體中的對映表
0 to 4K-1
BIOS和真實模式中斷 0x07BE to 0x07FF
可以傳遞到另外的bootloader的分割槽表 down from 8K-1
真實模式使用的棧 0x2000 to ?
stage1_5載入的啟始地址 0x2000 to 0x7FFF
多啟動核心以及模組的命令列快取 0x7C00 to 0x7DFF
BIOS或其它bootloader將stage1載入的啟始地址 0x7F00 to 0x7F42
LBA裝置引數 0x8000 to ?
stage2載入的啟始地址 The end of Stage 2 to 416K-1
stage2選單使用的堆 down from 416K-1
保護模式使用的棧 416K to 448K-1
檔案系統快取 448K to 479.5K-1
Raw裝置快取 479.5K to 480K-1
512-byte擴充套件空間 480K to 512K-1
變數引數,如口令、命令列、拷貝貼上的快取 The last 1K of lower memory
磁碟交換程式碼和資料
一。獲得可執行的Linux核心
當我們從www.kernel.org獲得Linux原始碼並正確編譯後,在原始碼根目錄下會生成檔案vmlinux,同時在arch/i386/boot/目錄下會生成bzImage檔案。下面我們看看vmlinux和bzImage分別是如何得到的。沒有特殊說明,本系列中Linux的參考物件都為版本2.6.22。
1。vmlinux的獲得
vmlinux是Linux原始碼編譯後未壓縮的核心,我們檢視原始碼根目錄下的.vmlinux.cmd檔案,可以看到:
cmd_vmlinux := ld -m elf_i386 -m elf_i386 -o vmlinux -T arch/i386/kernel/vmlinux.lds arch/i386/kernel/head.o arch/i386/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/i386/kernel/built-in.o arch/i386/mm/built-in.o arch/i386/mach-default/built-in.o arch/i386/crypto/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o lib/lib.a arch/i386/lib/lib.a lib/built-in.o arch/i386/lib/built-in.o drivers/built-in.o sound/built-in.o arch/i386/pci/built-in.o net/built-in.o --end-group .tmp_kallsyms2.o
這說明vmlinux是由arch/i386/kernel/head.o和arch/i386/kernel /init_task.o以及各個相關子目錄下的built-in.o連結而成的。注意按照連結順序我們可以發現arch/i386/kernel /head.S的目標檔案似乎比較靠前。
2。bzImage的獲得
bzImage是核心的壓縮版本,一般可以是vmlinux大小的三分之一左右。
首先檢視生成bzImage的連結檔案arch/i386/boot/.bzImage.cmd
cmd_arch/i386/boot/bzImage := arch/i386/boot/tools/build -b arch/i386/boot/bootsect arch/i386/boot/setup arch/i386/boot/vmlinux.bin CURRENT > arch/i386/boot/bzImage
接下去根據線索我們檢視生成vmlinux.bin的連結檔案arch/i386/boot/.vmlinux.bin.cmd
cmd_arch/i386/boot/vmlinux.bin := objcopy -O binary -R .note -R .comment -S arch/i386/boot/compressed/vmlinux arch/i386/boot/vmlinux.bin
然後檢視生成vmlinux的連結檔案arch/i386/boot/compressed/.vmlinux.cmd
cmd_arch/i386/boot/compressed/vmlinux := ld -m elf_i386 -m elf_i386 -T arch/i386/boot/compressed/vmlinux.lds arch/i386/boot/compressed/head.o arch/i386/boot/compressed/misc.o arch/i386/boot/compressed/piggy.o -o arch/i386/boot/compressed/vmlinux
接下去檢視生成piggy.o的連結檔案arch/i386/boot/compressed/.piggy.o.cmd
cmd_arch/i386/boot/compressed/piggy.o := ld -m elf_i386 -m elf_i386 -r --format binary --oformat elf32-i386 -T arch/i386/boot/compressed/vmlinux.scr arch/i386/boot/compressed/vmlinux.bin.gz -o arch/i386/boot/compressed/piggy.o
然後接下去檢視生成vmlinux.bin.gz的連結檔案arch/i386/boot/compressed/.vmlinux.bin.gz.cmd
cmd_arch/i386/boot/compressed/vmlinux.bin.gz := gzip -f -9 < arch/i386/boot/compressed/vmlinux.bin > arch/i386/boot/compressed/vmlinux.bin.gz
最後我們檢視生成vmlinux.bin的連結檔案arch/i386/boot/compressed/.vmlinux.bin.cmd,注意這裡的vmlinux就是根目錄下的vmlinux。
cmd_arch/i386/boot/compressed/vmlinux.bin := objcopy -O binary -R .note -R .comment -S vmlinux arch/i386/boot/compressed/vmlinux.bin
下面我們將生成bzImage的過程總結一下:
a。由vmlinux檔案strip掉符號表得到arch/i386/boot/compressed/vmlinux.bin
b。將vmlinux.bin壓縮成vmlinux.bin.gz
c。將vmlinux.scr和vmlinux.bin.gz連結成piggy.o
d。將head.o、misc.o和piggy.o連結成當前目錄下的vmlinux
e。將vmlinux檔案strip掉符號表得到arch/i386/boot/vmlinux.bin
f。將bootsect、setup和vmlinux.bin拼接成bzImage
二。核心裝載時的記憶體空間對映
下面是檔案Documentation/i386/boot.txt中提供的bzImage在記憶體中的對映圖,和本例項略有出入,下面我們會指出,但基本描述了bzImage在記憶體中的分佈情況。
下圖是傳統的Image或zImage記憶體對映圖
結合上一章在bootloader中boot_func所講的實際情況,核心在記憶體中的地址對映應該是這樣的:
0x100000以上:核心保護模式程式碼
0x99000-0x99100:核心引數命令
0x90000-0x99000:核心bootsect和setup真實模式程式碼,bootsect大小512位元組,setup0x1400位元組
0x9000開始:核心棧地址
三。核心啟始相關檔案分析
從以上bzImage的生成過程,我們可以發現,arch/i386/boot/bootsect和arch /i386/boot/setup應該是做初始工作的,接下來應該是arch/i386/boot/compressed/head.o,然後可能就是 vmlinux是由arch/i386/kernel/head.o。那麼我們就按照順序從arch/i386/boot/bootsect.S開始分析。
下面圖片是bzImage的前0x240個位元組內容。
四。arch/i386/boot/bootsect.S
bootsect.S生成的檔案bootsect大小隻有512位元組,也就是上圖中的0x0000到0x01ff的內容,是不是有點眼熟,其實裡面另有玄機。下面我們來看bootsect.S的內容。
_start:
jmpl $BOOTSEG, $start2 ;這裡的BOOTSEG就是段地址0x07C0,使用一個長跳轉到start2 start2:
movw %cs, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss movw $0x7c00, %sp sti cld movw $bugger_off_msg, %si ;bugger_off_msg中的資訊為"Direct booting from floppy is no longer supported.\r\nPlease use a boot loader program instead.\r\n\nRemove disk and press any key to reboot . . .\r\n" msg_loop:
lodsb andb %al, %al jz die ;列印完成後跳轉到die movb $0xe, %ah movw $7, %bx int $0x10 ;使用int10列印資訊到螢幕 jmp msg_loop die:
xorw %ax, %ax int $0x16 ;允許使用者按任意一鍵重啟 int $0x19 ljmp $0xf000,$0xfff0 ;一般上面的中斷呼叫後不會到這裡了,如果有例外情況,直接跳轉到BIOS的重啟程式碼 從這裡可以看出,此處核心的bootsect其實沒有任何意義,實際上在2.6版本的linux中,必須要有另外的bootloader才能啟動核心,例如grub。在前面我們分析grub的boot_func中的big_linux_boot裡,描述了實際上 grub的stage2將核心的bootsect和setup真實模式程式碼載入到地址0x90000後,是skip了頭0x200個位元組的,直接跳轉到地址 0x90200處執行的。
五。arch/i386/boot/setup.S
setup.S是真正核心的開始,上面圖片從0x200開始就是setup的內容。
從0x0202開始的4個位元組是特徵值"HdrS"。
0x206開始的內容0x0206是版本號,其實是Linux核心頭協議號。
0x20c開始的內容是SYSSEG,即系統載入的段地址0x1000。
接下來是kernel_version內容的偏移量,在這裡是0x11b8,實際上就是setup的啟始地址 0x200+0x11b8=0x13b8,在這裡因為太長沒有給出圖片,可以告訴大家實際內容是"2.6.22 ( [email protected] ) #6 SMP Thu Aug 2 16:57:24 CST 2007"。
0x211內容為1,指出此核心為big-kernel。
0x212開始的內容是0x8000,代表setup_move_size的大小,後面將會遇到。
0x214的內容代表了核心將要載入到的地址,在這裡是0x100000。
從0x240到0xeff是E820和EDD的保留空間。
下面我們介紹主要流程。
start:
jmp trampoline trampoline:
call start_of_setup start_of_setup:
movw $0x01500, %ax movb $0x81, %dl int $0x13 1。檢查特徵值
movw %cs, %ax ;此時cs程式碼段地址為SETUPSEG,即0x9020 movw %ax, %ds cmpw $SIG1, setup_sig1 ;檢查偏移地址setup_sig1處內容是否為0xAA55 ,這一般在編譯生成setup時就寫好了 jne bad_sig cmpw $SIG2, setup_sig2 ;檢查偏移地址setup_sig2處內容是否為0x5A5A jne bad_sig jmp good_sig1 ;檢查特徵值沒問題 good_sig1:
jmp good_sig good_sig:
movw %cs, %ax subw $DELTA_INITSEG, %ax ;這裡DELTA_INITSEG = SETUPSEG - INITSEG = 0x9020 - 0x9000 = 0x0020 movw %ax, %ds 2。檢查是否載入的是big-kernel
testb $LOADED_HIGH, %cs:loadflags ;檢查是否big-kernel,實際就是看bzImage的0x211處的值是否為1,在本例項中是big-kernel ,將會被載入到高位0x100000,從bzImage的0x214開始的內容也可以看出。 jz loader_ok cmpb $0, %cs:type_of_loader ;確認是否有loader可以處理接下來的工作,在這裡是沒有的,值為0,在偏移地址0x210處 jnz loader_ok pushw %cs popw %ds lea loader_panic_mess, %si call prtstr;列印"Wrong loader, giving up..." jmp no_sig_loop;掛起,進入死迴圈 3。檢查cpu情況
loader_ok:
call verify_cpu ;具體在arch/i386/kernel/verify_cpu.S中,這裡就不做詳細介紹了 testl %eax,%eax jz cpu_ok movw %cs,%ax movw %ax,%ds lea cpu_panic_mess,%si call prtstr ;列印"PANIC: CPU too old for this kernel." 1:
jmp 1b ;進入死迴圈 cpu_ok:
4。獲取記憶體大小,在這裡共使用了3種不同方式檢測記憶體:通過e820h方式獲取記憶體地圖,通過e801h方式獲得32位記憶體尺寸,最後通過88h獲得0-64m 。有關e820h可以訪問www.acpi.info獲得ACPI 2.0規範的詳細內容
下面的e820h方式
xorl %eax, %eax movl %eax, (0x1e0) movb %al, (E820NR) ;E820NR=0x1e8 meme820:
xorl %ebx, %ebx movw $E820MAP, %di ;E820MAP=0x2d0 jmpe820:
movl $0x0000e820, %eax movl $SMAP, %edx ;SMAP就是"SMAP" movl $20, %ecx pushw %ds popw %es int $0x15 jc bail820 cmpl $SMAP, %eax jne bail820 good820:
movb (E820NR), %al cmpb $E820MAX, %al ;E820MAX=128,即E820MAP例項的個數 jae bail820 ;128個後獲得後跳出迴圈,內容都在地址0x1e8 開始的128個例項中 incb (E820NR) movw %di, %ax addw $20, %ax movw %ax, %di again820:
cmpl $0, %ebx jne jmpe820 bail820:
下面是e801h方式
meme801:
stc xorw %cx,%cx xorw %dx,%dx ;據說這是為了避免有問題的BIOS產生錯誤 movw $0xe801, %ax int $0x15 jc mem88 cmpw $0x0, %cx jne e801usecxdx cmpw $0x0, %dx jne e801usecxdx movw %ax, %cx movw %bx, %dx e801usecxdx:
andl $0xffff, %edx shll $6, %edx movl %edx, (0x1e0) andl $0xffff, %ecx addl %ecx, (0x1e0) ;內容放到地址0x1e0中 這裡是88h方式,最古老的方式,難道最後內容放在地址0x02中?
mem88:
movb $0x88, %ah int $0x15 movw %ax, (2) 5。設定鍵盤敲擊速率到最大
movw $0x0305, %ax xorw %bx, %bx int $0x16 6。檢查顯示裝置引數並設定模式,具體看arch/i386/boot/video.S,這裡不做介紹了
call video 7。獲取hd0資料
xorw %ax, %ax movw %ax, %ds ldsw (4 * 0x41), %si movw %cs, %ax subw $DELTA_INITSEG, %ax pushw %ax movw %ax, %es movw $0x0080, %di movw $0x10, %cx pushw %cx cld rep movsb 8。獲取hd1資料
xorw %ax, %ax movw %ax, %ds ldsw (4 * 0x46), %si popw %cx popw %es movw $0x0090, %di rep movsb 9。檢查是否有hd1
movw $0x01500, %ax movb $0x81, %dl int $0x13 jc no_disk1 cmpb $3, %ah je is_disk1 no_disk1:
movw %cs, %ax subw $DELTA_INITSEG, %ax movw %ax, %es movw $0x0090, %di movw $0x10, %cx xorw %ax, %ax cld rep stosb is_disk1:
10。檢查微通道匯流排MCA,IBM提出的早期匯流排,目前一般系統都不帶MCA匯流排了
movw %cs, %ax subw $DELTA_INITSEG, %ax movw %ax, %ds xorw %ax, %ax movw %ax, (0xa0) movb $0xc0, %ah stc int $0x15 jc no_mca pushw %ds movw %es, %ax movw %ax, %ds movw %cs, %ax subw $DELTA_INITSEG, %ax movw %ax, %es movw %bx, %si movw $0xa0, %di movw (%si), %cx addw $2, %cx cmpw $0x10, %cx jc sysdesc_ok movw $0x10, %cx sysdesc_ok:
rep movsb popw %ds no_mca:
11。檢測PS/2點裝置
movw %cs, %ax subw $DELTA_INITSEG, %ax movw %ax, %ds movb $0, (0x1ff) int $0x11 testb $0x04, %al jz no_psmouse movb $0xAA, (0x1ff) ;裝置存在 no_psmouse:
12。準備進入保護模式
cmpw $0, %cs:realmode_swtch ;在這裡realmod_swtch實際上是0,所以跳轉到rmodeswtch_normal jz rmodeswtch_normal lcall *%cs:realmode_swtch jmp rmodeswtch_end rmodeswtch_normal:
pushw %cs call default_switch ;default_switch呼叫的實際操作是在真正進入保護模式前關閉中斷並禁止NMI rmodeswtch_end:
13。將系統移到正確的位置,如果是big-kernel我們就不移動了
testb $LOADED_HIGH, %cs:loadflags jz do_move0 jmp end_move do_move0:
movw $0x100, %ax movw %cs, %bp subw $DELTA_INITSEG, %bp movw %cs:start_sys_seg, %bx cld do_move:
movw %ax, %es incb %ah movw %bx, %ds addw $0x100, %bx subw %di, %di subw %si, %si movw $0x800, %cx rep movsw cmpw %bp, %bx jb do_move end_move:
14。載入段地址,確認bootloader是否支援啟動協議版本2.02,決定是否需要移動程式碼到0x90000 ,關於啟動協議可以參考Documentation/i386/boot.txt ,本例項中是不需要移動的
movw %cs, %ax movw %ax, %ds cmpl $0, cmd_line_ptr ;檢查是否需要向下相容小於2.01的bootloader ,在此例中cmd_line_ptr是0,為版本2.02以上 jne end_move_self cmpb $0x20, type_of_loader je end_move_self movw %cs, %ax ;bootloader不支援2.02協議,如果程式碼段不在0x90000,需要將其移動到0x90000 cmpw $SETUPSEG, %ax ;SETUPSEG=0x9020 je end_move_self cli subw $DELTA_INITSEG, %ax ;DELTA_INITSEG=0x0020 movw %ss, %dx cmpw %ax, %dx jb move_self_1 addw $INITSEG, %dx ;INITSEG=0x9000 subw %ax, %dx move_self_1:
movw %ax, %ds movw $INITSEG, %ax movw %ax, %es movw %cs:setup_move_size, %cx std movw %cx, %di decw %di movw %di, %si subw $move_self_here+0x200, %cx rep movsb ljmp $SETUPSEG, $move_self_here move_self_here:
movw $move_self_here+0x200, %cx rep movsb movw $SETUPSEG, %ax movw %ax, %ds movw %dx, %ss end_move_self:
15。開啟A20 ,A20地址線是一個歷史遺留問題,早期為了使用1M以上記憶體而使用的開關,目前一般硬體預設就是開啟的
a20_try_loop:
a20_none:
call a20_test ;先直接看看是否成功,萬一系統就不需要開啟A20,直接跳轉就可以了 jnz a20_done a20_bios:
movw $0x2401, %ax pushfl int $0x15 ;嘗試使用int15設定A20 popfl call a20_test ;看看是否成功 jnz a20_done a20_kbc:
call empty_8042 ;嘗試通過鍵盤控制器設定A20 call a20_test ;看看是否成功 jnz a20_done movb $0xD1, %al outb %al, $0x64 call empty_8042 movb $0xDF, %al outb %al, $0x60 ;開啟A20命令 call empty_8042 a20_kbc_wait:
xorw %cx, %cx a20_kbc_wait_loop:
call a20_test ;看看是否成功 jnz a20_done loop a20_kbc_wait_loop a20_fast:
inb $0x92, %al;最後的嘗試,通過配置Port A orb $0x02, %al andb $0xFE, %al outb %al, $0x92 a20_fast_wait:
xorw %cx, %cx a20_fast_wait_loop:
call a20_test ;看看是否成功 jnz a20_done loop a20_fast_wait_loop decb (a20_tries) ;嘗試了a20_tries=A20_ENABLE_LOOPS=255次 jnz a20_try_loop movw $a20_err_msg, %si call prtstr;仍然沒有效果,列印"linux: fatal error: A20 gate not responding!" a20_die:
hlt jmp a20_die;開啟A20失敗,進入死迴圈 a20_done:
16。設定gdt、idt和32位的啟動地址
lidt idt_48 xorl %eax, %eax movw %ds, %ax shll $4, %eax addl %eax, code32 ;設定32位啟動地址,修改code32指定的地址,將增加了程式碼段後的資料寫入code32指定的記憶體地址中 addl $gdt, %eax movl %eax, (gdt_48+2) ;將下面將介紹gdt的地址寫到gdt_48+2指定的地址中 lgdt gdt_48 17。復位所有可能存在的協處理器
xorw %ax, %ax outb %al, $0xf0 call delay outb %al, $0xf1 call delay 18。遮蔽所有中斷
movb $0xFF, %al outb %al, $0xA1 call delay movb $0xFB, %al outb %al, $0x21;又打開了中斷2,因為irq2是cascaded 19。真正進入保護模式,跳轉到arch/i386/boot/compressed/head.S中的startup_32
movw $1, %ax lmsw %ax ;真正進入保護模式 jmp flush_instr flush_instr:
xorw %bx, %bx xorl %esi, %esi movw %cs, %si subw $DELTA_INITSEG, %si shll $4, %esi .byte 0x66, 0xea ;這裡實際上是硬寫入程式碼指令66 ea,即進入到保護模式下的長跳轉 code32:
long startup_32 ;跳轉到startup_32 .word BOOT_CS ;要跳轉的程式碼段地址BOOT_CS=GDT_ENTRY_BOOT_CS * 8=2*8
startup_32:
movl $(BOOT_DS), %eax ;資料段地址BOOT_DS=GDT_ENTRY_BOOT_DS * 8=(GDT_ENTRY_BOOT_CS + 1)*8=(2+1)*8 movl %eax, %ds movl %eax, %es movl %eax, %fs movl %eax, %gs movl %eax, %ss ;在啟動時的保護模式裡,設定核心裡的地址段都為程式碼段地址 xorl %eax, %eax 1:
incl %eax movl %eax, 0x00000000 cmpl %eax, 0x00100000 je 1b ;檢查A20是否開啟,如果沒開啟就一直死迴圈 jmpl *(code32_start - start + (DELTA_INITSEG << 4))(%esi) ;這裡跳轉到arch/i386/boot/compressed/head.S裡的startup_32
20。初始化時第一次設定的gdt和idt
align 16 gdt:
fill GDT_ENTRY_BOOT_CS,8,0 .word 0xFFFF ;4Gb - (0x100000*0x1000 = 4Gb) .word 0 ;基地址為0 .word 0x9A00 ;程式碼段的屬性是可讀可執行 .word 0x00CF .word 0xFFFF ;4Gb - (0x100000*0x1000 = 4Gb) .word 0 ;基地址為0 .word 0x9200 ;資料段的屬性是可讀可寫 .word 0x00CF gdt_end:
align 4 word 0 idt_48:
word 0 word 0, 0 word 0 gdt_48:
word gdt_end - gdt - 1 ;gdt的尺寸 word 0, 0 ;gdt的地址,在執行時才填寫實際的絕對地址 六。arch/i386/boot/compressed/head.S
arch/i386/boot/compressed/head.S負責將壓縮的核心解壓縮,並跳轉到解壓後的核心執行,主要流程如下:
1。段地址準備,在核心裡都為BOOT_DS=GDT_ENTRY_BOOT_DS*8=(GDT_ENTRY_BOOT_CS + 1)*8=(2+1)*8
2。拷貝壓縮的核心到快取結尾以保證安全
3。計算核心啟始地址
4。將壓縮核心解壓
call decompress_kernel
5。跳轉到解壓後的核心執行
xorl %ebx,%ebx
jmp *%ebp
七。arch/i386/kernel/head.S
arch/i386/kernel/head.S是真正的32位啟動程式碼。
1。段地址準備
2。核心啟動引數準備
3。初始化頁面表
4。設定idt
5。檢查cpu型別
6。跳轉到start_kernel
jmp start_kernel
八。start_kernel
開始進入C語言的啟動流程,其中一些記憶體管理、裝置初始化、排程等相關細節將在後續章節詳細介紹,這裡只是簡要敘述基本流程。
1。初始化tick控制
2。頁面地址表page_address_maps和page_address_htable初始化
3。初始化核心程式碼、資料段並計算頁面數
4。設定記憶體頁面表對映
5。初始化排程
6。建立zonelists
7。設定系統trap呼叫
8。設定系統中斷呼叫
9。初始化pidhash
10。設定時鐘軟中斷TIMER_SOFTIRQ呼叫
11。設定高解析度時鐘軟中斷HRTIMER_SOFTIRQ呼叫
12。設定軟中斷TASKLET_SOFTIRQ和HI_SOFTIRQ的呼叫
13。初始化終端裝置
14。初始化dcache和inode
15。核心空間記憶體地址分配
16。初始化slab機制
17。優先樹結構index_bits_to_maxindex初始化
18。初始化fork機制
19。基樹結構height_to_maxindex初始化
20。初始化訊號機制
21。初始化acpi
22。啟動第一個核心執行緒kernel_init
九。第一個核心執行緒 - kernel_init
核心最後的初始化,準備開始進入第一個應用層程式。
1。初始化工作佇列
2。裝置框架初始化
例如一些需要滿足sys檔案系統的初始化,bus、class等
3。初始化所有的initcalls
initcalls機制在2.6早期版本是不完全的,例如把網路相關的sock_init仍然以呼叫方式初始化,現今的核心版本已經把所有的子系統都改為以do_initcalls的形式初始化了。
4。執行第一個應用程式
開啟系統終端,依次尋找/sbin/init、/etc/init、/bin/init和/bin/sh執行,若都沒有成功,則列印錯誤資訊,掛起系統。
十。參考資料
http://www.linux.org/docs/ldp/howto/Linux-i386-Boot-Code-HOWTO/index.html --------------------- 作者:王明威 來源:CSDN 原文:https://blog.csdn.net/u010510069/article/details/40263983 版權宣告:本文為博主原創文章,轉載請附上博文連結!