grub----Stage1.s源代碼分析

Stage1.s源文件是用古老的 at&t彙編編寫而成,是大名鼎鼎的unix家族操作系統引導程序GRUB中的第一個文件。它編譯後產生的二進制代碼正好是512字節(故意的, 也是必須的),剛好填充滿硬盤初始的一個扇區,也即0柱面、0磁道、1扇區。人們稱之爲MBR——主引導記錄。它的作用是載入stage2文件。

    閱讀本段代碼,gemfield建議你首先具備以下能力:cpu寄存器、BIOS中斷、PC架構、at&t彙編、GRUB背景知識。幸運的,青島 之光論壇(bbs.civilnet.cn)的嵌入系統版塊裏都或多或少包含了這些介紹。並且可以從青島之光論壇上查找stage1.s的源代碼,此處不 一一羅列了。

    程序剛開始處的宏定義使用了和gcc相同的規範,定義的3個宏變量在後面用到的地方再由gemfield詳細闡述。在定義了一個全局變量_start後, 程序的真正入口就到了。事實上,在二進制代碼中,開始部分的代碼是eb48,其中eb就是jmp的機器碼,在標號_start後,緊跟着的就是這個jmp 指令,跳轉到after_BPB處。Jmp後的nop指令,恐怕永遠也不會執行了。注意,剛開機時cpu會調用int19h將第一個扇區的內容調到內存地 址爲0x0000:0x7coo處,你要問爲什麼是這個地址或者爲什麼會發生這樣的調用,原因大抵和usb爲什麼是2根數據線是一樣的。

    .=_start + 4是一個讓人困惑的語句,其實這個dot是一個特殊的標號,在as彙編規範中,就代表當前的地址。從開始處的_start處填充空間至_start+4處,相當於4個字節的空間。但是,從_start開始後的jmp nop 和jmp的參數已經佔用了3個字節的空間,相當於在它們的後面再用0填充1個字節的空間即可。

    後面緊跟的是一系列稱之爲彙編directive的“僞指令”。這一部分是對磁盤等一些參數進行設置。像起始的扇區、磁道和柱面以及它們的起始地址、還有 stage1的版本號、boot_drive變量、force_lba變量、stage2的地址、扇區、段等參數,這在後面的代碼中涉及的時候再由 gemfield闡述,到時候gemfield會稱這部分爲初始化參數部分,切記。但在這一系列的參數設置中,還有個相似的語句,就 是.=_start+STAGE1_BPBEND,照樣是從上一條指令處填充0直至到達_start+0x3e處。

    在jmp之後,清中斷允許位,然後陳列80ca這個二進制代碼。80ca就相當於orb $ox80,%dl,意思是給dl寄存器賦值80,要知道,在開機初始, BIOS加載完啓動代碼會把%dl寄存器設置成啓動盤號(boot drive number):
