羽夏看Linux內核——啟動那些事
- 2022 年 8 月 7 日
- 筆記
- Linux 系統內核, 羽夏看Linux內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我。
前言
之前我們搭建好了Bochs
學習環境(沒搭好的回去弄好再回來看),可惜沒有合法的啟動盤,那麼什麼是啟動盤,如何正確的啟動,下面我們來開始介紹基礎部分。
BIOS
BIOS
全稱叫Base Input & Output System
,即基本輸入輸出系統。它的主要工作是檢測、初始化硬體。
實模式下的 1MB 記憶體布局
Intel 8086
有20條地址線,故其可以訪問1MB
的記憶體空間。若按十六進位來表示,是0x00000 - 0xFFFFF
。這lMB
的記憶體空間被分成多個部分。如下表格所示:
記憶體地址0x00000 - 0x9FFFF
的空間範圍是64KB
,這片地址對應到了動態隨機訪問記憶體DRAM
,也就是插在
上的記憶體條;而0xF0000 - 0xFFFFF
這64KB
記憶體是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開始的。
如果此扇區末尾的兩個位元組分別是魔數0x55
和OxAA
,BIOS
便認為此扇區中確實存在可執行的程式,此程式便是主引導記錄MBR
,它會被載入到物理地址0x7c00
,隨後跳轉到此地址,繼續執行。反之,它就不認。
BIOS
跳轉到Ox7c00
是用jmp 0:Ox7c00
實現的,此時段暫存器cs
會被替換成0
。
為什麼 MBR 住在這裡
因為近啊。就好比你會把經常用的放到身邊,用到就會直接拿出來,如果把它放到老遠的位置,這個不就費勁了嗎。對於BIOS
來說,MBR
就是經常用的東西,放到身邊才方便。
為什麼是 0x7c00 地址
據說是歷史原因,BIOS
規範。它最早出現在IBM
公司出產的個人電PC5150 ROM BIOS
的INT 19H
中斷處理程式中。
MBR
不是隨便放在哪裡都行的,首先不能覆蓋己有的數據,其次,不能過早地被其他數據覆蓋。通常MBR
的任務是載入某個程式(這個程式一般是內核載入器,很少有直接載入內核的)到指定位置,並將控制權交給它。
按DOS 1.0
要求的最小記憶體32KB
來說,MBR
希望給人家儘可能多的預留空間,這樣也是保全自己的作法,免得過早被覆蓋,所以MBR
只能放在32KB
的末尾。其次,MBR
本身也是程式,是程式就要用到棧,棧也是在記憶體中的,雖然本身只有512
位元組,但還要為其所用的棧分配點空間,所以其實際所用的記憶體空間要大於512
位元組,估計1KB
記憶體夠用了。
綜上,選擇32KB
中的最後1KB
最為合適,那此地址是多少呢?32KB
換算為十六進位為0x8000
,減去1KB
(0x400)的話,等於0x7c00
。
MBR 雜談
MBR
是獨立於作業系統的,能夠直接在裸機上運行。它的大小必須是512
位元組,保證0x55
和0xAA
這兩個
魔數恰好出現在該扇區的最後兩個位元組處。下面我們來編寫一個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 registers
和Control Block registers
。Command Block registers
用於向硬碟驅動器寫入命令宇或者從硬碟控制器獲得硬碟狀態,Control Block registers
用於控制硬碟工作
狀態。在Control Block registers
組中的暫存器已經精減了,而且咱們基本上用不到,就不贅述了,下面重點介紹Command Block registers
組中的暫存器。
埠是按照通道給出的,也就是說,埠不是直接針對某塊硬碟的。一個通道上的主、從兩塊硬碟都用這些埠號,要想操作某通道上的某塊硬碟,需要單獨指定。
Data
暫存器在名字上我們就知道它是負責管理數據的,它相當於數據的門,數據能進,也能出,所以其作用是讀取或寫入數據。這個暫存器是16位的,得到了特殊照顧。在讀硬碟時,硬碟準備好的數據後,硬碟控制器將其放在內部的緩衝區中,不斷讀此暫存器便是讀出緩衝區中的全部數據。在寫硬碟時,我們要把數據源源不斷地輸送到此埠,數據便被存入緩衝區里,硬碟控制器發現這個緩衝區中有數據了,便將此處的數據寫入相應的扇區中。
讀硬碟時,埠0x171
和0x1F1
的暫存器名字叫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 low
、LBA mid
、LBA 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
。
在讀硬碟時,埠0x1F7
和0x177
的暫存器名稱是Status
,它是8位寬度的暫存器,用來給出硬碟的狀態資訊。索引0位是ERR
位,如果此位為1,表示命令出錯了,具體原因可見Error
暫存器。索引3位是Request
位,如果此位為1,表示硬碟己經把數據準備好了,主機現在可以把數據讀出來。索引6位是 DRDY
,表示硬碟就緒,此位是在對硬碟診斷時用的,表示硬碟檢測正常,可以繼續執行一些命令。索引7位是BSY
位,表示硬碟是否繁忙,如果為1表示硬碟正忙著,此暫存器中的其他位都無效。剩餘的幾位用不到暫且不關注。
在寫硬碟時,埠0x1F7
和0x177
的暫存器名稱是Command
。此暫存器用來存儲讓硬碟執行的命令,只要把命令寫進此暫存器,硬碟就開始工作了。在咱們的系統中,主要使用了三個命令。
- identify: 0xEC ,即硬碟識別。
- read sector: 0x20 ,即讀扇區。
- write sector: 0x30 ,即寫扇區。
我們來用圖簡單總結一下:
不管是讀硬碟,還是寫硬碟,都不是一個指令就完事的。我們先理順一個步驟:
- 先選擇通道,往該通道的
Sector count
暫存器中寫入待操作的扇區數。 - 往該通道上的三個
LBA
暫存器寫入扇區起始地址的低24
位。 - 往
Device
暫存器中寫入LBA
地址的24-27
位,並置第6位置為1,使其為LBA
模式,設置第4位,選擇操作的硬碟(master
硬碟或slave
硬碟)。 - 往該通道上的
Command
暫存器寫入操作命令。 - 讀取該通道上的
Status
暫存器,判斷硬碟工作是否完成。 - 如果以上步驟是讀硬碟,進入下一個步驟。否則,結束。
- 將硬碟數據讀出。
硬碟工作完成後,它己經準備好了數據,咱們該怎麼獲取呢?一般常用的數據傳送方式如下:
- 無條件傳送方式
- 查詢傳送方式
- 中斷傳送方式
- 直接存儲器存取方式
DMA
- IO 處理機傳送方式
這些傳送方式我就不細說了,這不是我們的重點。感興趣可以翻閱《作業系統真相還原》的第139頁,或者其他資料。第1種方法不能用,因為硬碟需要在某種條件下才能傳輸。第4種和第5種需要單獨的硬體支援。所以我們實現會使用較為簡單的第2種和第3種。
在之後的章節,弄好保護模式和分頁的基礎,我們會使用所有已學知識,學習Linux 1.1
內核源碼,並仿照逐步完善寫一個十分簡單的內核。
實模式雜談
弄了這麼多,我們需要複習一下實模式相關的知識,因為這幾篇之後,我們就要搞保護模式,會花費大量的篇幅介紹基礎知識。
在實模式下CPU
訪問數據將按照基址 + 偏移
來進行。至於分類有暫存器定址、直接定址、記憶體定址。
在該模式下,用戶程式和作業系統可以說是同一特權的程式,因為實模式下沒有特權級,它處處和作業系統平起平坐,所以可以執行一些具有破壞性的指令。程式可以隨意修改自己的段基址,這樣便在記憶體空間內不受阻攔,可以隨意訪問任意物理記憶體,包括訪問作業系統所在的記憶體數據,完全沒有保護性可言。用戶程式甚至可以覆蓋作業系統在記憶體中的映像,整個電腦世界的和平全靠程式設計師的心情。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做成功,就不要看下一節教程了。
練習與思考
- 獨立完成本篇實驗,並成功在
Bochs
中列印出紅色的Loading system...!
字元串。