羽夏看Linux核心——啟動那些事

2022-08-07 12:00:34

寫在前面

  此係列是本人一個字一個字碼出來的,包括範例和實驗截圖。如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

前言

  之前我們搭建好了Bochs學習環境(沒搭好的回去弄好再回來看),可惜沒有合法的啟動盤,那麼什麼是啟動盤,如何正確的啟動,下面我們來開始介紹基礎部分。

BIOS

  BIOS全稱叫Base Input & Output System,即基本輸入輸出系統。它的主要工作是檢測、初始化硬體。

真真實模式下的 1MB 記憶體佈局

  Intel 8086有20條地址線,故其可以存取1MB的記憶體空間。若按十六進位制來表示,是0x00000 - 0xFFFFF。這lMB的記憶體空間被分成多個部分。如下表格所示:

  記憶體地址0x00000 - 0x9FFFF的空間範圍是64KB,這片地址對應到了動態隨機存取記憶體DRAM,也就是插在
上的記憶體條;而0xF0000 - 0xFFFFF64KB記憶體是ROM,是唯讀的,存的就是BIOS的程式碼。硬體自己提供了一些初始化的功能呼叫,BIOS可以直接呼叫,並建立了中斷向量表,就可以通過int 中斷號來實現相關的硬體呼叫。而這些中斷只有重要的、保證計算機能執行的那些硬體的基本IO操作,不像高階語言有各種花裡胡哨的功能。
  我們還要說明一個問題:在CPU眼裡,我們插在主機板上的實體記憶體不是它眼裡「全部的記憶體」。這個是由地址匯流排寬度決定了可以存取的記憶體空間大小。打個比方,比如小孩學數蘋果數目。結果他只會100以內的,如果蘋果數目超了100,就不會數了,不認識了。實體記憶體也是如此,再多的記憶體,只要識別能力不夠,也是浪費。

BIOS 啟動

  BIOS是計算機上第一個執行的軟體,所以它不可能自己載入自己,由此可以知道,它是由硬體載入的。BIOS程式碼所做的工作也是一成不變的,而且在正常情況下,其本身一般是不需要修改的,儲存在ROM中。ROM也是塊記憶體,記憶體就需要被存取。而ROM被對映在0xF0000 - 0xFFFFF處,只要存取此處的地址便是存取了BIOS,這個對映是由硬體完成的。如果不太理解,可以學一下微控制器的基礎知識和電工學下冊。
  BIOS本身是個程式,程式要執行,就要有個入口地址才行,此入口地址便是0xFFFF0

  當我們開始啟動虛擬機器器進入偵錯狀態時,你會看到如下內容:

  可以看到,ip指向的地址指令是jmp far f000:e05b,這個是跨段跳轉,最後執行結果是到了0xFE05B這個地址,這個是真正BIOS程式碼開始的地方。
  接下來BIOS便馬不停蹄地檢測記憶體、顯示卡等外設資訊,當檢測通過,並初始化好硬體後,開始在記憶體中0x000-Ox3FF處建中斷向量表IVT並填寫中斷例程。然後它的任務完成了,剩下的部分就是交給下一個「負責人」繼續處理。

0x7c00 雜談

  BIOS最後一項工作校驗啟動盤中位於0盤0道1磁區的內容。在計算機中是習慣以0作為起始索引的,用「相對」的概念,即偏移量來表示位置顯得很直觀,所以很多指令中的運算元都是用偏移表示的。0盤0道1磁區本質上就相當於0盤0道0磁區。為什麼稱為1磁區呢?因為硬碟磁區的表示法有兩種,我們描述0盤0道1磁區用的便是其中的一種:CHS方法,即柱面Cylinder、磁頭Header、磁區Sector;另外一種是LBA方式,這裡救不說了。0盤說的是0磁頭,因為1張盤是有上下兩個盤面的,1個盤面上對應一個磁頭,所以用磁頭Header來表示盤面。0道是指0柱面,柱面Cylinder指的是所有盤面上、編號相同的磁軌的集合,形象一點描述就是把很多環疊摞在一起的樣子,組合在之後是1個立體的管狀。1磁區是將磁軌等距劃分成一段段的小區間,由於磁軌是圓形的,確切地說是圓環,這些被劃分出來的小區間便是扇形,所以稱為磁區,而在CHS方式中磁區的編號是從1開始的
  如果此磁區末尾的兩個位元組分別是魔數0x55OxAABIOS便認為此磁區中確實存在可執行的程式,此程式便是主開機記錄MBR,它會被載入到實體地址0x7c00,隨後跳轉到此地址,繼續執行。反之,它就不認。
  BIOS跳轉到Ox7c00是用jmp 0:Ox7c00實現的,此時段暫存器cs會被替換成0

