大家好,我是呼嚕嚕,在上一篇文章聊聊x86電腦啟動發生的事?我們瞭解了x86電腦啟動過程,MBR、0x7c00是什麼?其中當bios引導結束後,操作系統接過電腦的控制權後,發生了哪些事?本文將揭開迷霧的序章-Bootsect.S 回顧電腦啟動過程 我們先來回顧一下,上古時期電腦按下電源鍵的 ...
大家好,我是呼嚕嚕,在上一篇文章聊聊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源碼圖解