大家好,我是呼嚕嚕,在上一篇文章聊聊x86計算機啟動發生的事?我們瞭解了x86計算機啟動過程,MBR、0x7c00是什麼?其中當bios引導結束後,作業系統接過計算機的控制權後,發生了哪些事?本文將揭開迷霧的序章-Bootsect.S
回顧計算機啟動過程
我們先來回顧一下,上古時期計算機按下電源鍵的啟動過程,這裡以8086架構為例:
8086、80x86是什麼意思?
有許多人不知道 經常遇到的8086、80x86是什麼意思?我們簡單科普一下:
- 8086是Intel公司推出的最早,也是最流行的面向個人電腦的CPU型號
- x86泛指一系列基於Intel 8086且向後相容的中央處理器指令集架構,由於以“86”作為結尾,因此其架構被稱為"x86"
- 80x86也就是在8086基礎上的增強版,包括80286,80386,80486,其後面就是我們所熟悉的奔騰、酷睿、i5、i7等等
暫存器初始化CS:IP
相比於上一篇文章聊聊x86計算機啟動發生的事,我們這裡再講細緻點,當計算機一按下電源後,8086CPU就處於真實模式的狀態,此時會將CPU的暫存器初始化為CS=0xFFFF;IP=0x0000
,也就是實際實體地址0xFFFF0
(CS左移4位+IP)
CS : 程式碼段暫存器;IP : 指令指標暫存器。CS:IP指向的內容 會被CPU當做計算機指令去執行
那麼從地址0xFFFF0
中取出來的指令是什麼?我們知道當電路通電後,記憶體是一片空白的,記憶體斷電後 資料是無法儲存的,所以BIOS程式需要事先被刷入只讀儲存器ROM中。實體地址0xFFFF0
就是指向這樣一段BIOS ROM
CPU是如何和ROM相連的?
那麼問題又來了,CPU是如何和ROM相連的?CPU 不僅和ROM相連,還和RAM(俗稱記憶體),IO介面等裝置相連,他們是透過匯流排相連。還好當時筆者將計算機組成原理好好複習了一遍,不然這部分真挺難理解的。
匯流排是貫穿整個系統的是一組電子管道,是連線各個部件的資訊傳輸線,是各個部件共享的傳輸介質,稱作匯流排,它攜帶資訊位元組並負責在各個計算機部件間傳遞。
匯流排按系統匯流排傳輸資訊內容的不同,又可以分為3 種:資料匯流排、地址匯流排和控制匯流排。我們這裡用到的就是地址匯流排,把 0xFFFF0 作為 CPU 的地址匯流排訊號傳輸出去,去這個地址匯流排對應的位置處找
由於計算機有多個裝置,必然會存在多個裝置同時競爭匯流排控制權的問題,這時候就需要匯流排仲裁,讓某個裝置優先獲得匯流排控制權,獲得了匯流排控制權的裝置,才能開始傳送資料。未獲勝的裝置只能等待獲勝的裝置處理完成後才能執行。
我們簡單總結一下:當匯流排仲裁器仲裁透過後,CPU可以依靠地址匯流排定址,找到對應裝置ROM上地址0xFFFF0
處的內容。
擴充可見:什麼是計算機中的高速公路-匯流排?
載入MBR到記憶體中
當BIOS自檢完成,設定啟動順序後,利用 BIOS 的輸入功能將啟動磁碟的啟動扇區MBR(也叫第一扇區,主開機記錄)的內容原封不動地搬到記憶體的0x7C00
地址處,並設定CPU暫存器CS=0x07C0,IP=0x0000
。到這一步,計算機的控制權將交到作業系統手中!
為什麼是0x7C00這個地址?如何得出?別再問了,本文不再解釋了,具體看筆者的上一篇文章聊聊x86計算機啟動發生的事
對於Linux0.12來說,第一個程式Bootsect.S 編譯成二進位制後,需要事先放到主開機記錄MBR中,MBR大小就是一個扇區的大小512位元組,如果這512位元組的最後兩個位元組是0x55AA
,表明這個裝置可以用於啟動。只有這樣我們BIOS才能識別它,才能把bootsect.S載入到記憶體中。
如果不是0x55和0xAA,表明裝置不能用於啟動,控制權於是被轉交給"啟動順序"中的下一個裝置。如果到最後還是沒找到符合條件的,直接報出一個無啟動區的error。
下面我們看下作業系統編譯後,存放在儲存裝置(硬碟)的模組分佈:
先簡單介紹一下,不必深究,後續文章會娓娓道來:
- bootsect.s的主要作用就是載入作業系統,把作業系統從硬碟中,載入到記憶體裡去
- setup.s的主要作用:首先獲得游標,記憶體,顯示卡,磁碟等硬體引數存放在記憶體空間中,方便後續程式使用;臨時建立gdt、idt表,並且從真實模式進入到了保護模式
- 在linux0.12原始碼,boot目錄下還有一個head.s,在上圖中被歸於system模組,屬於作業系統主體檔案,主要是進行進入保護模式之後的初始化工作
- system模組:就是作業系統的主體,比如檔案系統,IO,程序等模組。 Linux0.12 核心 system 模組大約佔隨後的 260 個扇區。
更多精彩文章在公眾號「小牛呼嚕嚕」
bootsect.S具體幹了什麼?
bootsect的主要作用就是載入作業系統,把作業系統從硬碟中,載入到記憶體裡去,我們下面結合bootsect.s的原始碼一起來看看bootsect.S具體幹了什麼?
呼嚕嚕這裡整個過程先匯成了圖,大家配合圖去閱讀下文,對照起來,更容易理解
設定段基址 & 記憶體分段機制
要想bootsect啟動,需要讓BIOS將bootsect.s 從硬碟的MBR中搬到 記憶體位置0x7c00
處,大小512個位元組。當bootsect被BIOS載入到記憶體後,計算機的控制權就到作業系統bootsect的手上了。
entry start ! 告知連結程式,程式入口是從start 標號開始執行的
start:
mov ax,#BOOTSEG !BOOTSEG=0x7c0 , 將 ds 段暫存器置為 0x7C0
mov ds,ax !再將 ax 段暫存器裡的值複製到 ds 段暫存器裡
mov ax,#INITSEG !SETUPSEG=0x9000,將 es 段暫存器置為 0x9000
mov es,ax !再將 ax 段暫存器裡的值複製到 es 段暫存器裡
mov cx,#256
sub si,si
sub di,di
rep
movw
jmpi go,INITSEG
我們可以看到CPU實際執行第一句的程式碼 mov ax,#BOOTSEG !BOOTSEG=0x7c0
,這是彙編寫的,其實這裡的0x7c0
對應的就是我們上文的地址0x7C00
0x7c0
是段地址,0x7C00
是其實際的實體地址,0x7c0
左移四位就是0x7c00
,這就是記憶體定址-分段機制
那麼大家一定會有疑問記憶體為什麼分段?
計算機記憶體究竟是什麼?其實它就像陣列一樣,咦有人不懂陣列是什麼,那麼我們可以再頭腦風暴一下,記憶體其實就像紙帶一樣,我們來看下上古時期的計算機:
穿孔紙帶,圖片來源於網路
紙帶上有一個個孔,這樣大家可能還看不明白,我們再來看一張圖:
這些孔排列組合其實就是二進位制數,紙帶其實就是儲存資料的介質,那麼記憶體就是足夠長的“紙帶”
在現代計算機中,記憶體它使用的是DRAM晶片,也叫動態隨機存取儲存器,即只需給出地址,就能直接訪問指定地址的資料,這一點特別像陣列,所以許多材料都是用陣列來畫記憶體圖
那麼CPU訪問記憶體明明可以直接透過地址訪問記憶體,為什麼還要分段?其實這又是一個歷史因素導致的,讓我們回到"分段"首次出現的時候:"分段"是從Intel 8086晶片開始的,8086又是你......
由於8086那個時代CPU、記憶體都很昂貴, CPU 和暫存器等寬度都是 16 位的,其可定址2的16次方位元組,也就是64kb,然而8086有20根地址線,可定址的最大記憶體空間是1MB。CPU和暫存器的定址能力遠遠不能滿足使用,於是機智的祖師爺們,採用了分段技術
分段,為解決這個問題,8086引入段暫存器,如CS、DS、ES、SS
。透過段基址+段內偏移地址的方式生成20位的地址,擴大定址能力,從而實現對1MB記憶體空間的定址。由於這樣程式中指令了只用到16位地址,縮短了指令長度,也變相地提高了程式執行速度。
- CS:程式碼段暫存器,存放程式碼段的段基址
- DS是資料段暫存器,存放資料段的段基址
- ES是擴充套件段暫存器,存放當前程式使用附加資料段的段基址,該段是串操作指令中目的串所在的段
- SS是堆疊段暫存器,存放堆疊段的段基址
- 80836還新增2個暫存器,FS標誌段暫存器、GS全域性段暫存器。
使用段地址還有一個好處是 程式可以重定位,那個時候的計算機可沒有虛擬地址之說,只有實體地址,訪問任何儲存單元都直接給出實體地址。這就帶來一個問題: 如果此時計算機多道程式併發執行,程式中的地址都是實際實體地址,這些程式編譯出來的程式執行地址是相同的,計算機只能執行一個程式。
重定向: 將程式中指令的地址改成另一個地址,但該地址處的內容還是原記憶體地址處的內容。這樣程式指令雖然還是實體地址,但程式能夠併發執行了。
1982年處理器80286,首次提出保護模式概念,為了保持相容性,所以同樣支援記憶體分段管理,將8086這種稱為真實模式,最大的區別是實體記憶體地址不能直接被程式訪問,這塊非常重要,篇幅也較長,筆者先挖坑,後續系列文章再單獨出一篇。
咳咳,擴充的有點多了,趕緊讓我們回到bootsect原始碼處
mov ds,ax
這句話程式碼的意思就是:將 ax 段暫存器裡的值複製到 ds 段暫存器裡。ds在上文我們提到,8086特地為採用記憶體分段機制,引入的段暫存器。ds具體表示 資料段暫存器,存放資料段的段基址
換句話說,就是將段基址設為0x07c0
,那麼後續資料段程式中只需寫段內偏移地址,就能訪問實際實體地址了。比如後續程式中出現mov ax,0x01
,0x01
其實是[ds:0x01]
,那麼ax的實際實體地址= 0x07c0 <<4 + 0x01
。將ds暫存器段基址設定好後,其實就是方便之後程式訪問記憶體,訪問的資料的記憶體地址都先預設加上 0x7c00
,然後再去記憶體中定址。
如果實際程式設計時,程式碼段的起始地址一般放到 CS暫存器,雖然CPU沒有強制規定程式碼段、資料段等分離。
mov ax,#INITSEG
,mov es,ax
將 ax 段暫存器裡的值0x9000
複製到 es 段暫存器裡,和ds賦值同理,不再贅述。需要注意的是8086無法直接給段暫存器進行賦值,需要使用通用暫存器來當中介(一般使用ax)
bootsect的"再次搬家"到0x90000
接著bootsect自己把自己從記憶體位置0x7c00
處,搬到0x90000
處,這次可沒BIOS幫忙了,得自食其力
start:
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256 ! 設定移動計數值=256 字(512 位元組);
sub si,si ! si暫存器 清零
sub di,di ! di暫存器 清零
rep ! 重複執行並遞減 cx 的值,直到 cx = 0 為止。
movw ! 即 movs 指令。從記憶體[si]處移動 cx 個字到[di]處。//一次移動兩個位元組,256B*2=512B
mov cx,#256
將cx 暫存器的值賦值為 256,單位是字(Word), 1 word=2Byte
sub si,si
是si暫存器 清零操作,sub
是組合語言中的一種運算指令,它用來執行減法運算,並將結果儲存到被減數(前者)上去。比如sub a,b
就是a = a-b
。再結合前面的ds,es,那麼此時si的段地址ds:si = 0x07C0:0x0000
,同理di的段地址es:di = 0x9000:0x0000
rep
就是重複執行後一條指令,movw
就是複製的意思。rep movw
就是重複多次搬運
我們可以知道這段的總體意思就是:迴圈256次,反覆將段地址0x07C0:0x0000
的內容一個字一個字的複製到段地址0x9000:0x0000
處,直到暫存器cx為0。這樣就實現了bootsect的"自我搬運",把實際實體記憶體地址0x7c00處
512個位元組的內容全部複製到實際實體記憶體地址0x90000處
。
那為啥bootsect還要"多此一舉" 將自己從0x7c00
,搬到0x90000
處?
- 作業系統system後續最終是要從實體記憶體起始位置處
地址0
開始存放,好處是讓system程式碼中的地址對應上實際的實體地址。- 一般要留
512KB
的記憶體空間放作業系統system,會覆蓋0x7c00地址的內容,所以需要把bootsect程式碼搬到記憶體更高處。
載入setup.s到記憶體0x90200
當上面bootsect完成自我搬運後,緊接著執行jmpi go,INITSEG
,jmpi有段間跳轉的作用。這裡 INITSEG 指出跳轉到的段地址0x9000
,標號 go 是段內偏移地址。
其實就是執行完jmpi go,INITSEG
後,CPU已經移動到記憶體0x90000+go
位置處的程式碼中 執行。為啥要加go?其實此時bootsect編譯後的二進位制內容,已經搬運到記憶體0x90000
處,但是我們不能再從頭執行start: mov ax,#BOOTSEG
操作,而是從go: mov ax,cs
處程式碼繼續執行下去。
jmpi go,INITSEG ! 段間跳轉。這裡 INITSEG 指出跳轉到的段地址,標號 go 是段內偏移地址。
go: mov ax,cs
mov dx,#0xfef4 ! arbitrary value >>512 - disk parm size
mov ds,ax
mov es,ax
push ax ! 臨時儲存段值(0x9000)
mov ss,ax ! put stack at 0x9ff00 - 12.
mov sp,dx
push #0 ! 置段暫存器 fs = 0。
pop fs ! fs:bx 指向存有軟碟機參數列地址處(指標的指標)
mov bx,#0x78 ! fs:bx is parameter table address
seg fs
lgs si,(bx) ! gs:si is source
mov di,dx ! es:di is destination
mov cx,#6 ! copy 12 bytes
cld
rep ! 複製 12 位元組的軟碟機參數列到 0x9000:0xfef4 處。
seg gs
movw
mov di,dx
movb 4(di),*18 ! patch sector count
seg fs ! 讓中斷向量 0x1E 的值指向新表。
mov (bx),di
seg fs
mov 2(bx),es
pop ax
mov fs,ax
mov gs,ax
xor ah,ah ! reset FDC 讓中斷向量 0x1E 的值指向新表。
xor dl,dl
int 0x13
上述主要是將 暫存器DS、ES 和SS 重新設定為CPU移動後,程式碼所在的段處0×9000
,設定SP棧暫存器0xfef4
棧指標要遠大於512位元組偏移(即 0x90200 )處都可以,一般setup程式大概佔用4個扇區,這樣棧頂段地址ss:sp
和現有的程式碼足夠遠 ,防止後續棧操作覆蓋掉已有的程式碼。
還有BIOS 設定的中斷 0x1e 的中斷向量值等操作。這邊和主幹操作不太相干,簡略過一下,主要就是把這些暫存器重新設定好值,方便後續使用。
更多精彩文章在公眾號「小牛呼嚕嚕」
接下來緊接著將setup.s 載入到記憶體0x90200
處
load_setup:
xor dx, dx ! 驅動器drive 0, 磁頭head 0
mov cx,#0x0002 ! 扇區sector 2, 磁軌號track 0,從第二個扇區開始讀
mov bx,#0x0200 ! 偏移address = 512, in INITSEG ,表示讀到0x90200
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors ,SETUPLEN是 4個扇區
int 0x13 ! read it
jnc ok_load_setup ! ok,就跳到ok_load_setup
push ax ! dump error code
call print_nl ! 螢幕游標回車
mov bp, sp
call print_hex ! 顯示十六進位制值
pop ax
xor dl, dl ! reset FDC
xor ah, ah
int 0x13
j load_setup ! j 即 jmp 指令,失敗就再跳轉到load_setup,重複執行
那怎麼簡單高效將磁碟裡的內容載入到記憶體中呢?linus這裡用的是bios的中斷程式,因為此時bios還在記憶體中,可以為我們所用,0x13
號中斷 在BIOS中是可以訪問軟盤、IDE、ROM、遠端磁碟服務的作用。
這裡0x13 和C語言中的函式呼叫是很像的,不過需要注意的是它的引數只能透過暫存器去傳參,而C語言函式呼叫不僅可以暫存器傳參,還可以棧傳參。所以0x13的引數就是其前面的dx,cx,bx,ax暫存器的值,另外磁碟只認磁頭磁軌扇區,如果給個地址,磁碟是不識別的,磁碟一副不太聰明的樣子。
另外xor
對兩個運算元進行邏輯(按位)異或操作,並將結果存放在目標運算元,xor dx,dx
也是一個置零操作,指定驅動和磁頭
那麼我們連起來,這段主要是讓bios 0x13號中斷處理程式 從磁碟的第2扇區開始讀,接連讀4個扇區的內容到記憶體0x90200
處中。成功就跳轉到ok_load_setup
,沒成功就回到load_setup
,重複執行上述操作。
載入system到記憶體0x10000
當bootsect成功將setup.s搬到記憶體0x90200
處後,CPU從ok_load_setup
處繼續執行指令。接下來就是需要將整個作業系統system(head.s+其他檔案,大約260個扇區)的內容載入到記憶體0x10000
處,下面我們就具體看下程式碼是如何實現的:
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
!提示這面段程式碼功能是:利用BIOSINT 0x13 中斷,來來取磁碟的一些引數,比如是取每磁軌扇區數,並儲存在
位置 sectors 處
xor dl,dl
mov ah,#0x08 ! AH=8 is get drive parameters
int 0x13
xor ch,ch
seg cs !表示下一條語句的運算元在 cs 段暫存器所指的段中。它隻影響其下一條語句
mov sectors,cx
mov ax,#INITSEG
mov es,ax !取磁碟引數中斷改了es暫存器的值,這裡重置es的值
! Print some inane message 提示下面這段功能是:列印一些訊息
mov ah,#0x03 ! read cursor pos 讀取當前游標的地址
xor bh,bh
int 0x10 ! bios 0x10中斷,其作用:在螢幕上顯示字元和字串
mov cx,#9
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 ! msg1的內容是: .byte 13,10(換行+回車) .ascii "Loading"
mov ax,#0x1301 ! write string, move cursor
int 0x10
! ok, we've written the message, now
! we want to load the system (at 0x10000) 載入system到記憶體0x10000
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it ! 讀磁碟上 system 模組
call kill_motor ! 關閉驅動器馬達
call print_nl ! 游標回車換行
... 省略非主幹程式碼...
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG !bootsect程式到這裡就結束了,跳轉到0x9020,同時setup獲得控制權
這裡int 0x10
號中斷,其作用是 在螢幕上顯示字元和字串,由於作業系統比較大,載入需要時間,這時在螢幕上顯示提示資訊"Loading"
這裡將作業系統載入到記憶體中,是透過子程式read_it
來實現的,read_it就不具體展開了,比較複雜。我們需要知道由於作業系統比較大,一個磁軌是遠遠放不下的,另外磁碟是不認地址的,在搬運過程中,需要進行磁軌、扇區和磁頭的計算,特別是一個段的大小是64k,如果放不下,需要更換段地址。如果不更換段地址,會從該段地址0位元組開始重新寫,這樣會覆蓋之前的內容。
那為什麼一個段的大小是64KB呢?
我們知道在8086CPU中,其記憶體地址是表示為段基址+段內偏移地址,其中偏移地址使用一個16位的二進位制數表示,表示範圍0000~FFFF
,所以總共有2^16(2的16次方)=64K個不同的地址,一個記憶體最小單元是位元組Byte,所以一個段大小為64KB
jmpi 0,SETUPSEG
,bootsect程式到這裡就結束了,跳轉到記憶體地址0x90200
,同時setup獲得控制權
為了幫助大家理解,呼嚕嚕這裡又把本篇文章全部串起來,大家可以根據下面這張圖重新回顧一下bootsect整個工作流程:
額外補充一下:
boot_flag: .word 0xAA55
最後2個位元組是0xAA55
,由於bootsect是採用AT&T彙編,小端顯示的,實際上就是0x55AA
與前文MBR那邊前後呼應
這也說明了作業系統在開始載入到記憶體的程式中,得與記憶體地址一一對應, 不能多一個位元組,也不能少一個位元組!!!
尾語
本文主要講解了bootsect.S的主要工作流程,Linux0.12雖然和如今的Linux6.x核心相比顯得過於簡陋,但麻雀雖小五臟俱全,它是我們開啟作業系統大門的鑰匙,後面讓我們看看setup.s獲得計算機的控制權後,會發生什麼?
最近實在太忙了,後面隨緣更新,留言可催更(bushi)~~
參考資料:
《Linux核心完全註釋5.0》
《作業系統真象還原》
https://elixir.bootlin.com/linux/0.12/source/boot/bootsect.S
https://files.embeddedts.com//old/saved-downloads-manuals/EBIOS-UM.PDF
本篇文章到這裡就結束啦,如果我的文章對你有所幫助的話,還請點個免費的贊,你的支援會激勵我輸出更高質量的文章,感謝!
作者:小牛呼嚕嚕 ,首發於公眾號 小牛呼嚕嚕,系列文章還有:
- 聊聊x86計算機啟動發生的事?
- Linux0.12核心原始碼解讀(2)-Bootsect.S
- Linux0.12核心原始碼解讀(3)-Setup.S
- 圖解CPU的真實模式與保護模式
- Linux0.12核心原始碼解讀(7)-陷阱門初始化
- 圖解計算機中斷
- 什麼是系統呼叫機制?結合Linux0.12原始碼圖解