1. 程式人生 > >【自制作業系統01】硬核講解計算機的啟動過程

【自制作業系統01】硬核講解計算機的啟動過程

本講只為講明白下面一個問題:

我們按下開機鍵後究竟發生了什麼?

好的,這似乎是好多人都特別想搞明白的一個問題,有時候非常納悶,為什麼一個看似這麼簡單的問題,就是搜不到一個直面問題的答案呢?

好問題,我也不知道為什麼會這樣,但我猜是因為:

  • 其一,似懂非懂的人太多,他們其實也不知道究竟發生了什麼,所以只能模糊大概地說一些教科書上的話。
  • 其二,知道這個答案的人一定是大牛,大牛要麼不回答這個問題,要麼就不會簡單地回答這個問題。而我呢,自認為剛好處於兩者之間,現在又特別想把自己知道的分享出來,所以你在這裡找到了答案。

我想當你探尋這個問題的答案是,搜到的大多數是這樣的描述:

BIOS按照“啟動順序”,把控制權轉交給排在第一位的儲存裝置:硬碟。然後在硬盤裡尋找主引導記錄的分割槽,這個分割槽告訴電腦作業系統在哪裡,並把作業系統被載入到記憶體中,然後你就能看到經典的啟動介面了,這個開機過程也就完成了。

這種描述簡直太魔幻了,為什麼是 BIOS 主導這一切?怎麼叫按照啟動順序?這個分割槽咋就被載入到記憶體了,有咋告訴電腦作業系統在哪裡了?我無法忍受這樣的魔幻描述,我非要把它說得清清楚楚。

首先學一個東西,一定要有一個前置的知識,我們把它當做已知的,我不可能從原子組成分子開始講原理。那學習計算機啟動過程的前置知識是什麼呢?我要求你已知以下幾點:

  1. 記憶體是儲存資料的地方,給出一個地址訊號,記憶體可以返回該地址所對應的資料。
  2. CPU 的工作方式就是不斷從記憶體中取出指令,並執行。
  3. CPU 從記憶體的哪個地址取出指令,是由一個暫存器中的值決定的,這個值會不斷進行 +1 操作,或者由某條跳轉指令指定其值是多少。

好了,只需要知道這三點前置知識,你就能專業地解釋計算機的啟動過程了。

一、為什麼是 BIOS 主導?

都說開機後,BIOS 就開始執行自己的程式了,又硬體自檢,又載入啟動區的。我就不服了,為什麼開機後是執行 BIOS 裡的程式?為啥不是記憶體裡的?為啥不是硬盤裡的?

好的,不要懷疑前置知識,CPU 的工作方式,就是不斷從記憶體中取指令並執行,那為什麼會說是執行 BIOS 裡的程式呢?這就不得不說說記憶體映射了。

二、記憶體對映

CPU 地址匯流排的寬度決定了可訪問的記憶體空間的大小。比如 16 位的 CPU 地址匯流排寬度為 20 位,地址範圍是 1M。32 位的 CPU 地址匯流排寬度為 32 位,地址範圍是 4G。你可以算算我們現在的 64 位機的地址範圍。

可是,可訪問的記憶體空間這麼大,並不等於說全都給記憶體使用,也就是說定址的物件不只有記憶體,還有一些外設也要通過地址匯流排的方式去訪問,那怎麼去訪問這些外設呢?就是在地址範圍中劃出一片片的區域,這塊給視訊記憶體使用,那塊給硬碟控制器使用,等等 。

這樣說,其實就不符合我們的前置知識了,所以可以有一種不太正確的理解方式,那就是記憶體中的這塊位置就是視訊記憶體,那塊位置就是硬碟控制器。我們在相應的位置上讀取或者寫入,就相當於在視訊記憶體等外設的相應位置上讀取或者寫入,就好像這些外設的儲存區域,被對映到了記憶體中的某一片區域一樣。這樣我們就不用管那些外設啦,關注點仍然是一個簡簡單單的記憶體。這就是所謂的記憶體對映。

