此係列是本人一個字一個字碼出來的,包括範例和實驗截圖。如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
之前我們搭建好了Bochs
學習環境(沒搭好的回去弄好再回來看),可惜沒有合法的啟動盤,那麼什麼是啟動盤,如何正確的啟動,下面我們來開始介紹基礎部分。
BIOS
全稱叫Base Input & Output System
,即基本輸入輸出系統。它的主要工作是檢測、初始化硬體。
Intel 8086
有20條地址線,故其可以存取1MB
的記憶體空間。若按十六進位制來表示,是0x00000 - 0xFFFFF
。這lMB
的記憶體空間被分成多個部分。如下表格所示:
記憶體地址0x00000 - 0x9FFFF
的空間範圍是64KB
,這片地址對應到了動態隨機存取記憶體DRAM
,也就是插在
上的記憶體條;而0xF0000 - 0xFFFFF
這64KB
記憶體是ROM
,是唯讀的,存的就是BIOS
的程式碼。硬體自己提供了一些初始化的功能呼叫,BIOS
可以直接呼叫,並建立了中斷向量表,就可以通過int 中斷號
來實現相關的硬體呼叫。而這些中斷只有重要的、保證計算機能執行的那些硬體的基本IO
操作,不像高階語言有各種花裡胡哨的功能。
我們還要說明一個問題:在CPU
眼裡,我們插在主機板上的實體記憶體不是它眼裡「全部的記憶體」。這個是由地址匯流排寬度決定了可以存取的記憶體空間大小。打個比方,比如小孩學數蘋果數目。結果他只會100以內的,如果蘋果數目超了100,就不會數了,不認識了。實體記憶體也是如此,再多的記憶體,只要識別能力不夠,也是浪費。
BIOS
是計算機上第一個執行的軟體,所以它不可能自己載入自己,由此可以知道,它是由硬體載入的。BIOS
程式碼所做的工作也是一成不變的,而且在正常情況下,其本身一般是不需要修改的,儲存在ROM
中。ROM
也是塊記憶體,記憶體就需要被存取。而ROM
被對映在0xF0000 - 0xFFFFF
處,只要存取此處的地址便是存取了BIOS
,這個對映是由硬體完成的。如果不太理解,可以學一下微控制器的基礎知識和電工學下冊。
BIOS
本身是個程式,程式要執行,就要有個入口地址才行,此入口地址便是0xFFFF0
。
當我們開始啟動虛擬機器器進入偵錯狀態時,你會看到如下內容:
可以看到,ip
指向的地址指令是jmp far f000:e05b
,這個是跨段跳轉,最後執行結果是到了0xFE05B
這個地址,這個是真正BIOS
程式碼開始的地方。
接下來BIOS
便馬不停蹄地檢測記憶體、顯示卡等外設資訊,當檢測通過,並初始化好硬體後,開始在記憶體中0x000-Ox3FF
處建中斷向量表IVT
並填寫中斷例程。然後它的任務完成了,剩下的部分就是交給下一個「負責人」繼續處理。
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
。
因為近啊。就好比你會把經常用的放到身邊,用到就會直接拿出來,如果把它放到老遠的位置,這個不就費勁了嗎。對於BIOS
來說,MBR
就是經常用的東西,放到身邊才方便。
據說是歷史原因,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
是獨立於作業系統的,能夠直接在裸機上執行。它的大小必須是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
。不過我們得把這幾個命令列引數介紹一下。
as86
是生成16位元組合程式碼,如果用了超過8086指令集發出警告。
ld86
是生成16位元檔案頭,這個我們不要,需要刪除。
啟用與Minix asld
的部分相容性,看不懂可以不管。
刪除檔案頭。
輸出檔名/路徑。
得到boot.bin
之後,我們需要將這個資料寫入虛擬映象test.img
當中,我們需要輸入以下命令:
dd if=boot.bin of=test.img bs=512 count=1 conv=notrunc
dd
是用於磁碟操作的命令,可以深入磁碟的任何一個磁區。如果要了解詳情,請在終端輸入man dd
。這裡我們僅僅介紹我們使用的引數。
指定要讀取的檔案。
指定把資料輸出到哪個檔案。
指定塊的大小,dd
是以塊為單位來進行IO
操作的,得指明塊是多大位元組。
指定拷貝的塊數。
指定如何轉換檔案。建議在追加資料時,conv
最好用notrunc
方式,也就是不打斷檔案。
執行完這些操作後,我們雙擊我們的startLearning.sh
看看結果:
這就說明成功了。
以後我們如果頻繁更改編譯,每次輸入這幾個指令是不是太麻煩了?我們可以寫一個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
。此暫存器用來儲存讓硬碟執行的命令,只要把命令寫進此暫存器,硬碟就開始工作了。在咱們的系統中,主要使用了三個命令。
我們來用圖簡單總結一下:
不管是讀硬碟,還是寫硬碟,都不是一個指令就完事的。我們先理順一個步驟:
Sector count
暫存器中寫入待操作的磁區數。LBA
暫存器寫入磁區起始地址的低24
位。Device
暫存器中寫入LBA
地址的24-27
位,並置第6位置為1,使其為LBA
模式,設定第4位元,選擇操作的硬碟(master
硬碟或slave
硬碟)。Command
暫存器寫入操作命令。Status
暫存器,判斷硬碟工作是否完成。硬碟工作完成後,它己經準備好了資料,咱們該怎麼獲取呢?一般常用的資料傳送方式如下:
DMA
這些傳送方式我就不細說了,這不是我們的重點。感興趣可以翻閱《作業系統真相還原》的第139頁,或者其他資料。第1種方法不能用,因為硬碟需要在某種條件下才能傳輸。第4種和第5種需要單獨的硬體支援。所以我們實現會使用較為簡單的第2種和第3種。
在之後的章節,弄好保護模式和分頁的基礎,我們會使用所有已學知識,學習Linux 1.1
核心原始碼,並仿照逐步完善寫一個十分簡單的核心。
弄了這麼多,我們需要複習一下真真實模式相關的知識,因為這幾篇之後,我們就要搞保護模式,會花費大量的篇幅介紹基礎知識。
在真真實模式下CPU
存取資料將按照基址 + 偏移
來進行。至於分類有暫存器定址、直接定址、記憶體定址。
在該模式下,使用者程式和作業系統可以說是同一特權的程式,因為真真實模式下沒有特權級,它處處和作業系統平起平坐,所以可以執行一些具有破壞性的指令。程式可以隨意修改自己的段基址,這樣便在記憶體空間內不受阻攔,可以隨意存取任意實體記憶體,包括存取作業系統所在的記憶體資料,完全沒有保護性可言。使用者程式甚至可以覆蓋作業系統在記憶體中的映像,整個計算機世界的和平全靠程式設計師的心情。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做成功,就不要看下一節教學了。
Bochs
中列印出紅色的Loading system...!
字串。