作業系統——初識MBR(一)
作業系統——初識MBR(一)
2020-09-0615:30:21 hawk
這裡填補一下計算機基礎方面缺失的基礎知識,為之後的資訊保安學習打基礎。這部分主要是閱讀和學習《作業系統——真象還原》這本書,按照書籍內容完成相關實驗,從而對於計算機作業系統有一個更為完成的瞭解。所有相關的程式碼都會放在我的倉庫中。
概述
這一篇部落格主要用來總結一下書中的主要內容,並且將書中給出的樣例進行具體實現。
計算機啟動
根據我們的常識,計算機的啟動過程,主要是載入資料(既可以是單純的資料,也可以是引用到這些資料的程式指令等)至記憶體,然後通過CPU進行執行,然後成功運行了作業系統,這樣就基本相當於完成了計算機的啟動。
但是說起來雖然簡單,但實際上細究起來還有有一些疑問的——為什麼要載入資料到記憶體中去執行,直接在硬碟中執行不行麼?實際上這是由CPU決定的,因為CPU的硬體電路就是被設計成只能執行處於記憶體中的程式,因此如果我們想要CPU執行相關的程式,必須首先將程式將載入記憶體中,然後才能讓CPU執行。另一方面,第一個程式是什麼,怎麼被載入的?實際上第一個被載入的程式是BIOS,即Basic Input & Output System系統。
對於其他程式來說,其載入往往直接依賴於作業系統,由作業系統自動的幫助我們完成程式的載入功能。但是對於第一個被啟動的程式來說,自然不可能這樣子完成載入,否則就變成了一個雞生蛋、蛋生雞的問題。因此第一個程式的載入,即BIOS程式的載入不同於其他程式的載入。下面我們具體分析一下BIOS程式的載入。
真實模式1MB記憶體佈局
我們知道,計算機方面一直有相容性的優良傳統——而在早期的時候,Intel 8086處理器有20條地址線(1MB記憶體空間),但是暫存器都僅僅是16位的。因此對於其記憶體,會進行一些特別的佈置。而當前雖然處理器已經有了很大的進步,其地址線從32位到64位等,但是遵循於相容性,計算機開機時CPU仍然處於真實模式,即僅僅能訪問1MB記憶體,暫存器也是16位的。而由於真實模式下的記憶體實在有限,因此往往其各個部分的用途都是約定好的,如下所示
起始地址 | 結束地址 | 大小 | 用途 |
FFFF0 | FFFFF | 16B | BIOS入口地址 |
F0000 | FFFFF | 64KB |
BIOS程式碼,包括上面介紹的BIOS入口地址 |
C8000 | EFFFF | 160KB | 對映硬體介面卡的ROM或者記憶體對映式I/O |
C0000 | C7FFFF | 32KB | 顯示介面卡BIOS |
B8000 | BFFFF | 32KB | 用於文字模式顯示介面卡 |
B0000 | B7FFF | 32KB | 用於黑白顯示介面卡 |
A0000 | AFFFF | 64KB | 用於彩色顯示介面卡 |
9FC00 | 9FFFF | 1KB | EBDA(Extended BIOS Data Area)拓展BIOS資料區 |
07E00 | 9FBFF | 622080B | 可用區域 |
07C00 | 07DFF | 512B | MBR被BIOS載入到此處 |
00500 | 07BFF | 30464B | 可用區域 |
00400 | 004FF | 256B | BIOS Data Area(BIOS資料區) |
00000 | 003FF | 1KB | Interrupt Vector Table(中斷向量表) |
實際上,根據上述的表格,我們也很容易知道BIOS是如何被進行載入的——直接對映入記憶體的對應地址即可(這裡的對映是通過硬體實現的,因此和藉助於作業系統等程式進行載入是不同的),並且在上述記憶體地址範圍內的固定地址處當作入口地址進行執行,這個固定地址即為0xFFFF0。但是根據前面的介紹,真實模式下我們最多也就可以訪問1MB的記憶體空間,也就是在這個入口地址下,我們最多隻能訪問16B大小的指令,這必然不可能是全部的BIOS程式的指令(否則其無法實現基礎功能),因此我們可以猜想到這應該是一個跳轉指令。事實上也確實如此,這是一個長跳轉指令,命令如下所示
jmp 0xf000:0xe05b
這裡大家可以自行查詢一下真實模式線性地址的資料,這裡簡單說一下——由於暫存器是16位,而地址空間是20位,因此這裡查詢地址通過段基址暫存器:段偏移暫存器進行表示地址,其地址結果為16 *段基址暫存器 +段偏移暫存器。容易看出來,這裡實際上跳轉到的地址為0xFE05B處。
這裡之後,就基本完成了BIOS的載入記憶體過程,即可以正式執行BIOS程式。實際上BIOS程式也主要就是簡單的檢測記憶體、顯示卡等外設資訊,並初始化硬體並完成中斷向量表IVT等並完成向量表的填寫等。當執行完這些操作後,BIOS最後會校驗啟動盤位於0盤0道1扇區的內容——如果該扇區末尾的兩個位元組分別是0x55、0xaa,則將該扇區內容載入到0x07C00處,然後跳轉至該地址並執行;否則會出錯。
這裡需要說明幾點——首先是0盤0道1扇區,實際上就是指的是磁碟最開始的扇區。
其次是0x55,0xaa。這裡可以認為是一個標識,用來標記該磁碟中存在可執行程式(實際上就是MBR,Master Boot Record程式),如果存在則表明該扇區中確實存在可執行程式;否則不存在。
下面是將其載入到0x07C00,根據前面的表格我們知道,實際上0x07C00到0x07DFF處512B空間是MBR程式,而一個扇區恰好是512B,因此恰好可以複製完全。
最後是跳轉到0x07C00,這裡實際上BIOS中執行的指令是
jmp 0x0:0x7C00
實際上這裡將CS段基址暫存器設定為了0,IP段偏移暫存器設定為了0x7c00,之後在編寫MBR程式時需要注意一下。
最後總結,即入口地址位於0xFFFF0的BIOS可執行程式最後會呼叫最大大小為512B的位於0x7C00處的MBR可執行程式。
MBR程式
下面我們簡單的分析一下MBR可執行程式。
實際上MBR,又名Master Boot Record,即主引導記錄,也被稱為主引導扇區,主要記錄著硬碟本身的相關資訊以及硬碟各個分割槽的大小及位置資訊。當然不會一開始就直接這麼複雜,我們將簡單分析一下,之後進行程式碼實驗測試。
首先根據上面分析的,其通過jmp跳轉後,即完成了MBR的載入,會直接執行MBR程式——這也就意味著,我們並不能通過gcc生成ELF檔案作為MBR,因為ELF檔案中還包含諸如魔數,段表等非程式碼段,其是用於作業系統進行載入的可執行檔案格式,而這裡要求的MBR程式是CPU可以直接執行的二進位制檔案,因此我們必須直接生成該二進位制檔案。
除此之外,二進位制檔案的大小不能超過512B,並且該可執行檔案的511B、512B位元組必須分別為0x55、0xaa。這樣子BIOS程式才能正確識別並載入該MBR程式。
實驗——簡單的MBR程式
為了之後實現複雜的MBR程式,這裡實現一個簡單的MBR程式,其作用是輸出“This is Hawk's MBR",之後會進行懸停——即無限迴圈,從而確保可以看到輸出結果,並且不崩潰。
實驗環境
nasm、qemu、Linux
Linux是我們實驗的宿主機平臺——因為其上有各種方便的開源環境,這裡我的環境是ubuntu20.04
由於我們需要生成純二進位制檔案——即CPU可以直接執行的程式。因此這裡我們通過nasm程式,通過編寫簡單的彙編程式,直接輸出純二進位制檔案即可。
qemu是我們的模擬器——考慮到實際情況,我們不可能找一臺真正的機器去跑我們的MBR程式在觀察,這樣子不僅不現實,並且除錯也十分麻煩。因此直接通過qemu進行模擬執行即可。qemu的安裝方式在我的其他部落格中也有寫,可以點選檢視。
原始碼
; 簡單的主載入程式,但並沒有實現引導的功能,僅僅實現輸出字串 ; 但是其並不需要藉助任何庫等,僅僅依靠BIOS中斷實現部分功能即可 ;------------------------------------------------------------------------ SECTION MBR vstart=0x7c00 ;這個地址表示將起始地址設定為0x7c00——因為BIOS會將MBR程式載入到0x7c00處 mov sp, 0x7c00 ;根據已知,至少0x500-0x7DFF為可用區域,則將其當用作棧即可 ; 下面首先清空螢幕,這裡使用BIOS提供的中斷即可, ;------------------------------------------------------------------------ ; INT 0x10; 功能號:0x06 功能描述:上卷視窗 ;------------------------------------------------------------------------ ; 輸入: ; AH--功能號: 0x06 ; AL--上卷的行數(如果為0,表示全部) ; BH--上卷行屬性 ; (CL, CH)--視窗左上角(X, Y)位置 ; (DL, DH)--視窗右下角(X, Y)位置 ; 輸出: ; 空 mov ax, 0x600 ;AH = 0x06; AL = 0x0; mov bx, 0x700 ;BH = 0x07; BL = 0x0; mov cx, 0x0 ;CH = 0x0; CL = 0x0; mov dx, 0x184f ;DH = 0x18; DL = 0x4f; int 0x10 ;------------------------------------------------------------------------ ; 我們將上面的系統呼叫分析一下 ; 輸入: AH--0x06; AL--0x0; BH--0x7; CL--0x0;CH--0x0; DL--0x4f;DH--0x18; ; 也就是我們呼叫了功能號為0x6的BIOS中斷,視窗左上角為(0x0/0, 0x0/0),視窗右上角座標為(0x4f/80, 0x18/25),上卷所有的視窗 ; 在VGA文字模式中,一般一行容納80個字元,共25行,也就相當於清空了整個螢幕 ; 下面獲取當前的游標位置 ;------------------------------------------------------------------------ ; INT 0x10; 功能號:0x03 功能描述:獲取當前游標位置 ;------------------------------------------------------------------------ ; 輸入: ; AH--功能號: 0x03 ; BH--帶獲取游標的頁碼號 ; 輸出: ; CH--游標開始行 ; CL--游標結束行 ; DH--游標所在行號 ; DL--游標所在列號 mov ah, 0x03 ;AH = 0x3; mov bx, 0 ;BH = 0x0; BL = 0x0; int 0x10 ;------------------------------------------------------------------------ ; 我們將上面的系統呼叫分析一下 ; 輸入: AH--0x3; BH--0x0;BL--0x0; ; 也就是我們呼叫了功能號為0x3的BIOS中斷,獲取了當前游標位置的相關資訊,並將相關資訊儲存在對應的暫存器中 ; 下面進行列印字串 ;------------------------------------------------------------------------ ; INT 0x10; 功能號:0x13 功能描述:打印出字串 ;------------------------------------------------------------------------ ; 輸入: ; ES:BP--字串地址 ; AH--功能號 0x13 ; AL--設定寫字串方式 1表示游標跟隨移動 ; CX--字串長度(不包括最後的0) ; BH--設定要顯示的頁號 ; BL--設定字元屬性 0x2表示黑底綠字 mov ax, cs mov es, ax ;es = 0x0 這裡解釋一下,實際上BIOS跳轉到MBR時,執行jmp 0:0x7c00,將cs暫存器設定為0,因此這裡最後將es段暫存器設定成了0 mov ax, String mov bp, ax ;bp = String 這裡也解釋一下,String表示對應的字串的地址,這裡即將String對應的字串虛擬地址賦給了bp mov ax, 0x1301 ;AH = 0x13; AL = 0x1; mov bx, 0x2 ;BH = 0x0; BL = 0x2; mov cx, 0x12 ; int 0x10 ;------------------------------------------------------------------------ ; 我們將上面的系統呼叫分析一下 ; 輸入: AH--0x13;AL--0x1; BH--0x0;BL--0x2 CX--0x12; ES--0;BP--String ; 也就是我們呼叫了功能號為0x13的BIOS中斷,將0:String地址處,長度為0x12的字串進行了輸出,並且游標跟隨移動 ; 下面進行迴圈,確保程式懸停在該處,從而觀察輸出 ;------------------------------------------------------------------------ jmp $ ;------------------------------------------------------------------------ ; 我們將上面的指令分析一下 ; $表示當前行的地址,這樣子相當於始終執行這一行指令,從而使程式懸停 ; 下面進行字串設定 ;------------------------------------------------------------------------ String db "This is Hawk's MBR"; 即偽操作指令,表示每一個元素大小為1位元組 ; 下面進行空白填充,確保最後程式為512位元組 ;------------------------------------------------------------------------ times 510 - ($ - $$) db 0 ;------------------------------------------------------------------------ ; 我們將上面的彙編語句分析一下 ; $表示當前行的地址,$$表示當前SECTION的起始地址,times也是偽操作指令,相當於將後面的資料重複指定次數 ; 這個指令確保了將程式填充至512位元組,中間部分以0填充 ; 下面我們最後填充該512位元組刪除的最後兩個位元組,為0x55,0xaa,從而使BIOS成功識別MBR ;------------------------------------------------------------------------ db 0x55, 0xaa
實際上上面的註釋已經很詳細了,這裡在具體說明一下
1. 這個程式編譯出來的二進位制檔案應該放置於0盤0道1扇區位置處,應為BIOS會在改位置處判斷是否存在MBR程式
2. 因為MBR程式被BIOS載入的地址為0x7c00,因此我們在編譯的時候需要設定程式起始地址為0x7c00(因為後面有跳轉和字串引用,因此會有地址的引用,所以我們需要設定程式起始地址)
測試
這樣子,我們相當於完成了上述要求的簡單的MBR程式的編寫,下面我們需要在機器上執行,觀察對應的效果。首先我們需要編譯出對應的CPU可以直接執行的二進位制檔案,這裡我們使用nasm將上述的組合語言直接轉換為CPU可直接執行的二進位制檔案,命令如下所示
nasm -o mbr.bin mbr.S
nasm預設生成的檔案格式為CPU可直接執行的二進位制檔案,因此我們不需要再指定格式,我們再觀察一下對應的檔案大小,如圖所示
可以看到,確實是512位元組的大小,即一個扇區的大小。下面我們需要構造一個qemu虛擬機器可以使用的磁碟,並且磁碟中0盤0道1扇區的內容就是上面的檔案內容,這裡我們使用dd命令來構造。
這裡根據書上的內容,簡單介紹一下dd命令的用法
if=FILE #從FILE中讀取資料 of=FILE #將資料輸出到FILE bs=BYTES #指定每一個塊的大小,可以通過ibs、obs分別指定輸入塊、輸出塊的大小 count=BLOCKS #指定拷貝的塊數 seek=BLOCKS #指定輸出時想要跳過的塊數 conv=CONVS #指定如何轉換檔案
這裡實際上我們需要將該二進位制檔案放置到虛擬的磁碟的0盤0道1扇區,也就是第一個塊,因此我們的命令也很簡單,如下所示
dd if=/path/mbr.bin of=/path/mbr.img bs=512 count=1 conv=notrunc
操作如下所示
可以看到,我們成功完成了包含MBR程式的虛擬磁碟的構建,下面我們使用qemu啟動該磁碟即可,命令如下所示
qemu-system-x86_64 mbr.img
命令執行結果如圖所示,可以看到,我們確實實現了該程式的執行,並且也完成了對應的要求。