太好了,現在又用簡單的前置知識就能解釋得通了,我們繼續往下推。

三、真實模式下的記憶體分佈

剛剛說到記憶體中劃分出了一片一片區域給各種外設,那麼問題自然就來了,哪塊區域,分給了哪塊外設了呢?如果是規定,那應該有一張表比較好吧。嗯沒錯,還真有,它就是真實模式下的記憶體分佈,筆者給它畫了一張圖:

哎喲我真是個小天使,把比例都表現出來了,網上能再找出比我這個更直觀的請給我留言。真實模式之後再解釋,現在簡單理解就是計算機剛開機的時候只有 1M 的記憶體可用。

我們看到,記憶體被各種外設瓜分了,即對映在了記憶體中。BIOS 更狠,不但其空間被對映到了記憶體 0xC0000 - 0xFFFF 位置,其裡面的程式還佔用了開頭的一些區域,比如把中斷向量表寫在了記憶體開始的位置,真所謂先到先得啊。

四、怎麼就從 BIOS 裡的程式開始執行了

好了,現在我們知道 BIOS 裡的資訊被對映到了記憶體 0xC0000 - 0xFFFF 位置,其中最為關鍵的系統 BIOS 被對映到了 0xF0000 - 0xFFFF 位置。假如我現在說,CPU 開機就是執行了這塊區域的程式碼,然後巴拉巴拉一頓操作就開機了,你肯定要噴我了,為什麼就執行到這了呢,那咋不從頭開始執行?

這就自然有了一種猜想,我們要用到另一個前置知識了,就是 CPU 從記憶體的哪個位置取出執行並執行呢?是 PC 暫存器中的地址值。BIOS 程式的入口地址也就是開始地址是 0xFFFF0(人家就那麼寫的),也就是開機鍵一按下,一定有一個神奇的力量,將 pc 暫存器中的值變成 0xFFFF0,然後 CPU 就開始馬不停蹄地跑了起來。沒錯,接下來這句話,可能就是你找了很久的答案,請做好準備:

在你開機的一瞬間,CPU 的 PC 暫存器被強制初始化為 0xFFFF0。如果再說具體些,CPU 將段基址暫存器 cs 初始化為 0xF000,將偏移地址暫存器 IP 初始化為 0xFFF0,根據真實模式下的最終地址計算規則,將段基址左移 4 位,加上偏移地址,得到最終的實體地址也就是抽象出來的 PC 暫存器地址為 0xFFFF0。

當我在學習這段知識時,看到這句話才讓將我心裡積壓了很久的疑惑解開,多麼簡單粗暴的道理啊。寫到這裡我也是長舒了一口氣,因為剩下的過程,就幾乎只是流水賬一樣的正推了。

至於怎麼強制初始化的,我覺得就越過了前置知識的邊界了,況且各個廠商的硬體實現也不一定相同,有很多辦法,也很簡單。討論起來意義就不大了。

五、BIOS 裡到底寫了什麼程式

好了,我們現在知道了 BIOS 被對映到了記憶體的某個位置,並且開機一瞬間 CPU 強制將自己的 pc 暫存器初始化為 BIOS 程式的入口地址,從這裡開始 CPU 馬不停蹄地向前跑了起來。那接下來的問題似乎也非常自然地就問出來了,那就是 BIOS 程式裡到底寫了啥?

把 BIOS 程式裡的二進位制資訊全貼出來也不合適,我們分析一些主要的。我們首先還是來猜測,你看入口地址是 0xFFFF0,說明程式是從這執行的。真實模式下記憶體的下邊界就是 0xFFFFF,也就是隻剩下 16 個位元組的空間可以寫程式碼了,這夠幹啥的呢?如果你有心的話應該能猜出,入口地址處可能是個跳轉指令,跳到一個更大範圍的空間去執行自己的任務。沒錯就是這樣,0xFFFF0 處儲存的機器指令,翻譯成組合語言是:

