本文的目的,在將Linux kernel的boot部份做一個介紹,因為筆者覺得很少有這樣的
文章來介紹一個操作系統最最開始的一步----把kernel本身載入至內存中,同時進行一些
機器相關(machine dependent)的初始化工作,由於linux剛好使用的是大家最熟悉的386,
486系列PC,所以在說明其程序流程時,也剛好可以對其相關的PC硬體架構做探討,可以
說是一舉兩得。不過,我必須假設讀者對於匯編語言及PC最基礎的架構,如寄存器,分段,
分頁,中斷服務等有大概的認識。
讀者可在linux source code的/boot子目錄下找到幾個以.S作為副檔名的組合語言檔,
本文要說明的即是其中的bootsect.S及setup.S兩個檔案,及盡量簡單地說明其所牽涉的
相關硬件部份。
bootsect.S
這個程序是linux kernel的第一個程序,包括了linux自己的bootstrap程序,但是
在說明這個程序前,必須先說明一般IBM PC開機時的動作(此處的開機是指"打開PC的電源"):
一般PC在電源打開時,是由內存中地址FFFF:0000開始執行(這個地址一定在ROMBIOS
中,ROMBIOS一般是在FE000h到FFFFFh中),而此處的內容則是一個jump指令,jump到另
一個位於ROMBIOS中的位置,開始執行一系列的動作,包括了檢查RAM,keyboard,顯示
器,軟硬磁盤等等,這些動作是由系統測試碼(system test code)來執行的,隨著制作
BIOS廠商的不同而會有些許差異,但都是大同小異,讀者可自行觀察自家機器開機時,
屏幕上所顯示的檢查訊息。
緊接著系統測試碼之後,控制權會轉移給ROM中的啟動程序(ROM bootstrap routine),
這個程序會將磁盤上的零道零扇區讀入內存中(這就是一般所謂的bootsect,如果你曾
接觸過電腦病毒,就大概聽過它的大名),至於被讀到內存的哪裡呢?----絕對位置07C0
:0000(即07C00h處),這是IBM系列PC的特性。而位在linux開機磁盤的bootsect上的正
是linux的bootsect程序,也就是說,bootsect是第一個被讀入內存中並執行的程序。
現在,我們可以開始來看看到底bootsect做了什麼。
第一步
首先,bootsect將它"自己"從被ROMBIOS載入的絕對地址0x7C00處搬到0x90000處,
然後利用一個jmpi(jumpindirectly)的指令,跳到新位置的jmpi的下一行去執行,關鍵
的匯編代碼如下:
.
(搬移bootsect本身)
.
.
jmpi go,INITSEG
go:
.
.
.
表示將跳到CS為0x9000,IP為offset"go"的位置(CS:IP=0x9000:offsetgo),其中
INITSEG=0x9000定義於程序開頭的部分,而go這個label則恰好是下一行指令所在的位置。
第二步
接著,將其它segment registers包括DS,ES,SS都指向0x9000這個位置,與CS看齊。
另外將SP及DX指向一任意位移地址(offset),這個地址等一下會用來存放磁盤參數表
(disk parameter table)。
提到磁盤參數表,就必須提到BIOS中斷1Eh。先簡單地介紹一下BIOS的中斷服務:
80x86將內存最低的256*4bytes保留給256個中斷向量(每個interrupt vector大小為4bytes,
所以一共有256*4=1024bytes),而其中的第1Eh個向量指向"磁盤參數表",這個表會告訴
電腦如何去讀取磁盤機,而我們所要做的事是搬移磁盤參數表到剛才所設定的任意地址。
接著,改變搬移來的參數表的參數,以符合我們的需要。再將中斷向量1Eh指向我們
所修改過的磁盤參數表,然後呼叫BIOSinterrupt的int13h(function0,即AH=0)重置磁
盤控制卡及磁盤驅動器,之後磁盤機就會照我們的意思動作了。如果你曾trace過DOS的
kernel,你會發現,上述的動作在DOS中也有類似的對應流程。
現在讓我們來看看關鍵的程序碼:.
.
.
push #0
pop fs
mov bx,#0x78
.
(使GS:SI=FS:BX,指向磁盤參數表,
再將GS:SI所指地址的內容搬移6個
Word至ES:DI所指的地址)
.
.
此段程序是將FS:BX調整成0000:0078,接著再將GS:SI的內容設成與FS:BX相同,此
處0x78h即為int1Eh的起始位置(7*16+8=120,(1*16+14)*4=120)。調整ES:DI為剛才所設
定的任意地址,從GS:SI搬移6個word(即12byte)到ES:DI所指的位置,顯然磁盤參數表的
長度就是6個word,(不過事實上,磁盤參數表的確實長度是11個byte)。關於磁盤參數表,
有興趣的讀者可自行參閱講述BIOSinterruptservices的技術手冊,會有詳細的說明。
讀者可以用debug自行觀察自家機器上dos的磁盤參數表的起始位置(即int1Eh的內容)。
以下是筆者機器的情形(筆者使用的操作系統是msdos6.2):
C:>debug
-d0000:0000
0000:0000 8A101601F4067000-1600CB04F4067000......p.......p.
0000:0010 F40670000301790E-43EB00F0EBEA00F0..p...y.C.......
0000:0020 04108E340C118E34-5700CB046F00CB04...4...4W...o...
0000:0030 8700CB0408079433-B700CB04F4067000.......3......p.
0000:0040 0C01790E4DF800F0-41F800F0BA165F06..y.M...A....._.
0000:0050 39E700F01B01790E-70118E341201790E9.....y.p..4..y.
0000:0060 00E000F085175F06-6EFE00F0EE067000......_.n.....p.
0000:0070 53FF00F0A4F000F0-220500003E4600C0S......."...>F..
^^^^^^^^
由上圖中可知,在DOS中磁盤參數表的起始位置(int1Eh的內容)為0000:0522。接著觀
察dos中位置0000:0522開始的11個byte,也就是磁盤參數表的內容
C:>debug
-d0000:0520l10
0000:0520 4D53DF022502121B-FF54F60F08000000MS..%....T......
^^^^^^^^^^^^^^^^^^^^^^
此11byte即為磁盤參數表的內容(分別是byte00h到0Ah)
在程序中我們所更動的是第五個byte(byte04h),改為18h(在上圖例子中為12h),這
個byte的功能是定義磁軌上一個磁區的資料筆數。關鍵的程序碼如下:
.
movb 4(di),*18
.
第三步
接著利用BIOS中斷服務int13h的第0號功能,重置磁盤控制器,使得剛才的設定發揮
功能。
.
.
xor ah,ah
xor dl,dl
int 0x13
.
.
第四步
完成重置磁盤控制器之後,bootsect就從磁盤上讀入緊鄰著bootsect的setup程序,
也就是以後將會介紹的setup.S,此讀入動作是利用BIOS中斷服務int13h的第2號功能。
setup的image將會讀入至程序所指定的內存絕對地址0x90200處,也就是在內存中緊鄰著
bootsect所在的位置。待setup的image讀入內存後,利用BIOS中斷服務int13h的第8號功
能讀取目前磁盤機的參數。
第五步
再來,就要讀入真正linux的kernel了,也就是你可以在linux的根目錄下看到的
vmlinuz。在讀入前,將會先呼叫BIOS中斷服務int10h的第3號功能,讀取游標位置,之
後再呼叫BIOS中斷服務int10h的第13h號功能,在螢幕上輸出字符串"Loading",這個字
符串在boot linux時都會首先被看到,相信大家應該覺得很眼熟吧。
linux的kernel將會被讀入至內存絕對地址0x10000處,關鍵的程序碼如下:
.
.
mov ax,#SYSSEG
mov es,ax
call read_it
call kill_motor
.
.
其中SYSSEG於程序開頭時定義為0x1000,先將ES內容設為0x1000,接著在read_it這
個子程序,便以ES為目的地的節地址,將kernel讀入內存中,至於read_it子程序的詳細內
容筆者並不想一一介紹,不過聰明的讀者們應該已經猜到,read_it一定又利用了BIOS
int13h與磁盤有關的I/O中斷服務了。
至於kill_motor子程序,它的功能在於停止軟盤機的馬達(各位聰明的讀者會不會覺
得這個子程序的名稱取得頗為傳神呢?),其程序碼如下:
.
.
kill_motor:
push dx
mov dx,#0x3f2
xor al,al
outb
pop dx
ret
.
.
首先利用DX指定要輸出的port,而03f2這個port則是代表了軟盤控制器(floppy
disk controller)的所在,再利用outb將資料送出,而我們送出的資料,當然就是歸零過的
AL了。如此一來,軟盤的馬達就停止了。
第六步
接下來做的事是檢查root device,之後就仿照一開始的方法,利用indirect jump跳
到剛剛已讀入的setup部份,程序碼如下:
.
.
jmpi 0,SETUPSEG
其中SETUPSEG已在先前定義為0x9020,所以CS:IP會設定為9020:0000,即跳到絕對
地址為0x90200,也就是setup的起點,而bootsect也大功告成了。
到此為止,內存的內容應該如下圖所示:
比較
把大家所熟知的msdos與linux的開機部份做個粗淺的比較,msdos由位於磁盤上
bootsect的boot程序負責把io.sys載入內存中,而io.sys則負有把dos的kernel--
msdos.sys載入內存的重大責任。而linux則是由位於bootsect的bootsect程序負責
把setup及linux的kernel載入內存中,再將控制權交給setup。
至於setup.S,就留到下一次再來討論了。
bootsect的boot程序負責把io.sys載入內存中,而io.sys則負有把dos的kernel--
msdos.sys載入內存的重大責任。而linux則是由位於bootsect的bootsect程序負責
把setup及linux的kernel載入內存中,再將控制權交給setup。
至於setup.S,就留到下一次再來討論了。