Linux0.12核心原始碼解讀(2)-Bootsect.S

小牛呼噜噜發表於2024-04-12

大家好,我是呼嚕嚕,在上一篇文章聊聊x86計算機啟動發生的事?我們瞭解了x86計算機啟動過程,MBR、0x7c00是什麼?其中當bios引導結束後,作業系統接過計算機的控制權後,發生了哪些事?本文將揭開迷霧的序章-Bootsect.S

回顧計算機啟動過程

我們先來回顧一下,上古時期計算機按下電源鍵的啟動過程,這裡以8086架構為例:

8086、80x86是什麼意思?

有許多人不知道 經常遇到的8086、80x86是什麼意思?我們簡單科普一下:

  1. 8086是Intel公司推出的最早,也是最流行的面向個人電腦的CPU型號
  2. x86泛指一系列基於Intel 8086且向後相容的中央處理器指令集架構,由於以“86”作為結尾,因此其架構被稱為"x86"
  3. 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。

下面我們看下作業系統編譯後,存放在儲存裝置(硬碟)的模組分佈:


先簡單介紹一下,不必深究,後續文章會娓娓道來:

  1. bootsect.s的主要作用就是載入作業系統,把作業系統從硬碟中,載入到記憶體裡去
  2. setup.s的主要作用:首先獲得游標,記憶體,顯示卡,磁碟等硬體引數存放在記憶體空間中,方便後續程式使用;臨時建立gdt、idt表,並且從真實模式進入到了保護模式
  3. 在linux0.12原始碼,boot目錄下還有一個head.s,在上圖中被歸於system模組,屬於作業系統主體檔案,主要是進行進入保護模式之後的初始化工作
  4. 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,0x010x01其實是[ds:0x01],那麼ax的實際實體地址= 0x07c0 <<4 + 0x01。將ds暫存器段基址設定好後,其實就是方便之後程式訪問記憶體,訪問的資料的記憶體地址都先預設加上 0x7c00,然後再去記憶體中定址。

如果實際程式設計時,程式碼段的起始地址一般放到 CS暫存器,雖然CPU沒有強制規定程式碼段、資料段等分離。

mov ax,#INITSEGmov 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


本篇文章到這裡就結束啦,如果我的文章對你有所幫助的話,還請點個免費的,你的支援會激勵我輸出更高質量的文章,感謝!


作者:小牛呼嚕嚕 ,首發於公眾號 小牛呼嚕嚕,系列文章還有:

  1. 聊聊x86計算機啟動發生的事?
  2. Linux0.12核心原始碼解讀(2)-Bootsect.S
  3. Linux0.12核心原始碼解讀(3)-Setup.S
  4. 圖解CPU的真實模式與保護模式
  5. Linux0.12核心原始碼解讀(7)-陷阱門初始化
  6. 圖解計算機中斷
  7. 什麼是系統呼叫機制?結合Linux0.12原始碼圖解

相關文章