jmp far f000:e05b

意思是跳轉到實體地址 0xfe05b 處開始執行(回憶下前面說的真實模式下的地址計算方式)。

地址 0xfe05b 處開始,便是 BIOS 真正發揮作用的程式碼了,這塊程式碼會檢測一些外設資訊,並初始化好硬體,建立中斷向量表並填寫中斷例程。這裡的部分不要展開,這只是一段寫死的程式而已,而且對理解開機啟動過程無幫助,我們看後面精彩的部分,也就是 BIOS 的最後一項工作:載入啟動區。

六、0x7c00 是啥

該較真的地方就是要較真,我絕對不會讓載入這種魔幻的詞出現在這裡,我們現在就來把它拆解成人話。

其實這個詞也並不魔幻,載入在計算機領域就是指,把某裝置上(比如硬碟)的程式複製到記憶體中的過程。那載入啟動區這個過程,翻譯過來就是,BIOS 程式把啟動區的內容複製到了記憶體中的某個區域。好了,問題又自然出來了,啟動區是哪裡?被複制到了記憶體的哪個位置?然後呢?我們一個個來回答。

什麼是啟動區呢?即使你不知道,你也應該能夠猜到,一定是符合某種特徵的一塊區域,於是人們把它就叫做啟動區了,那要符合什麼特徵呢?先不急,不知道你有沒有過設定 BIOS 啟動順序的經歷,通常有 U 盤啟動、硬碟啟動、軟盤啟動、光碟啟動等等,BIOS 會按照順序,讀取這些啟動盤中位於 0 盤 0 道 1 扇區的內容。

至於磁碟格式的劃分,本篇就不做講解了,總之對於記憶體,我們給出一個數字地址就能獲取到該地址的資料,而對於磁碟,我們需要給出磁頭、柱面、扇區這三個資訊才能定位某個位置的資料,都是描述位置的一種方式而已。

接著說, 這 0 盤 0 道 1 扇區的內容一共有 512 個位元組,如果末尾的兩個位元組分別是 0x55 和 0xaa,那麼 BIOS 就會認為它是個啟動區。如果不是,那麼按順序繼續向下個裝置中尋找位於 0 盤 0 道 1 扇區的內容。如果最後發現都沒找到符合條件的,那直接報出一個無啟動區的錯誤。

BIOS 找到了這個啟動區之後幹嘛呢?哦,前面說過了是載入,就是把這 512 個位元組的內容,一個位元都不少的全部複製到記憶體的 0x7c00 這個位置。怎麼複製的?當然是指令啦。哪些指令呢?這裡我只能簡單說指令集中是有 in 和 out 的,用來將外設中的資料複製到記憶體,或者將記憶體中的資料複製到外設,用這兩個指令,以及外設給我們提供的讀取方式,就能做到這一點啦。

啟動區內容此時已經被 BIOS 程式複製到了記憶體的 0x7c00 這個位置,然後呢?這個其實也不難猜測,啟動區的內容就是我們自己寫的程式碼了,複製到這裡之後,就開始執行唄,之後我們的程式就接管了接下來的流程,BIOS 的使命也就結束啦。所以複製完之後,接下來應該是一個跳轉指令吧!沒錯,正是這樣,PC 暫存器的值變為 0x7c00,指令開始從這裡執行。

咦?不知道你有沒有發現,我們似乎不知不覺又把之前的一句魔法語言翻譯成人話了,開頭我們說:

BIOS 把控制權轉交給排在第一位的儲存裝置。

所以這句話是什麼意思呢?就是 BIOS 把啟動區的 512 位元組複製到記憶體的 0x7c00 位置,並且用一條跳轉指令將 pc 暫存器的值指向 0x7c00。你看,這不是也沒多幾個字嘛,就把這個問題說得明明白白,簡簡單單。

