走進程序世界的田園——引導扇區釋疑 (轉載zjneter的專欄)

走進程序世界的田園
——引導扇區釋疑


什麼?又是醬雞翅?!你眉頭皺起——公司的工作餐怎麼就不能出點新的花樣?吹着空調還會出汗的大熱的天卻還要忍受這份油膩,此刻的你顯然更需要西芹百合!這就如同你曾深深癡迷的這份工作,其實你是多麼懷念在黑色背景上敲出一行命令之後屏幕嘩嘩捲動的景象,多麼自豪於曾經用匯編在屏幕上繪出的彩色的圖案,而如今,虛擬機,API等等新鮮玩意讓你總感覺彷彿想親吻女友的手時卻親到了手套,沒有親密感,也許,你不僅僅需要Java、.NET這樣的高樓大廈,你同樣需要清新的田園,比如你每天都在使用卻可能一知半解的——計算機如何引導。
那麼,現在就讓我們一起走進這田園,看看計算機究竟是如何引導的。

計算機的啓動過程

如果你買來新的計算機,硬盤上還是一片空白的時候,你按下power鍵,仍然可以看到許許多多字符和圖案,這顯然不是操作系統的一部分,而是BIOS的程序在工作。當然,最後你能看到一行字,提示你插入引導盤。是的,BIOS在尋找一個可供引導的磁盤,找到之後,便會加載盤上的引導模塊,並交出控制權,將接力棒傳遞給操作系統。
我們考慮最簡單最易學習的情況,那就是軟盤。
如果你將一張非引導磁盤插入軟驅的話,BIOS仍然會報錯,提示你插入一張系統盤,這說明BIOS並非來者不拒全部試圖加載執行的,那麼它選擇的標準是什麼呢?實際上很簡單,它會去檢查軟盤的0面0磁道1扇區(大小爲512字節),如果發現它以55AA結束,則BIOS認爲它是一個引導扇區,也就是我們說的Boot Sector。當然,一個正確的Boot Sector只有55AA這個結束標誌是沒有意義的,它還應該包含一段少於512字節的執行碼,以便能被放在一個扇區內並正確運行。
一旦BIOS發現了Boot Sector,它就會將這512字節的整個扇區內容裝載到內存的0:7c00處,然後跳轉到0:7c00處將控制權徹底交給這段引導代碼。到此爲止,計算機不再有BIOS中固有的程序來控制,而變成由操作系統的一部分來控制。

馬上實踐——一個最小的引導扇區

準備工作:
硬件
一臺計算機(Windows操作系統)
一張空白軟盤
軟件
彙編編譯器NASM。最新版本可以在此鏈接處獲得:http://sourceforge.net/projects/nasm。(此刻你可能會有疑問,爲什麼是NASM,而不是MASM或者TASM,對於這一點我稍候再來解釋)
軟盤絕對扇區讀寫工具。其實你完全可以自己寫一個,用CreateFile和WriteFile這兩個API就搞定了,非常容易。我就是這樣做的,省去了在網上尋找工具的時間。
最好有一個虛擬機比如VirtualPC,可以在試驗的時候不必重啓自己的計算機。


代碼
我們來看這一段代碼:
org 07c00h  ;告訴編譯器程序加載到7c00處
jmp $  ;無限循環
times 510-($-$$) db 0 ;填充剩下的空間,使生成的二進制代      碼恰好爲512字節
dw  0aa55h   ;引導扇區需要以55AA結束

在對程序進行解釋之前,爲了儘快看到效果有初步的感性認識,請先隨我來做以下操作:
首先用NASM編譯一下:

nasm boot.asm -o boot.bin


我們就得到了一個512字節大小的boot.bin,使用軟盤絕對扇區讀寫工具將這個文件寫到一張空白軟盤的第一個扇區,好了,這張軟盤已經是一個引導盤了。
然後把它放到你的軟驅中重新啓動計算機,或者使用VirtualPC模擬啓動過程,從軟盤引導,你看到了什麼?
答案是什麼令人驚喜的結果也沒有出現,這倒容易理解,因爲我們的程序第一個語句就是一個死循環。我們除了讓程序停滯在那裏,其餘什麼也沒做。
這顯然並不令人滿意,我們得看到些效果才行,讓我們將代碼稍作修改:

