一、計算機啟動過程回顧
要想寫一個啟動區代碼,就需要了解開機的啟動過程,因為開機過程中一些硬件的規定決定了這段代碼應該怎么寫,不明白沒關系,且聽我慢慢道來。
具體過程在我上一篇文章 【自制操作系統01】硬核講解計算機的啟動過程 講述得一清二楚,這里我們簡單回顧一下。了解開機過程,並不是一個簡單的問題,我總結成你需要有三個前置知識,並記住四次跳躍。
首先說下三個前置知識,這些必須假設你是知道的,否則我不可能從質子中字原子開始講起。這三個前置知識就是:
- 內存是存儲數據的地方,給出一個地址信號,內存可以返回該地址所對應的數據。
- CPU 的工作方式就是不斷從內存中取出指令,並執行。
- CPU 從內存的哪個地址取出指令,是由一個寄存器中的值決定的,這個值會不斷進行 +1 操作,或者由某條跳轉指令指定其值是多少。
有這三個前置知識后,接下來就是要記住計算機開機后的四次關鍵跳躍,因為這些都是當時 Intel 和 BIOS 等制作廠商的大叔們定下來的,沒什么道理可言,記住就好:
- 按下開機鍵,CPU 將 PC 寄存器的值強制初始化為 0xffff0,這個位置是 BIOS 程序的入口地址(一跳)
- 該入口地址處是一個跳轉指令,跳轉到 0xfe05b 位置,開始執行(二跳)
- 執行了一些硬件檢測工作后,最后一步將啟動區內容加載(復制)到內存 0x7c00,並跳轉到這里(三跳)
- 啟動區代碼主要是加載操作系統內核,並跳轉到加載處(四跳)
二、我們需要做什么
假如我們有上帝之手
知道了上面這些后,我們就可以寫啟動區代碼了。別急,我們先想一下我們需要做什么。先不說需要什么軟件,需要什么代碼,這些都不是核心問題,我們先想一下,假如我們有無所不能的上帝之手,我們應該怎么做才能讓開機過程順利走下去呢?
首先,BIOS 里面有一段寫死的代碼,會幫我們把啟動區的第一扇區的 512 字節的內容,原封不動復制到內存 0x7c00 這個位置,並跳轉到此處,這個是不用我們管的。
所以我們要做的就是,把我們的指令,寫到硬盤(當然也可以是光盤、軟盤、U 盤,這里我們就只拿硬盤舉例)的第一扇區的 512 字節,並把這部分標記為“啟動區”(就是把最后兩個字節寫死為 0x55, 0xaa)。
好了,那我們可以試想下,如果我們有上帝之手,應該是這樣一個操作流程。
- 把硬盤從電腦里拿出來
- 在其第一扇區的位置從頭開始寫,010100111001010...,保證最后兩個字節是 0x55, 0xaa(十六進制表示)
- 再把硬盤塞回去
- 按下開機鍵
就是這么簡單,這樣電腦就會乖乖從開機后的某一時刻,開始執行你寫的 010100111001010 的代碼了。
沒有上帝之手的我們該怎么辦?
其實並不需要上帝之手,有很多工具可以直接往磁盤中寫數據,或者你把硬盤拿出來用電路板操作也是可以的,然后在真機上跑你的操作系統。但這樣做太麻煩了,不過就是有人會特別糾結這一點,非要在真機上跑出來才肯罷休,這種精神是值得稱贊的。但學習計算機,該有的抽象還是要有,能用低成本的方式實現等價的事情,不去糾結這一點是很必要的。
所以我們還是用低成本的方式來做今天這個事,剛剛上面說的真機,我們用虛擬機來實現。上面說的硬盤,我們用虛擬磁盤映像文件來實現。剛剛說的寫硬盤這個過程,我們也用軟件命令往這個虛擬磁盤映像里寫。這其實和上面的效果完全等價,只不過都虛擬化了而已。總結起來就是:
- 真機:用虛擬機實現(QEMU)
- 硬盤:用虛擬磁盤映像實現(img)
- 寫數據:用軟件往磁盤映像中寫(dd 命令)
三、最直觀的方式寫一個操作系統的啟動區
好了,有了上面的知識儲備,終於可以實踐了。可是一上來就三個工具,聽起來還是很恐怖,別怕,這部分我們只用到上述的 QEMU 工具即可。
因為虛擬磁盤映像,其實就只是一個二進制文件而已,在 Windows 電腦上直接右鍵,新建一個文本文件就可以了。而寫數據這個過程,我們可以直接用二進制編輯器打開這個文件,直接往里面寫就好了,暫時也不需要什么工具幫助。
因此總結起來,我們這部分的開發環境,就是下面的兩個(具體安裝步驟見章節四)
- Notepad++(含二進制編輯插件)
- QEMU
此時問題已經簡化到極致了,我們只需要編輯一個虛擬磁盤映像文件,再用 QEMU 啟動一下就好了。那么我再簡化一下,虛擬磁盤映像有很多種格式,但其實映射到真正的硬盤中,是一樣的,只不過多種多樣的格式,適合我們一些工具進行方便讀取和展示。
這里我們使用和真正硬盤中數據一一對應的無格式的格式(raw),哈哈這個對刷過 B 站的人應該很容易理解,就是生肉,不經任何加工的原汁原味的格式。這個格式是什么樣的呢?該格式中記錄磁盤第一扇區的 512 字節的位置,就是該文件從第一個字節開始往后的 512 字節,就是這么簡單粗暴。
ok,接下來,我們終於可以開始真正動筆啦!
--------------- 正片開始預警 ------------------
不廢話,直接上三部曲。
第一步:新建文件
右鍵,新建,文本文檔。隨便取一個名字和擴展名。我這里取的是 mbr.raw。mbr 的意思是主引導記錄,raw 的意思是無格式的虛擬磁盤映像格式。但你不必和我一樣。
第二步:寫入數據並保存
用 Notepad++ 文本編輯工具打開它,切換到 16 進制模式,開始一個字節一個字節寫如下內容,寫好后記得保存。
第三步:QEMU 啟動
shift + 右鍵,在此處打開命令行窗口,輸入如下命令並按下回車
qemu-system-i386 mbr.raw
等一秒鍾,QEMU 啟動,並展示出如下畫面。
第四步:沒了
哈哈就是這么簡單,如果你看《30 天自制操作系統》,也有一部分會讓你編輯這個二進制文本文件,但你幾乎要花上半小時時間敲出來,再花半小時時間檢查下到底哪里錯了,最后放棄了,再花十分鍾時間找到這本書的源碼直接 copy 出來,我覺得完全沒有必要。
如果你還覺得多,那你可以試試下面這版:
但再用 QEMU 啟動時會是這個效果(注意第一個字母 h 是筆者通過指令打上去的哦):
嘿嘿,這時你是不是發現了什么,這版比上一版少了哪部分呢?
如果你還不服,那我再來個賴皮版:
別找了,除了最后兩個字節之外,其余的都是 0,用 QEMU 啟動后是這個樣子,這些純是 QEMU 虛擬機中 BIOS 的輸出了,完全沒有我們的一行指令,但它卻正確地開機了。
哈哈,這回我可以自豪地說,我這個是世界上最簡單的啟動區了么?沒有騙你吧。相信你已經有很多疑問了,那我們接下來就是揭秘這個二進制文件的時刻。
四、開發環境准備
不廢話,先直接上開發環境。
文本編輯器:Notepad++
不要下載太高版本,似乎運行插件會有問題,我的是 Notepad++ 7.5.4 release,可以參考。
虛擬機:QEMU
找到 Windows 版的下載按鈕,同樣也是無腦下載,下載好后記得把目錄加入到環境變量里。
匯編工具:NASM
官網下載地址:https://www.nasm.us/
同樣也是找到對應的 Windows 系統,選擇一個版本下載,這里我推薦:
直接點這個連接選擇 nasm-2.14.02-installer-x64.exe 下載即可。
下載好后同樣也記得把 bin 目錄加入到環境變量。
虛擬磁盤映像工具:dd
下載完之后其實就是一個 dd.exe 文件,把這個文件所在的目錄,也加到環境變量里。
OK,整個環境搭建的工作,就結束了,直接全部官方網站一鍵下載,配置下環境變量,就大功告成了,是不是很簡單。這里又想吐槽下《30 天自制操作系統》,作者用了自己寫的匯編工具,自己寫的磁盤拷貝工具,自己寫的虛擬映像生成工具,還有自己為 QEMU 寫的啟動腳本。雖然直接下載其代碼就能直接運行啟動,但我就是感覺很不友好。
五、最簡啟動區代碼的實現
好了,現在我們開始真正實現這個啟動區代碼了。咦,沒錯,剛剛其實已經實現過了,但沒有人會用這種方式寫操作系統。不過當然也可以,你可以只用一個文本編輯器,從第一個字節開始敲,敲到最后一個字節,完成一個操作系統的制作。話說在沒有匯編語言的時候,可不就是這樣做的么,那時候虛擬機更是沒有,需要用紙帶在真機上一遍一遍試。這樣想想看現在幸福多了。
通過上面的環境介紹不難猜出,我們將使用 NASM 匯編語言來實現這個啟動區,最后編譯出來的二進制文件,其實和我們上面手寫的二進制文件是一樣的,反過來說就是,上面給大家展示的二進制文件,可不是我一個個手打的哦,是我實現寫好了匯編代碼,然后編譯出來的(偷笑)。
我們就拿剛剛的第一版二進制文件來看,我們可以把這里面的字節一一取出來,去查 Intel x86 指令集架構下,這些機器指令對應的 NASM 匯編代碼是什么?如果你足夠耐心,是可以一個個查出來的,但我們 NASM 這個軟件本身就有現成的工具來實現這個“反編譯”的功能。
ndisasm -o0x7c00 mbr.raw
我們看到:
- 第一列就是內存地址,第一行 00007c00,剛好應了我們上面說的 BIOS 會把啟動區的代碼加載到內存 0x7c00 這個位置。
- 第二列就是機器指令,對比上面的二進制文件,我們可以看到他們是一一對應的。
- 第三列就是反編譯出來的匯編指令:后面中文已經標注了,第一部分是一段清屏指令的代碼,不然屏幕會亂糟糟出現 QEMU 本身的 bios 輸出。第二部分就是為什么能在屏幕上打印出 hello。第三部分都是 0,其實這不是指令,但如果硬要給他解讀成指令也是可以的。
也就是說,我們按照第三列的匯編指令把代碼寫出來,再編譯,就可以了,對吧?沒錯,不過還需要進行一些小調整,我們直接上代碼。
第一步:新建一個文本文件,命名為 mbr.asm
;----BIOS把啟動區加載到內存的該位置,所以需設置地址偏移量
section mbr vstart=0x7c00
;----卷屏中斷,目的是清屏
mov ax,0x0600
mov bx,0x0700
mov cx,0
mov dx,0x184f
int 0x10
;----直接往顯存中寫數據
mov ax,0xb800
mov gs,ax
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'
;----512字節的最后兩字節是啟動區標識
times 510-($-$$) db 0
db 0x55,0xaa
我們看到,和上面反編譯出來的匯編語句,幾乎是一樣的,但還是有幾處不同,你找找看。下面我來一一解釋。
首先第一行
section mbr vstart=0x7c00
其實也可以寫成:
org 0x7c00
這個的意思用通俗的話講就是把以下代碼加載到內存 0x7c00 這個位置,但這個說法不正確,其本質是 指定一個地址,后面的程序或數據從這個地址值開始分配。比如有個跳轉指令,跳轉到某處的代碼,編譯器可以通過該處代碼偏移了第一行代碼多少來計算出一個相對的偏移地址,但卻無法知道其絕對地址是多少,所以有必要讓程序員來告訴編譯器這個信息。
第二部分
;----卷屏中斷,目的是清屏
mov ax,0x0600
mov bx,0x0700
mov cx,0
mov dx,0x184f
int 0x10
前面幾行是設置一些寄存器的值,有點像高級語言中方法的入參。最后一行 int 0x10 是調用 BIOS 的 10 號中斷程序,有點像高級語言中調用一個方法。這個不是我們的重點,總之它實現了一個效果就是清屏。但你不寫這一段也可以,無非就是讓屏幕多了 QEMU 本身的輸出,難看一點罷了。
第三部分
;----直接往顯存中寫數據
mov ax,0xb800
mov gs,ax
mov byte [gs:0x00],'h'
mov byte [gs:0x02],'e'
mov byte [gs:0x04],'l'
mov byte [gs:0x06],'l'
mov byte [gs:0x08],'o'
這部分的目的就是讓我們的操作系統運行能夠看出一點效果,有很多種方式,我們選擇直接往顯存中寫數據的方式,這種方式最簡單,也最直觀。
上一節課我們講了實模式下的內存分布,知道 0xB8000 - 0xB8FFFF 這段內存空間是文本模式下顯存的內存映射區域,往這個區域里寫數據,就相當於在屏幕上輸出文本了。
所以前兩行就是往 gs 段寄存器中寫入該內存區域的起始地址 0xB800(注意這里不是少寫了一個 0,因為實模式下段寄存器還需要乘以 16,所以此處先除以 16,詳見上一節課的內容)。
后面的五條 mov 語句,就是往這片內存區域的開始的幾個字節處,分別寫入 'h'、'e'、'l'、'l'、'o'。你可能注意到上面反匯編出來的語句中后面不太一樣,其實也可以寫成:
mov byte [gs:0x0],0x68
mov byte [gs:0x2],0x65
mov byte [gs:0x4],0x6c
mov byte [gs:0x6],0x6c
mov byte [gs:0x8],0x6f
這是完全等價的,0x68 是 ‘h’ 的 ASCII 碼,你寫成 ‘h’,匯編工具會自動幫你轉換成 0x68。
第四部分
;----512字節的最后兩字節是啟動區標識
times 510-($-$$) db 0
db 0x55,0xaa
- $ 代表當前行行首的標號(段內地址)
-
\[ 代表當前段的起始匯編地址(段內地址) \]
第二步:編譯
我們上一步得到了 mbr.asm,現在執行下面的命令來編譯它,生成一個 mbr.bin:
nasm -o mbr.bin mbr.asm
執行完后可以看到文件夾下多了一個叫 mbr.bin 的文件,用 Notepad++ 打開它,看看是不是和上面我們看到的二進制文件一樣呢?
第三步:創建虛擬磁盤映像,並填充第一扇區
實際上,我們上面得到的 mbr.bin 文件,直接用 QEMU 啟動也是可以的,因為它與一個 raw 格式的虛擬磁盤映像是一模一樣的。不過我們還是裝模作樣地創建一個虛擬磁盤映像,然后把剛剛的 bin 文件填充到磁盤的第一扇區,這樣才像回事嘛。
執行下面的命令創建一個虛擬磁盤映像:
qemu-img create -f raw mbr.raw 1440K
我們看到后面的數字為 1440K,這就更像回事了,剛剛我們自己手寫的二進制文件,只有 512 字節,沒有那個硬盤是這么小的。這回我們用工具直接創建一個 1440K 的映像,就更接近真實的硬盤啦。
將 mbr.bin 文件的內容,裝載到 mbr.raw 這個空磁盤映像文件的第一扇區:
dd if=mbr.bin of=mbr.raw bs=512 count=1
該命令也是能夠見名知意,就是從 if=mbr.bin 這個文件,往 of=mbr.raw 這個文件拷貝數據,以 bs=512 字節大小作為單位,拷貝 count=1 這么多單位的數據。
第四步:啟動
hooo!到了這步,和我們一開始的情形就是一樣的了,我們有了一個 raw 格式的虛擬磁盤映像文件,我們也有了 QEMU 這個虛擬機可以進行模擬,自然接下來就是最激動人心的時刻(其實早就激動過了)。
qemu-system-i386 mbr.raw
運行效果也是見了很多次的:
好了,啟動區代碼就是這么簡單,其實重點在於對整個過程的理解,並不在於最終運行的那一刻,所以你可以看到大部分時間是在幫你捋順這個過程,實際的代碼量,很少很少。如果本 chat 中有不了解的地方,歡迎在留言區留言,或者先去看一下我上一節課的內容,好多問題可能都會找到答案:
六、開源項目和課程規划
如果你對自制一個操作系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們(下方有公眾號和小助手微信),一起來開發。
項目開源
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都准備一個可執行的代碼。當然文章中的代碼也是全的,采用復制粘貼的方式也是完全可以的。
如果你有興趣加入這個自制操作系統的大軍,也可以在留言區留下您的聯系方式,或者在 gitee 私信我您的聯系方式。
課程規划
本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的操作系統,我覺得這是最好的學習操作系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。