哦,對了,現在似乎就剩下一個問題了,為什麼非要是 0x7c00 呢?好問題,當然答案也很簡單,那就是人家 BIOS 開發團隊就是這樣定的,之後也不好改了,不然不相容。為什麼不好改?我們看一個簡單的啟動區 512 位元組的程式碼。(程式碼摘抄自《30天自制作業系統》)

; hello-os
; TAB=4

        ORG     0x7c00          ;程式載入到記憶體的 0x7c00 這個位置
        
; FAT12格式軟盤專用

        JMP     entry
        DB      0x90    ;
        DB      "HELLOIPL"      ;啟動區名稱(8位元組)
        DW      512             ;每個扇區大小(必須為512位元組)
        DB      1               ;簇大小(必須為1個扇區)
        DW      1               ;FAT的起始位置(一般從第一個扇區開始)
        DB      2               ;FAT的個數(必須為2)
        DW      224             ;根目錄大小(一般為224項)
        DW      2880            ;該磁碟的大小(必須為2880扇區)
        DB      0xf0            ;磁碟的種類(必須是0xf0)
        DW      9               ;FAT的長度(必須是9扇區)
        DW      18              ;1個磁軌有幾個扇區(必須為18)
        DW      2               ;磁頭數(必須是2)
        DD      0               ;不使用分割槽,必須是0
        DD      2880            ;重寫一次磁碟大小
        DB      0,0,0x29        ;意義不明,固定
        DD      0xffffffff      ;卷標號碼
        DB      "HELLO-OS   "   ;磁碟的名稱(11位元組)
        DB      "FAT12   "      ;磁碟格式名稱(8位元組)
        RESB    18              ;空出18個位元組

;程式主體

entry:
        MOV     AX,0            ;初始化暫存器
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX           ;段暫存器初始化為 0
        MOV     ES,AX
        MOV     SI,msg
putloop:
        MOV     AL,[SI]
        ADD     SI,1
        CMP     AL,0            ;如果遇到 0 結尾的,就跳出迴圈不再列印新字元
        JE      fin
        MOV     AH,0x0e         ;指定文字
        MOV     BX,15           ;指定顏色
        INT     0x10            ;呼叫 BIOS 顯示字元函式
        JMP     putloop
fin:
        HLT
        JMP     fin
msg:
        DB      0x0a,0x0a       ;換行、換行
        DB      "hello-os"
        DB      0x0a            ;換行
        DB      0               ;0 結尾
    
        RESB 0x7dfe-$           ;填充0到512位元組
        DB  0x55, 0xaa          ;可啟動裝置標識

我們看第一行:

ORG     0x7c00

這個數字就是剛剛說的啟動區載入位置,這行彙編程式碼簡單說就表示把下面的地址統統加上 0x7c00。正因為 BIOS 將啟動區的程式碼載入到了這裡,因此有了一個偏移量,所以所有寫啟動區程式碼的人就需要在開頭寫死一個這樣的程式碼,不然全都串位了。

然後正因為所有寫作業系統的,啟動區的第一行彙編程式碼都寫死了這個數字,那 BIOS 開發者最初定的這個數字就不好改了,否則它得挨個聯絡各個作業系統的開發廠商,說唉我這個地址改一下哈,你們跟著改改。在公司推動另一個團隊改個程式碼都得大費周折,想想看這樣的推動得耗費多大人力。況且即使改了,之前的程式碼也都不相容了,這不得被人們罵死啊。

再看最後一行:

DB  0x55, 0xaa

這也驗證了我們之前說的這 512 位元組的最後兩個位元組得是 0x55 0xaa,BIOS才會認為它是一個啟動區,才會去載入它,僅此而已。

回過頭來說 0x7c00 這個值,它其實就是一個規定死的值,但還是會有人問,那必然有它的合理性吧。其實,我的解釋也只能說是人家規定了這個值,後人們替他們解釋這個合理性,並不是說當初人家就一定是這樣想的,就好比我們做語文的閱讀理解題一樣。