org 07c00h   ; 告訴編譯器程序加載到7c00處
mov ax, 0b800h
mov es, ax   ; 設置 es 以便直接寫顯存
mov byte [es:0], 'a' ; 在顯存第一個字節寫入字符‘a’
mov byte [es:1], 0ch ; 在顯存第二個字節寫入十六        進制值C,表示黑底紅字
jmp $   ; 無限循環
times 510-($-$$) db 0 ; 填充剩下的空間,使生成的        二進制代碼恰好爲512字節
dw  0aa55h    ; 引導扇區需要以55AA結束

我們在程序無限循環之前插入了四行,目的是讓我們的引導程序能顯示一個紅色的字符‘a’。插入的這四行比較易懂,效果是在B800:0000處寫入了兩個字節:'a'和0Ch。我們知道,B800:0000恰好是顯存的首地址。
同樣的方法編譯,寫入磁盤並重啓,你看到了什麼?
你看到紅色的字符a了!
多麼令人激動啊,這表明我們的程序正確運行了,進一步,這說明我們自己編寫的引導扇區試驗成功了!
如果你用的是VirtualPC,出現的應該是這樣的景象(局部):

r_07-扇區釋疑01%20copy.jpg

這簡直是太妙了,因爲有了這樣的開頭,就意味着你可以在此基礎上做出任意的擴展,甚至寫出自己的操作系統!這一步邁出來的確並不難,但卻具有歷史意義!

爲什麼是NASM
你可能感到很奇怪,爲什麼居然有人用NASM這樣東西,而不是你從前使用的MASM或者TASM,實際上這有點涉及到個人喜好,但是事實是,我從開始無意中接觸到NASM開始,就決定從此徹底拋棄MASM了。因爲它具備以下幾個主要特點:
1、代碼清晰,避免了MASM中容易混淆的語法。
這項特點在NASM多個細節都有體現,這裏我僅舉兩例。
第一,在NASM中,任何不被方括號[]括起來的標籤或變量名都將被認爲是地址,訪問標籤中的內容必須使用[]。所以,mov ax, Message將會把Message對應字符串的首地址傳給寄存器ax。又比如:
如果有:foo dw 1則mov ax, foo將把foo的地址傳給ax,而mov bx, [foo]將把bx的值賦成爲1。
實際上,在NASM中,變量和標籤是一樣的,也就是說,

foo dw 1 ≡ foo: dw 1

而且你會發現,offset這個東東在NASM也是不需要的。因爲不加方括號時表示的就是offset。
我個人認爲這是NASM的一大優點,要地址就不加方括號,也不必額外的offset,想要訪問地址中的內容就必須加上方括號,代碼規則非常鮮明,一目瞭然。
第二,既然所有標籤都是地址,使得NASM具有另外一個特點,就是不記憶變量類型,所以在給變量賦值的時候,必須加上賦值的類型,比如:

mov byte [var1], 'a'

2、 可以在不同平臺中使用
如果你想學習一次就可以在不同平臺下使用的話,NASM幾乎是唯一的選擇。如果你想進行完全的代碼移植,NASM是完美的工具。因爲不管在Dos,Windows還是Linux,NASM都是可用的,而且用法完全相同。
3、 免費
可能這項特性已經不足以吸引你的眼球,但的確是它的一個可愛的特性。
本文不是專門的NASM介紹文章,但是我認爲它的確是一個值得推薦的工具,尤其是,如果你不想僅僅瞭解引導扇區的寫法,而是在此基礎上深究下去,進行操作系統的研究,我保證你會越來越體會到NASM這一工具的優點。

關鍵代碼解釋

上面兩段代碼的註釋已經寫得比較清晰,在這裏對幾個問題着重強調一下。
1、org的使用
org的作用是告訴編譯器,這個程序將來被加載到內存的哪個位置。我們在稍後的例子中會使用到常量,編譯器就是以org指定的這個地址爲基準來確定常量的地址。
2、關於$和$$
$表示當前行被彙編後的地址。這好像不太好理解,不要緊,我們把剛剛生成的二進制代碼文件反彙編來看看:

ndisasmw -o 0x7c00 boot.bin >> a.asm

打開a.asm,你會發現這樣一行:

00007C09  EBFE              jmp short 0x7c09

