1. 程式人生 > >bootloader詳解(轉載)

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  版權宣告:本文為博主原創文章,轉載請附上博文連結!