第一個 BIOS 開發團隊是 IBM PC 5150 BIOS,當時被認為的第一個作業系統是 DOS 1.0 作業系統,BIOS 團隊就假設是為它服務的。但作業系統還沒出,BIOS 團隊假設其作業系統需要的最小記憶體為 32 KB。BIOS 希望自己所載入的啟動區程式碼儘量靠後,這樣比較“安全”,不至於過早的被其他程式覆蓋掉。可是如果僅僅留 512 位元組又感覺太懸了,還有一些棧空間需要預留,那擴大到 1 KB 吧。這樣 32 KB 的末尾是 0x8000,減去 1KB(0x400) ,剛好等於 0x7c00。哇塞,太精準了,這可以是一種解釋方式。

七、啟動區裡的程式碼寫了啥

其實寫到這,我這篇文章就應該戛然而止了,因為最初的那個問題已經解決了,CPU 已經開始馬不停蹄地從我們預期的位置跑起來了,萬事開頭難,剩下的內容,就是作業系統想怎麼玩就怎麼玩了。

但我覺得還不夠味,似乎還有些問題縈繞在你腦海裡。比如說這個問題:

啟動區裡的程式碼寫了啥?就 512 位元組就是全部作業系統內容了?

這是一個好問題,512 個位元組確實幹不了啥,現在的作業系統怎麼也得按 M 為單位算吧,512 個位元組遠遠不夠呢,那是怎麼回事呢?

其實我們可以按照之前的思路猜測,BIOS 用很少的程式碼就把 512 位元組的啟動區內容載入到了記憶體,並跳轉過去開始執行。那按照這個套路,這 512 位元組的啟動區程式碼,是不是也可以把更多磁碟中儲存的作業系統程式,載入到記憶體的某個位置,然後跳轉過去呢?

沒錯,就是這個套路。所以 BIOS 負責載入了啟動區,而啟動區又負責載入真正的作業系統核心,這配合默契吧?

由於用於啟動盤的磁碟是人家寫作業系統的廠商製作的,俗稱制作啟動盤,所以他也肯定知道作業系統的核心程式碼儲存在磁碟的哪個扇區,因此啟動區就把這個扇區,以及之後的好多好多扇區(具體取決於作業系統有多大)都讀到記憶體中,然後跳轉到開始的程式開始的位置。跳轉到哪裡呢?這個就不像 0x7c00 這個數那麼經典了,不同的作業系統肯定也不一樣,也不用事先規定好,反正寫作業系統的人給自己定一個就好了,別覆蓋其他關鍵裝置用到的區域就好。

八、作業系統核心寫了啥

好了現在經過好幾輪跳跳跳,終於跳到核心程式碼啦,我們來一起回顧一下:

  1. 按下開機鍵,CPU 將 PC 暫存器的值強制初始化為 0xffff0,這個位置是 BIOS 程式的入口地址(一跳)
  2. 該入口地址處是一個跳轉指令,跳轉到 0xfe05b 位置,開始執行(二跳)
  3. 執行了一些硬體檢測工作後,最後一步將啟動區內容載入到記憶體 0x7c00,並跳轉到這裡(三跳)
  4. 啟動區程式碼主要是載入作業系統核心,並跳轉到載入處(四跳)

經過這連續的四次跳躍,終於來到了作業系統的世界了,剩下的內容,可以說是整個作業系統課程所講述的原理。寫到這,我真的可以結束了,可是,我還是感覺對不起我的讀者。

所以作業系統核心究竟寫了啥?其實我也沒研讀過原始碼,只是知道大概的流程罷了,有幾個可能你一直困惑的問題我想在這裡和你聊聊。

軟硬體協同發展