DL = 00h 1st floppy disk ( "drive A:" )
DL = 01h 2nd floppy disk ( "drive B:" )
DL = 80h 1st hard disk
DL = 81h 2nd hard disk

    硬盤的代號是80,所以上面代表的是stage1裝到硬盤上的情形,如果是軟盤的話,就是orb $0x00,%dl,很顯然,軟驅代號是0x00。

    關於boot_drive_mask這一部分,包含的ljmp $0,ABS(real_start)指令的意思是,跳轉到cs:ip = 0x0000:$ABS(real_start)這個地方執行指令。程序的開頭部分定義了ABS這個宏,在此處就相當於real_start- _start+0x7c00。如果是“正常的”int19h中斷,這句就是廢話。因爲物理地址是(Segment value * 16) + Offset value,正常情況下MBR被加載到cs:ip = 0x0000:0x7c00上,而有些糟糕的BIOS會將其加載到07c0:0000上,其實這兩個代表的物理地址是完全一樣的(你可以用上上行的公式計 算)。有些人從來就不考慮這種事實,那就是大多數人常常把segment值設爲0,這樣引導代碼就可以假定任何段寄存器都是0從而只對付ip裏的偏移量。 所以,在grub裏,加上這麼一個長轉移,就防止了這類糟糕的BIOS帶來的大麻煩。

    接着進入real_start了,ax清零,ds賦值0,ss賦值0,將STAGE1_STACKSEG(0x2000)賦值給sp,這樣就設置了實模式 下的堆棧段地址(棧頂位置)ss:sp = 0x0000:0x2000。接着置中斷允許位,然後檢查是否設置了啓動的磁盤。先用MOV_MEM_TO_AL宏將boot_drive量存到al中, 然後與0xff進行比較,用的是cmpb $0xff,%al ;je 1f。cmpb指令是將兩個操作數進行相減,對標誌位的影響同sub指令,但是不保存結果。其中,此處用到的是zf標誌位(因爲是je指令),這樣,當操 作數相等(即相減爲零時)zf被置1。所以,cmpb和je一起使用時,就是指,當操作數相等時,跳轉至je制定的標號。所以,在這裏,若 boot_drive等於0xff,則使用BIOS傳遞過來的默認的驅動器進行啓動;如果不是,movb %al,%dl,將boot_drive的值保存至dl中,表示由boot_drive的值確定啓動設備。不管怎麼樣,現在開始正式啓動了……

    驅動器號信息壓棧、輸出信息“GRUB”,注意,在屏幕上輸出信息時調用了MSG宏。下面分析一下這個宏,#define MSG(X)  movw $ABS(x),%si ;call message

輸出GRUB字樣時,變量是 notification_string,相當於將notification_string地址上的16位內容送入si寄存器,然後調用message函 數,而message函數使用了int10中斷來在屏幕上顯示字符。涉及到串操作指令。message函數:lodsb,從%si指向的源地址中逐一讀取 一個字符,送入al中,然後檢查al是否爲零,如果爲零,表示字符已經傳輸完成了(.string僞指令會在指定的字符串後加入一個字節的0),此時調用 ret返回。而若不爲零,表明字符還未傳輸完,此時跳轉到int 10h“中斷前夕”,用int 10h 的oeh子功能在屏幕上以telemode模式寫字符,其中,ah是子功能號,al是字符,bh是頁,bl是前背景色(在圖形模式下)。所以這裏movw $0x0001,%bx ; movb $0x0e,%ah ;int $0x10(顯示一個字符)就ok了。

    在屏幕上顯示完GRUB後,要來決定是進入chs模式還是lba模式(也就是看硬盤是否支持LBA模式,因爲兩種模式對硬盤的讀寫等操作有很不一樣的地 方),但在這之前,你得首先判斷這裏是硬盤而不是軟盤或者根本就沒有盤(言下之意就是,如果不是硬盤,判斷LBA或者CHS模式就沒有意義了),所以,在 判斷硬盤是否支持LBA時,先判斷是不是硬盤。這裏用testb $STAGE1_BIOS_HD_FLAG,%dl來判斷,dl寄存器裏裝載的是磁盤號,有三大類情況:硬盤(0x80、0x81)、軟盤(0x00、 0x01)、無效的盤(0xff)。而前面的宏就是0x80,所以通過testb和jz指令判斷,如果dl中不是80或81(也就是不是硬盤),就跳轉到 chs_mode函數下面。另外,如果此處判斷出是硬盤的話,再接着判斷是否支持LBA,使用的工具就是BIOS的int 13h中斷。通過 BIOS 調用 INT 0x13 來確定是否支持擴展,
