2010年10月21日 星期四

GBUB4DOS一開始一定是先處理MBR:stage1.S

stage1.s源代碼分析(很詳盡): 文章出處:http://www.civilnet.cn/blog/browse.php?operation=display&type=blog&entryno=85
Stage1.s原始檔案是用古老的at&t彙編編寫而成,是大名鼎鼎的unix家族作業系統引導程式GRUB中的第一個檔。它編譯後產生的二進位碼正好是512位元組(故意的,也是必須的),剛好填充滿硬碟初始的一個磁區,也即0柱面、0磁軌、1磁區。人們稱之為MBR——主引導記錄。它的作用是載入stage2檔(GRLDR)。閱讀本段代碼,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模組。

沒有留言:

張貼留言