為什麼 MBR 住在這裡

  因為近啊。就好比你會把經常用的放到身邊,用到就會直接拿出來,如果把它放到老遠的位置,這個不就費勁了嗎。對於BIOS來說,MBR就是經常用的東西,放到身邊才方便。

為什麼是 0x7c00 地址

  據說是歷史原因,BIOS規範。它最早出現在IBM公司出產的個人電PC5150 ROM BIOSINT 19H中斷處理程式中。
  MBR不是隨便放在哪裡都行的,首先不能覆蓋己有的資料,其次,不能過早地被其他資料覆蓋。通常MBR的任務是載入某個程式(這個程式一般是核心載入器,很少有直接載入核心的)到指定位置,並將控制權交給它。
  按DOS 1.0要求的最小記憶體32KB來說,MBR希望給人家儘可能多的預留空間,這樣也是保全自己的作法,免得過早被覆蓋,所以MBR只能放在32KB的末尾。其次,MBR本身也是程式,是程式就要用到棧,棧也是在記憶體中的,雖然本身只有512位元組,但還要為其所用的棧分配點空間,所以其實際所用的記憶體空間要大於512位元組,估計1KB記憶體夠用了。
  綜上,選擇32KB中的最後1KB最為合適,那此地址是多少呢?32KB換算為十六進位製為0x8000,減去1KB(0x400)的話,等於0x7c00

MBR 雜談

  MBR是獨立於作業系統的,能夠直接在裸機上執行。它的大小必須是512位元組,保證0x550xAA這兩個
魔數恰好出現在該磁區的最後兩個位元組處。下面我們來編寫一個MBR程式,並讓它跑起來。
  我們本教學使用的16位元組合器是as86,也是Linux 0.11編寫啟動程式碼的其中一個組合器。它的組合語法類似Intel的,而不是麻煩的AT&T,具體用法請在終端輸入man as86檢視。
  現在as86並不自帶,我們需要安裝,在終端輸入以下指令:

sudo apt install bin86

  安裝成功後,如果輸入as86顯示如下資訊,表示安裝成功:

as: usage: as [-03agjuwO] [-b [bin]] [-lm [list]] [-n name] [-o obj] [-s sym] src

  從頭啥也不會開始寫也不現實,我給出一個以供參考:

.globl begtext,begdata,begbss,endtext,enddata,endbss ;全域性識別符號,供 ld86 連結使用。
.text 
begtext:
.data
begdata:
.bss
begbss:
.text

BOOTSEC=0x7C0

entry start
start:
    jmpi go,BOOTSEC ;段間跳轉 BOOTSEC 指出跳轉地址,標號go是偏移地址
go:    
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov cx,#20      ;共顯示20個字元
    mov dx,#0x1004  ;字元顯示在螢幕第17行,第5列處
    mov bx,#0x000c  ;字元顯示屬性為紅色
    mov bp,#msg     ;指向要顯示的字元
    mov ax,#0x1301  ;寫字串並移動遊標到串結尾處
    int 0x10
loop0: jmp loop0    ;死迴圈
msg: 
    .ascii "Loading system...!" 
    .byte 13,10
.org 510 ;表示以後語句從地址 510 偏移開始存放
    .word 0xAA55    ;有效引導磁區標誌,提BIOS載入引導磁區
.text
endtext:
.data
enddata:
.bss
endbss:

  這個程式碼我命名為boot.s,然後在該程式碼所在資料夾下進入終端,輸入以下指令:

as86 -0 -a -o boot.o boot.s
ld86 -0 -d -o boot.bin boot.o

編譯引數

  這樣得到的就是我們想要的內容檔案boot.bin。不過我們得把這幾個命令列引數介紹一下。

-0

  as86是生成16位元組合程式碼,如果用了超過8086指令集發出警告。
  ld86是生成16位元檔案頭,這個我們不要,需要刪除。

-a

  啟用與Minix asld的部分相容性,看不懂可以不管。

-d

  刪除檔案頭。

-o

  輸出檔名/路徑。

寫入映象

  得到boot.bin之後,我們需要將這個資料寫入虛擬映象test.img當中,我們需要輸入以下命令:

dd if=boot.bin of=test.img bs=512 count=1 conv=notrunc 

  dd是用於磁碟操作的命令,可以深入磁碟的任何一個磁區。如果要了解詳情,請在終端輸入man dd。這裡我們僅僅介紹我們使用的引數。

if=

  指定要讀取的檔案。

