上一節中說到BIOS會將MBR中的主引導程序(512字節)加載到內存的0x7c00處,其中這512字節的主引導程序是軟件程序,是操作系統的一部分,因此也是由操作系統開發者來編寫的,BIOS將其加載到內存后,會自動跳到0x7c00處去執行。接下來我們自己實現一個“主引導程序”,功能很簡單,就是讓它打印一串字符串到屏幕上(真正的主引導程序是加載操作系統內核用的),注意,這段程序現在是獨立運行在裸機上的,我們用匯編語言來實現它。
這個過程我們需要調用中斷向量表中的“打印函數”,調用的動作是通過int 0x10來實現的。
編寫程序之前,我們先來介紹幾個要用到的指令:
mov :賦值操作,將右操作數賦值給左操作數
例:mov ax, 0 ;將0賦值給ax寄存器
int :觸發中斷
例:int 0x10 ; 觸發0x10中斷,對屏幕進行操作
hlt :停止,CPU進入暫停狀態,不進行任何操作
例:hlt ;使程序進入睡眠狀態
匯編中的地址訪問形式:段地址:段內偏移地址
例:mov byte[0xb800:0x01] ; 0xb800:0x01 -> 0xb800 + 0x01
標簽:
用於標識后續指令的地址,(與C語言中的標簽等價)
$ 與 $$
$表示當前指令行地址,$$表示當前匯編段起始地址
中斷調用與函數調用的對應關系如下所示:
實現真正的打印之前,要向bx寄存器中寫入0x0f,向ah寄存器中寫入0x0e,這些都是打印函數規定好的。而al寄存器中需要存入要打印的字符,寄存器中的內容准備好了之后使用int 0x10來調用打印函數,而后,就可以將字符打印到屏幕上。
下面直接給出用匯編語言寫的“主引導程序“。
org 0x7c00 start: mov ax, cs mov ss, ax mov ds, ax mov es, ax mov si, msg print: mov al, [si] add si, 1 cmp al, 0x00 je last mov ah, 0x0e mov bx, 0x0f int 0x10 jmp print last: hlt jmp last msg: db 0x0a, 0x0a db "Hello, DTOS!" db 0x0a, 0x0a times 510-($-$$) db 0x00 db 0x55, 0xaa
主引導程序實際待的起始地址是0x7c00,因此第一行的org 0x7c00告訴編譯器,這段程序的加載地址是0x7c00,這樣編譯器就可以對地址進行正確的處理。
然后將ss,ds,ed寄存器分別清零,msg標簽處在內存中定義了一些數據,這些數據是實實在在在內存中和磁盤文件中占用空間的。msg就代表了這片數據所占用空間的起始地址。mov si, msg就是將這個起始地址送到si寄存器中。
接下來print標號處定義了打印相關的功能,mov al, [si]就是取si所代表的內存地址處的一個字節數據,並放到al寄存器中,然后使si加1,指向下一個數據,此時先判斷al中的數據是否是0x00,也就是ASCII碼‘\0’,這個字符代表字符串的結束,如果確實為0x00,則跳轉到last處,讓cpu停機。如果不為0x00,則開始准備打印,正如我們上面說到的將0x0e存到ah寄存器中,將0x0f存到bx寄存器中,相當於格式化參數(%c),然后通過int 10來調用中斷向量表里面的打印函數,打印完一個字符后繼續跳轉到print處進行下一次循環,直到到達字符串的末尾。
下面我們講一講msg標號處的數據的定義,db偽指令定義一個字節,其后可以是數據,也可以是字符串。db 0x0a即定義一個字節,其中的數據初始化為0x0a,也就是換行符的ASCII碼,db "Hello, DTOS!"相當於定義了11個字節。
根據前面內容知道,主引導扇區為512字節大小,而BIOS判斷存儲介質是否包含主引導扇區的標准就是看看它的前512個字節的最后兩個字節是不是0x55和0xaa,如果是,那么BIOS就認為這個存儲介質有主引導扇區,接下來就讀取主引導扇區到內存中了。
前面寫的程序不足以占滿512字節,而我們的需求是要將最后兩個字節寫入0x55和0xaa,怎么辦呢?我們讓編譯器去判斷前面程序和數據占用空間的大小,並用適當數量的數據填滿前面不足510字節的部分,然后在最后兩個字節我們寫入0x55和0xaa。
times 510 - ($ - $$) db 0x00這條命令的意思就是填寫510 - ($ - $$)個0x00到內存中,$代表times這條偽指令的地址,$$代表當前匯編程序的起始地址(即start處,0x7c00),相減后即為以上所寫程序和數據占用的空間大小,再用510減去這個值即為所需要填充的空間的大小。最后,第511字節填入0x55,第512字節填入0xaa。
用到的工具:匯編語言編譯器nasm,創建虛擬盤的工具bximage,二進制寫入工具dd。由於我們不會將程序真正的刻錄到軟盤上,因為那樣太麻煩了,而且還需要一個真正的物理機器。所以我們使用虛擬盤創建工具bximage,它可以創建一個文件,這個文件存儲數據時的格式和軟盤是一樣的。
下面,我們實際做實驗看現象,首先打開ubuntu虛擬機,到工作目錄下建立boot.asm文件,敲入上述程序,使用編譯器nasm進行編譯,生成boot.bin文件,如下所示:
下面創建虛擬盤,執行過程如下圖:
這樣,虛擬盤就創建好了,名為a.img,這個文件就相當於一個容量1.44M大小的全盤,見下圖:
下面我們將boot.bin的內容刻錄到這張軟盤中,執行命令dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc,結果如下:
至此,這張軟盤的前512字節就寫入了主引導記錄,因此,它就成為了一張啟動盤了。
下面使用VmWare創建一個物理機器,詳細過程不在貼出,創建好的機器如下,這是一個未安裝任何操作系統的空的機器。
我們直接啟動這個機器看看效果,如下所示:
從啟動界面可以看到,BIOS掃描存儲介質時,根本沒有發現啟動盤,也就是沒有找到主引導扇區,因此給出Operating System not found的提示。
接下來,我們將軟盤插入這台電腦中,首先將帶有主引導扇區的軟盤(a.img文件)拷貝到windows系統下,然后將其插入剛才創建的機器中,如下所示:
接下來,給這台機器加電,效果如下:
可見,主引導程序成功被加載並運行了。
小插曲:
主引導程序部分是16位操作,因此,我們需要一個16位的匯編器,現存的匯編器大概有gas匯編器和nasm匯編器,nasm是16位的,用來編譯英特爾格式匯編代碼,gas是32位的用來編譯AT&T格式匯編的,也是唯一支持AT&T格式的匯編器,因此,我們不能選擇AT&T格式進行實驗。linux的boot程序使用的是as86匯編器進行編譯的,這個匯編器是16位的,但是資料比較少。as86是編寫Minix操作系統的那個教授編寫 的,這個匯編器支持的格式既不是標准的AT&T格式也不是Intel格式,只是和Intel格式比較接近。因此,我們選擇了nasm匯編器。
本文參考狄泰軟件學院操作系統教程。