$在這裏的意思原來就是0x7c09(在加載到內存之後)。
那麼$$表示什麼?它表示一個節(section)的開始處被彙編後的地址。在這裏,我們的程序只有一個節,所以$$實際上就表示程序被編譯後的開始地址,也就是0x7c00。
在寫程序的過程中,“$-$$”可能會被經常用到,它表示本行距離程序開始處的相對距離。現在,你應該明白510-($-$$)表示什麼意思了吧?times  510-($-$$) db 0表示將0這個字節重複510-($-$$)遍,也即在剩下的空間中不停地填充0,直到程序有510字節爲止,這樣,加上結束標誌55AA佔用的兩個字節,恰好是512個字節。
3、 55AA還是AA55
初學者經常被這個問題搞得非常頭痛,總也搞不清楚到底誰在前誰在後,其實歸根到底還是沒把本質弄明白。
IBMPC的原則是“高位在高字節”。舉個例子,如果有一個DWORD類型的數0x12345678放在內存中,看起來會是這樣:

L ———> H
78 56 34 12

因爲78處在數字的低位,於是也會被放到內存的低位。
這裏有一點需要思考一下,就是計算機只知道數字,不知道類型,所以,從內存的某個地址取出一個數,必須在指明類型的情況下才是有意義的,比如已知有這樣的內存映像:

L ———> H
78 56 34 12

若想取出一個BYTE,你會得到0x78;若想取出一個WORD,你會得到0x5678;若想取出一個DWORD,你會得到0x12345678。
回頭看看我們的代碼:

dw  0aa55h

我們指定把0aa55h這個WORD類型數字放在引導扇區最末端,aa處在數字的高位,會被放到內存的高位,於是它在內存中的映像應該是:

L———>H
55 aa

很簡單,也很明瞭不是嗎?

再作擴充——一個變一行

只顯示一個字符顯然是不夠的,我們想要更進一步的成就感,比如顯示一個字符串。可是如果顯示每一個字符都要兩行代碼來實現的話,難免顯得笨拙而低效。是的,你一定想到了,我們可以使用BIOS中斷。
請看代碼:

org 07c00h   ; 程序會被加載到7c00處,所以需要這一句
 mov ax, cs
 mov ds, ax
 mov es, ax
 Call DispStr   ; 調用顯示字符串例程
 jmp $   ; 無限循環
DispStr:
 mov ax, BootMessage
 mov bp, ax   ; ES:BP = 串地址
 mov cx, 16   ; CX = 串長度
 mov ax, 01301h  ; AH = 13,  AL = 01h
 mov bx, 000ch  ; 頁號爲0(BH = 0) 黑底
         紅字(BL = 0Ch,高亮)
 mov dl, 0
 int 10h   ; int 10h
 ret
BootMessage:   db    " Hello, OS world!"
 times 510-($-$$) db   0 ;填充剩下的空間,使生成的二
      進制代碼恰好爲512字節
 dw  0aa55h   ; 引導扇區需要以55AA結束

這段代碼看上去長了許多,但實際上主體框架只有5行(從第2行到第6行),其中調用了一個顯示字符串的子程序。程序的第2、3、4行是三個mov指令,使ds和es兩個段寄存器指向與cs相同的段,以便在以後進行數據操作的時候能定位到正確的位置。第5行調用子程序顯示字符串,然後jmp $讓程序無限循環下去。
我們來試驗一下,編譯,寫入磁盤,啓動:

r_07-扇區釋疑02%20copy.jpg

成功!
來來來,下面泡一杯咖啡,然後靠在椅背上靜靜欣賞一下自己的成果吧,讓你的屏幕暫時停在這一刻。這是一件多麼有趣的作品!雖然我們的代碼很短,卻已經涉及到了如此多的技術細節,我們甚至使用了BIOS中斷,在中斷例程的幫助下,我們幾乎是無所不能的,想象一下吧,最振奮人心的一點是,你可以進行磁盤操作,將更多的程序加載到內存中並且執行,這意味着你真的已經可以在這個小東西的基礎上一點點擴充,甚至建造操作系統的大廈!

之所以這是田園,因爲這裏離泥土最近

你可能很久都沒有過如此透徹地瞭解一件事,就好像你又看到泥土中生長出綠色的植物。這是一種迴歸自然的感覺,那麼,就請盡情享受這一刻爽快的感受吧,忘掉Java,.NET,還有那討厭的醬雞翅。

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