of=

  指定把資料輸出到哪個檔案。

bs=

  指定塊的大小,dd是以塊為單位來進行IO操作的,得指明塊是多大位元組。

count=

  指定拷貝的塊數。

conv=

  指定如何轉換檔案。建議在追加資料時,conv最好用notrunc方式,也就是不打斷檔案。

測試

  執行完這些操作後,我們雙擊我們的startLearning.sh看看結果:

  這就說明成功了。

編寫 Makefile

  以後我們如果頻繁更改編譯,每次輸入這幾個指令是不是太麻煩了?我們可以寫一個Makefile檔案,每次只需在該目錄下輸入make就可以重新編譯:


all: boot.bin img

boot.bin: boot.s
    as86 -0 -a -o boot.o boot.s
    ld86 -0 -d -o boot.bin boot.o

img: boot.bin
    rm -f test.img
    bximage -hd -mode="flat" -size=60 -q test.img 
    dd if=boot.bin of=test.img bs=512 count=1 conv=notrunc  

clean:
    rm -f boot.bin boot.o test.img

磁碟讀寫

  有關磁碟結構,這裡就不多說了,我們把重點放到如何用組合來讀寫磁碟相關內容。如果想詳細瞭解建議看《作業系統真相還原》的第134頁,或者從網路找相關資料。
  硬碟控制器屬於IO介面,CPU和硬碟打交道是通過硬碟控制器實現的,開始硬碟和控制器是分開的,後來被整到一起,這種介面便稱為整合裝置電路( Integrated Drive Electronics, IDE )。
  讓硬碟工作,我們需要通過讀寫硬碟控制器的埠,埠的概念在此重複下,埠就是位於IO制器上的暫存器,此處的埠是指硬碟控制器上的暫存器。但硬碟十分複雜,目前我們只用到其中的一小部分,具體瞭解詳情請自行搜尋AT Attachment with Packet Interface,一共三卷。

  埠可以被分為兩組,Command Block registersControl Block registersCommand Block registers用於向硬碟機寫入命令宇或者從硬碟控制器獲得硬碟狀態,Control Block registers用於控制硬碟工作
狀態。在Control Block registers組中的暫存器已經精減了,而且咱們基本上用不到,就不贅述了,下面重點介紹Command Block registers組中的暫存器。
  埠是按照通道給出的,也就是說,埠不是直接針對某塊硬碟的。一個通道上的主、從兩塊硬碟都用這些埠號,要想操作某通道上的某塊硬碟,需要單獨指定。
  Data暫存器在名字上我們就知道它是負責管理資料的,它相當於資料的門,資料能進,也能出,所以其作用是讀取或寫入資料。這個暫存器是16位元的,得到了特殊照顧。在讀硬碟時,硬碟準備好的資料後,硬碟控制器將其放在內部的緩衝區中,不斷讀此暫存器便是讀出緩衝區中的全部資料。在寫硬碟時,我們要把資料來源源不斷地輸送到此埠,資料便被存入緩衝區裡,硬碟控制器發現這個緩衝區中有資料了,便將此處的資料寫入相應的磁區中。
  讀硬碟時,埠0x1710x1F1的暫存器名字叫Error暫存器,只在讀取硬碟失敗時有用,裡面才會記錄失敗的資訊,尚未讀取的磁區數在Sector count暫存器中。在寫硬碟時,此暫存器有了別的用途,被稱之為Feature暫存器。有些命令需要指定額外引數,這些引數就寫在Feature暫存器中。暫存器都是8位元寬度。
  Sector count暫存器用來指定待讀取或待寫入的磁區數。硬碟每完成一個磁區,就會將此暫存器的值減一,所以如果中間失敗了,此暫存器中的值便是尚未完成的磁區。這是8位元暫存器,最大值為255,若指定為0,則表示要操作256個磁區。
  硬碟中的磁區在物理上是用柱面-磁頭-磁區來定位的Cylinder Head Sector,簡稱為CHS,但每次我們要事先算出磁區是在哪個盤面,哪個柱面上,這太麻煩了,但這對於磁頭來說很直觀,它就是根據這些資訊來定位磁區的。我們希望磁碟中磁區從