LBA 擴展功能分兩個子集 , 如下 :
第一個子集提供了訪問大硬盤所必須的功能 , 包括
1.檢查擴展是否存在 : ah = 41h , bx = 0x55aa , dl = drive( 0x80 ~ 0xff )
2.擴展讀  : ah = 42h
3.擴展寫  : ah = 43h
4.校驗扇區  : ah = 44h
5.擴展定位  : ah = 47h
6.取得驅動器參數 : ah = 48h
第二個子集提供了對軟件控制驅動器鎖定和彈出的支持 ,包括
1.檢查擴展  : ah = 41h
2.鎖定/解鎖驅動器 : ah = 45h
3.彈出驅動器  : ah = 46h
4.取得驅動器參數 : ah = 48h
5.取得擴展驅動器改變狀態: ah = 49h

    下面開始具體檢測 , 首先檢測擴展是否存在。此時寄存器的值和 BIOS 調用分別是:AH = 0x41,BX = 0x55AA,DL = driver( 0x80 ~ 0xFF ),然後INT  13H,看返回結果:如果支持CF= 0;否則 CF = 1;CF = 0 (支持LBA) 時的寄存器值代表含義:
ah:擴展功能的主版本號( major version of extensions )
al:內部使用( internal use )
bx :AA55h ( magic number )
cx:Bits  Description
0  extended disk access functions
1  removable drive controller functions supported
2  enhanced disk drive (EDD) functions (AH=48h,AH=4Eh) supported.
Extended drive parameter table is valid
3~15  reserved (0)
CF = 1 (不支持LBA) 時的寄存器值 :
ah = 0x01 ( invalid function )

    現在stage1.s使用movb $0x41, %ah;movw $0x55aa, %bx;int $0x13; jc chs_mode來進行上述判斷。如果不支持LBA,則cf就是1,跳轉到chs_mode函數運行。有的bios的int 13h中斷會影響到dl,所以此處用pop和push指令將其保護起來。然而cf不等於1也不表示就支持LBA了,還得再判斷bx是不是aa55h,使用 cmpb $0xaa55,%bx ;jne chs_mode再判斷一次,如果bx裏存的不是預期的返回值,同樣不支持lba,也要進入chs_mode函數。這裏有個強制LBA模式要注意下,就是 說,當cf是1,bx也是aa55,那麼可以不用在判斷就進入強制LBA模式,代碼是這樣寫的,使用MOV_MEM_TO_AL宏將force_lba變 量值傳遞到al,判斷是否爲0。不爲零強行進入lba_mode函數。然後判斷cx,如果cx爲0的話表明不支持擴展第一子集,這時也進入 chs_mode函數。所以總結進入chs_mode的情況,如下:

第一、   磁盤號非80h或81h,進入chs_mode

第二、   int13h,41h子功能,返回cf爲0,進入進入chs_mode

第三、   int13h,41h子功能,返回bx不爲aa55,進入chs_mode

第四、   如果沒有設置強制LBA,而且也不支持擴展第一子集,進入chs_mode

第五、   其它情況,進入lba模式

    那我們就先來分析進入chs模式的代碼,你看,我們是以以上種種情況的發生而進入chs模式的,所以進入chs模式時,再來進行一些檢測,來確定具體的情況。首先就是int13h的08功能號的使用。使用08功能可以檢測chs模式中硬盤的參數,保存在各寄存器裏:

DL:本機軟盤驅動器的數目
DH:最大磁頭號(或說磁面數目)。0表示有1個磁面,1表示有2個磁面
CH:存放10位磁道柱面數的低8位(高2位在CL的D7、D6中)。1表示有1個柱面,2表示有2個柱面,依次類推。
CL:0~5位存放每磁道的扇區數目。6和7位表示10位磁道柱面數的高2位。
AX=0
BH=0
BL表示驅動器類型:
1=360K 5.25
2=1.2M 5.25
3=720K 3.5
4=1.44M 3.5
ES:SI 指向軟盤參數表

    如果成功返回參數,則進入final_init函數;但是如果調用失敗,進位標誌CF=1,AH存放錯誤信息碼。表明不支持硬盤的chs模式(前面也判斷 了不支持lba),那就要考慮是不是軟盤了。再使用testb和jz指令,若dl是00或01,則認爲是軟盤,就跳轉到floppy_probe函數執行 (後文討論此函數)。但是若連軟盤也不是,只好準備報錯了。跳轉到hd_probe_error函數,這個函數調用MSG函數連同 general_error函數一道輸出“hard disk error”的字符。

    好了,現在我們回來。剛開始經過一些列的判斷,我們進入了LBA模式。然後,代碼做了以下工作,movl 0x10(%si),%ecx,這個代碼就是個廢話,ecx寄存器被置入了一個無意義的值;然後將標號disk_address_packet處的地址賦 給si,再接着將[si-1]內存處置1(也就是mode被置1,表示LBA擴展讀;如果是0,就是CHS尋址讀)、將stage2的扇區數賦予ebx、 在[si]和[si+1]處存放10和00(movw $0x0010,(si))、在[si+2]和[si+3]處存放01和00、在[si+4]和[si+5]處存放00和00、在[si+6]和 [si+7]處存放0x00和0x70(這是stage1_bufferseg的值)、在[si+8][si+9][si+A][si+B]處存放 0x01/0x00/0x00/0x00、在[si+c][si+d][si+e][si+f]處存放0x00/0x00/0x00/0x00。設置完畢 後,開始調用int 13h的42功能中斷。如果出錯,就跳轉到chs_mode處。那麼中斷執行成功呢?

    由si及其偏移量指向的內存保存着磁盤參數塊,如下:

偏移量     大小       位數       描述

00h       BYTE        8        數據塊的大小 (10h or 18h)
01h       BYTE        8        保留,必須爲0

02h       WORD      16       傳輸數據塊數,傳輸完成後保存傳輸的塊數

04h       DWORD     32       傳輸時的數據緩存地址

08h       QWORD     64       起始絕對扇區號(即起始扇區的LBA號碼)

    所以,通過int13h(42)中斷的作用,硬盤上第二個扇區上的內容就被讀到由si偏移量爲4h、5h、6h、7h確定的內存區域上了,此處是0x7000:0x0000。執行成功,將bx賦值0x7000,然後跳至copy_buffer子函數處。

    LBA已完,gemfield在閱讀copy_buffer前再回頭看當初程序跳至chs_mode後是怎麼運行的。上文中已經指出了,到達 chs_mode後經過條件判斷,一共產生了三種情況,第一是進入硬盤的chs子函數(final_init);第二是進入軟盤子程序 (floppy_probe);第三種情況是進入報錯子函數,在屏幕上輸出一系列錯誤。那就由gemfield從第一種情況開始吧。程序運行到 final_init後,將扇區數保存到si、設置mode爲0、eax清零爲存放磁頭數做準備、將dh中存放的磁頭數保存到al中、使用incw %ax指令(因爲磁頭數是以0~n-1方式排列的,所以增1後纔是真正的磁頭數)、將磁頭數保存至[si+4][si+5][si+6][si+7]內存 地址上、清dx爲存放扇區數做準備、cl中的0~5位存放的是扇區數,所以dx邏輯左移2位後在dh中出現的兩位就是柱面數的高2位,並且把這2位移到 ah中,而ch存放的柱面數低8爲移至al中,這樣ax裏就是柱面數了,這裏因爲同樣的道理要進行incw %ax操作,並且把真正的柱面數放到地址爲[si+8][si+9]的內存上、然後用同樣的移動方法產生真正的扇區數並保存在地址爲[si][si+1] [si+2][si+3]的內存上。

    然後在使用int 13h(0x02)功能前要進行必備的參數設置:eax存放stage2的扇區編號(stage2_sector,默認爲1)、清edx寄存器、然後通過 (stage2扇區數)/(扇區數)獲得引導扇區數。注意對於div指令來說,eax恆定存放被除數,div後面的寄存器存放的是除數。餘數在edx中存 放,第一個餘數(扇區數)放到地址爲[si+10]的內存上並將edx清零、再用(上一步除法的商) /(磁頭數)得到的餘數爲磁頭數,存放在[si+11]內存地址上。商爲柱面數並存放在eax中並同時保存至[si+12][si+13]內存地址上。然 後將之前中斷獲得的柱面數與此處stage2所佔柱面數相比較,如果stage2柱面數大,那麼明顯錯誤,程序將跳至geometry_error處。

    現在,將[si+13]的內容賦值給dl(柱面數的高2位)並且左移6位、將扇區數放到cl中再增1、然後通過orb %dl,%cl和movb 12(%si),%dh指令達到這麼一種情況,即:cl中存放的是扇區數和柱面數的高2位,ch中存放的是柱面數的低8位、然後恢復驅動器號(popw %dx)、然後將磁頭數放置到dh中,然後將0x7000賦值給es並將bx清零,賦值0x0201給ax(獲得中斷功能號),參數現在設置完畢,開始調用int 13h中斷:

%al = number of sectors(需要讀的扇區數)

%ah= 0x02(功能號)
                %ch = cylinder(起始柱面數)
                %cl = sector (bits 6-7 are high bits of "cylinder")
                %dh = head
                %dl = drive (0x80 for hard disk, 0x0 for floppy disk)
                %es:%bx = segment:offset of buffer

    調用中斷後,將0柱面、0磁道、2扇區的內容讀到0x7000:0x0000內存處。然後程序跳轉至copy_buffer處,和LBA殊途同歸呀。

    我們看看copy_buffer做了什麼。將0x8000賦值給es、給cx賦值0x100、給ds賦值0x7000、si和di清零、方向標誌DF置 零,然後使用rep和movsw指令將ds:si處連續的512字節內容傳輸到es:di指定的內存地址(0x8000:0x0000)。其中,rep指 令的含義就是重複執行後一句指令,沒執行一次。cx減1,直至cx爲0。這也是前面cx賦值0x100(256)的原因。movsw每次傳輸一個 字,256次就是512字節。然後popw %ds; popa還原寄存器。

    接着,程序跳轉到0x8000處繼續執行,到此就開始執行新的模塊了,stage1的任務也已經結束了。代碼中*(stage2_address)的星號是at&t彙編的規範:絕對跳轉/調用(相對於與程序計數器有關的跳轉/調用)操作數前面要加星號"*"。

    然而,前面所述的chs模式中的第二種情況——軟驅情況將會帶領gemfield進入floppy_probe子函數,此處要使用int 13h(0x00功能號)來進行軟驅的復位。成功的話cf=0; 然後準備調用int 13h(功能號是0x02),這和chs中的int 13h,ah=0x02是一樣的。所以,先來爲中斷準備必須的參數:軟驅復位後,將[si]處的值賦給cl(cl是起始扇區數),我們知道,由於循環,我們給了cl 4次機會,因爲循環中有incw %si指令,所以si中的值是遞增的,從probe_values開始,在每一次的機會中依次給cl賦予了0x24、0x12、0x0f、0x09這幾種值,當然,試完後還不對的話就要執行報錯函數了。

    像以前那樣,依次準備好bx、ah、al、ch、cl、dh的值後,就要int 13h了。成功後,dh賦值1、ch賦值0x4f,dh 設置爲 79 , 表示柱面最大值爲 79(80柱:0~79),dh 設置爲 1 , 表示磁頭數最大值爲 1(2頭:0~1),然後跳轉至 final_init,在上文中關於final_init的分析 , 我們知道保存時會把柱面和磁頭分別加 1 , 扇區不變,因此 , 在軟盤加載時 , 將設置 Cylinder : Head : Sector = 80 : 2 : start_sector。最終就跳轉至final_init函數處執行了。

    gemfield的本文中,依然要注意的還有爲了兼容性而設置的windows nt魔術頭標識的偏移、part_start作爲標識的分區表起始地址的標記的偏移、以及引導扇區結束標誌0xaa55。

    總的說來,在gemfield這篇稍顯凌亂的文章裏,主要介紹了stage1.s的使命,簡介來說,就是開機時首先被BIOS INT19H裝載到內存0x7c00處,然後判斷chs和lba模式,然後使用int13h中斷將磁盤上第二扇區的內容讀到0x7000處,然後通過子函 數copy_buffer再將其調到0x8000的位置上,這個第二扇區的內容就是以後gemfield的嵌入系統版塊中將要介紹的start.s模塊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章