有幾個概念我想放在一塊說,中斷、分段、分頁。這有啥關係呢?我想說,他們的共性就是他們都是軟體與硬體相結合的產物。

  • 中斷,軟體提供了中斷向量表和中斷程式,而硬體留給我們一個 IDTR 暫存器來儲存中斷向量表的起始地址,並且提供了硬體機制來傳遞中斷訊號。
  • 分段,軟體提供了段表和端描述符,而硬體留給我們一個 GDTR 暫存器來儲存段表的起始地址,並且提供了硬體機制,將段基址暫存器裡的值通過查段表來和段偏移地址進行拼接形成實體地址,並且提供了段保護機制。
  • 分頁,軟體提供了一級頁表和二級頁表,以及提供缺頁中斷異常的處理程式。而硬體留給我們一個 PTBR 暫存器來儲存頁表的其實地址,並且提供了硬體機制,將邏輯地址轉換為實體地址,記錄頁的訪問次數,並在適當的時候發出缺頁中斷異常。

所以你看,他們放在一起是不是很工整呢?所以有很多人有這樣的誤區,認為某個技術只是軟體來實現的,或者只是硬體來實現的,那必然怎麼想都想不通。

歷史遺留問題

還有一些你可能覺得很彆扭的概念,比如說真實模式與保護模式。

覺得彆扭就對了,因為有些計算機問題是歷史遺留問題,並不是合理發展的產物。最初的 x86 架構的 CPU 是 8086,它是 16 位機,所有的暫存器都是 16 位的,只有地址線是 20 位的,可以訪問 1M 的地址空間。它訪問空間的方式是段基址暫存器左移四位,加上段偏移地址,形成一個實體地址,送到地址線上。

這有兩個壞處,第一就是可訪問的空間太小,跟不上計算機發展的需要。第二就是缺少保護機制,程式可以隨意訪問這 1M 的地址空間,甚至覆蓋掉作業系統本身的程式碼和資料。

這樣作業系統的發展和需求,就倒逼著 CPU 的發展,於是有了新的 32 位 CPU。32 位 CPU 地址空間擴大到了 4G,也通過段選擇子的方式對記憶體進行了保護,有了很多新的特性。但為了相容老程式,必須還要支援 16 位機的特徵,所以就有了兩個模式,並且程式還要通過呼叫一些 CPU 的指令從一個模式切換到另一個模式,比如經典的開啟 A20 地址線。

而這兩個模式,其實我們完全可以按照更準確的理解,叫做 16 位模式和 32 位模式,然後補充一句說 32 位模式比 16 位模式有了更大的定址空間(這是自然的)和更好的記憶體保護機制。但由於 CPU 廠商希望凸顯他們新模式的優勢,所以直接把最關鍵的優勢“保護”給放到了名字裡,叫保護模式。而為了對比也要給之前的模式起一個名字,之前的模式比較實在,給出什麼地址就直接是實體地址,也沒有轉換也沒有安全保護,那就叫它真實模式吧。

所以,這個命名方式,以及這是歷史遺留問題而妥協的設計方式,成為了好多人理解這兩種模式的障礙,總覺得不能見名知意是自己沒有理解到位,感覺彆彆扭扭的就說明自己沒有了解到它的好處。其實除了相容老版本外,根本沒有一點好處,重新設計 32 位機才是最乾淨的做法。

歷史遺留問題還有很多,我敢說所有你覺得設計的很彆扭的地方,可能大概率都是歷史遺留問題,比如說段基址暫存器的存在,如果不是因為當初 16 位機器卻有 20 位地址線,可能就沒有這種蹩腳的設計了。不過好在後來把它用在了段保護機制上,才有了點作用,可畢竟最初的設計,只是為了妥協呀。瞭解歷史因素,還是有好處的。

九、參考資料

好了,這回我真的要結束了,相信如果你真的看完了全文,計算機的啟動過程,可以說有了比較具象的瞭解。如果你想深入細節,也就是了解整個過程的每一點,那可要下功夫了。

初學者推薦兩本書籍,可以順序閱讀:

  • 《30 天自制作業系統》
  • 《作業系統真象還原》

十、開源專案和課程規劃

如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們,一起來開發。

專案開源

專案開源地址:https://gitee.com/sunym1993/flashos

當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。

如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。

課程規劃

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