0開始依次遞增編號,不用考慮磁區所在的物理結構,這是一種邏輯上為磁區址的方法,全稱為邏輯塊地址Logical Block Address
  LBA有兩種,一種是LBA28,用28位位元來描述一個磁區的地址,最大支援128 GB,為了簡單,我們可以使用該方式;另外一種是LBA48,用48位位元來描述一個磁區的地址,最大支援131072 TB,目前沒有任何記憶體超過該大小。
  介紹完了LBA,現在可以說LBA暫存器了,這裡有LBA lowLBA midLBA high三個,它們三個都是8位元寬度的。LBA low暫存器用來儲存0-7位,LBA mid暫存器用來儲存8-15位,LBA high暫存器儲存16-23位。但這總共才24位元,連LBA28都不夠,咱們怎麼用呢?Device暫存器。
  Device暫存器是個雜項,它的寬度是8位元。在此暫存器的低4位元用來儲存LBA地址的24-27位。索引4位用來指定通道上的主盤或從盤,0代表主盤,1代表從盤。索引5位用來設定是否啟用LBA方式,1代表啟用LBA模式,0代表啟用CHS模式。剩餘的兩位,稱為MBS位,都固定是1
  在讀硬碟時,埠0x1F70x177的暫存器名稱是Status,它是8位元寬度的暫存器,用來給出硬碟的狀態資訊。索引0位是ERR位,如果此位為1,表示命令出錯了,具體原因可見Error暫存器。索引3位是Request位,如果此位為1,表示硬碟己經把資料準備好了,主機現在可以把資料讀出來。索引6位是 DRDY,表示硬碟就緒,此位是在對硬碟診斷時用的,表示硬碟檢測正常,可以繼續執行一些命令。索引7位是BSY位,表示硬碟是否繁忙,如果為1表示硬碟正忙著,此暫存器中的其他位都無效。剩餘的幾位用不到暫且不關注。
  在寫硬碟時,埠0x1F70x177的暫存器名稱是Command。此暫存器用來儲存讓硬碟執行的命令,只要把命令寫進此暫存器,硬碟就開始工作了。在咱們的系統中,主要使用了三個命令。

  1. identify: 0xEC ,即硬碟識別。
  2. read sector: 0x20 ,即讀磁區。
  3. write sector: 0x30 ,即寫磁區。

  我們來用圖簡單總結一下:

  不管是讀硬碟,還是寫硬碟,都不是一個指令就完事的。我們先理順一個步驟:

  1. 先選擇通道,往該通道的Sector count暫存器中寫入待操作的磁區數。
  2. 往該通道上的三個LBA暫存器寫入磁區起始地址的低24位。
  3. Device暫存器中寫入LBA地址的24-27位,並置第6位置為1,使其為LBA模式,設定第4位元,選擇操作的硬碟(master硬碟或slave硬碟)。
  4. 往該通道上的Command暫存器寫入操作命令。
  5. 讀取該通道上的Status暫存器,判斷硬碟工作是否完成。
  6. 如果以上步驟是讀硬碟,進入下一個步驟。否則,結束。
  7. 將硬碟資料讀出。

  硬碟工作完成後,它己經準備好了資料,咱們該怎麼獲取呢?一般常用的資料傳送方式如下:

  1. 無條件傳送方式
  2. 查詢傳送方式
  3. 中斷傳送方式
  4. 直接記憶體存取方式DMA
  5. IO 處理機傳送方式

  這些傳送方式我就不細說了,這不是我們的重點。感興趣可以翻閱《作業系統真相還原》的第139頁,或者其他資料。第1種方法不能用,因為硬碟需要在某種條件下才能傳輸。第4種和第5種需要單獨的硬體支援。所以我們實現會使用較為簡單的第2種和第3種。
  在之後的章節,弄好保護模式和分頁的基礎,我們會使用所有已學知識,學習Linux 1.1核心原始碼,並仿照逐步完善寫一個十分簡單的核心。

真真實模式雜談

  弄了這麼多,我們需要複習一下真真實模式相關的知識,因為這幾篇之後,我們就要搞保護模式,會花費大量的篇幅介紹基礎知識。
  在真真實模式下CPU存取資料將按照基址 + 偏移來進行。至於分類有暫存器定址、直接定址、記憶體定址。
  在該模式下,使用者程式和作業系統可以說是同一特權的程式,因為真真實模式下沒有特權級,它處處和作業系統平起平坐,所以可以執行一些具有破壞性的指令。程式可以隨意修改自己的段基址,這樣便在記憶體空間內不受阻攔,可以隨意存取任意實體記憶體,包括存取作業系統所在的記憶體資料,完全沒有保護性可言。使用者程式甚至可以覆蓋作業系統在記憶體中的映像,整個計算機世界的和平全靠程式設計師的心情。

練習與思考

  俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做成功,就不要看下一節教學了。

  1. 獨立完成本篇實驗,併成功在Bochs中列印出紅色的Loading system...!字串。

下一篇

  羽夏看Linux核心——段相關入門知識