今天開始我的讀核罹難記.第一次讀內核,實在難度很大。
面對近123 M 的源碼,困惑是難免的所以我決定先從大面上把握,再在某一些具體的點上切入.這樣一來linux 的啓動過程便十分重要,因此我先用dmesg命令察看一下linux啓動時打出的消息.
(我想源文件應在/usr/src/linux/init/main.c中)
內核的啓動最後是到 start_kernel ( in /init/main.c )也就是說啓動的過程是從 head.S ( arch/i386/boot/ ) 一直運行到 main.c(start_kernel) .它的作用是完成開機後的設置與內核的初始化,然後,系統究竟入一個無限的循環中等待用戶的輸入,調用fork來產生子進程.從而達到交互式操作系統的設計要求.
啓動系統
當PC機加電開始啓動時,80X86的處理器(CPU)在實模式下自檢,開始執行物理地址 0xFFFF0即ROM-BIOS的起始地址處的代碼。PC機的BIOS進行系統自檢,初始化中斷向量表到物理地址0x0。然後把引導設備的第一個扇區加 載到地址0x7C00,執行此處的指令。到這裏與linux無關,x86系列的硬件設置如此.
linux的內核本身是不能自舉的,所以lilo和loadlin的作用就是加載系統內核.有關lilo的原理可以參考lilo的readme.從加電到內核加載的過程是:加電->執行BIOS->加載第一扇區->lilo->加載內核
Linux內核的最初部分代碼是用匯編語言寫的(文件是boot/bootsect.s)。它首先把自身這部分代碼移到絕對地址0x90000,把下面的2K代碼從引導設備加載到地址0x90200上,內核的其餘部分加載到地址 0x10000處。在加載系統時顯示“loading...”. 然後,程序控制權交給另一個實模式彙編程序(boot/Setup.S)。接下來,此程序把整個系統從地址0x10000移到地址0x1000,進入保護 模式。程序控制轉給系統的其餘部分 即地址0x1000。
下一個步驟是系統內核的解壓過程,這部分代碼在地址0x1000(文件/Boot/head.S),該段程序初始 化寄存器,然後執行decompress_kernel(),這個函數源於zBoot/inflate.c、zBoot/unzip.c和 zBoot/misc.c三個文件
Loading ....[ bootsect.S ]
uncompress .....[ decompress_kernel() ]
main.c ---> start_kernel() 開始.
開始 printk(banner);
由於我主要負責bootsect.S部分,所以重點分析它。這個程序是linuxkernel的第一個程序,包括了linux自己的bootstrap程序,但是在說明這個程序前,必須先說明一般IBMPC開機時的動作(此處的開機是指"打開PC的電源" ):
一般PC在電源一開時,是由內存中地址FFFF:0000開始執行(這個地址一定在ROMBIOS中,ROMBIOS一般是在FEOOOh到FFFFFh中),而此處的內容則是一個jump指令,jump到另一個位於ROMBIOS中的位置,開始執行一系列的動作,包括了檢查RAM,keyboard,顯示器,軟硬磁盤等等,這些動作是由系統測試碼(systemtestcode)來執行的,隨着製作BIOS廠商的不同而會有些許差異,但都是大同小異。
緊接着系統測試碼之後,控制權會轉移給ROM中的啓動程序(ROMbootstraproutine) ,這個程序會將磁盤上的零道零扇區讀入內存中(這就是一般所謂的bootsector,因爲我曾接觸過電腦病毒,所以很早以前就聽過它的大名),至於被讀到內存的哪裏呢?--絕對位置07C0:0000(即07C00h處),這是IBM系列PC的特性。而位在linux開機磁盤的bootsector上的正是linux的bootsect程序,也就是說,bootsect是第一個被讀入內存中並執行的程序。現在,可以開始來看看到底bootsect做了什麼。
第一步
首先,bootsect將它"自己"從被ROMBIOS載入的絕對地址0x7C00處搬到0x90000處,然後利用一個jmpi(jumpindirectly)的指令,跳到新位置的jmpi的下一行去執行,關鍵的assemblycode如下:
.
(搬移bootsect本身)
.
.
jmpigo,INITSEC
go:
.
.
.
表示將跳到CS爲0x9000,IP爲offset"go"的位置(CS:IP=0x9000:offsetgo),其中INITSEC=0x9000定義於程序開頭的部份,而go這個label則恰好是下一行指令所在的位置。
第二步
接着,將其它segmentregisters包括DS,ES,SS都指向0x9000這個位置,與CS看齊。另外將SP及DX指向一任意位移地址(offset),這個地址等一下會用來存放磁盤參數表 (diskpara-metertable)
提到磁盤參數表,就必須提到BIOS中斷1Eh。先簡單的介紹一下BIOS的中斷服務:80x86將內存最低的256*4byte保留給256箇中斷向量(每個interruptvector大小爲4byte,所以一共有256*4=1024byte),而其中的第1Eh個向量指向"磁盤參數表",這個表會告訴電腦如何去讀取磁盤機,而我們所要做的事是搬移磁盤參數表到剛纔所設定的任意地址。
接着,改變搬移來的參數表的參數,以符合我們的需要。再將中斷向量1Eh指向我們所修改過的磁盤參數表,然後呼叫BIOSinterrupt的int13h(function0,即AH=0)重置磁盤控制卡及磁盤驅動器,之後磁盤機就會照我們的意思動作了。如果你曾trace過DOS的 kernel,你會發現,上述的動作在DOS中也有類似的對應流程。
現在來看看關鍵的程序碼:.
.
push#0
popfs
movbx,#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的磁盤參數表的起始位置(即int 1Eh的內容)。以下是我做的實驗的情形(我使用的系統是MSDOS6.2):
C:>debug
-d0000:0000
0000:00008A101601F4067000-1600CB04F4067000......p.......p.
0000:0010F40670000301790E-43EB00F0EBEA00F0..p...y.C.......
0000:002004108E340C118E34-5700CB046F00CB04...4...4W...o...
0000:00308700CB0408079433-B700CB04F4067000.......3......p.
0000:00400C01790E4DF800F0-41F800F0BA165F06..y.M...A....._.
0000:005039E700F01B01790E-70118E341201790E9.....y.p..4..y.
0000:006000E000F085175F06-6EFE00F0EE067000......_.n.....p.
0000:007053FF00F0A4F000F0-220500003E4600C0S......."...>F..
由上圖中可知,在DOS中磁盤參數表的起始位置(int1Eh的內容)爲0000:0522。接着觀察DOS中位置0000:0522開始的11個byte,也就是磁盤參數表的內容
C:>debug
-d0000:0520l10
0000:05204D53DF022502121B-FF54F60F08000000MS..%....T......
此11byte即爲磁盤參數表的內容(分別是byte00h到0Ah)
在程序中我們所更動的是第五個byte(byte04h),改爲18h(在上圖例子中爲12h),這
個byte的功能是定義磁軌上一個磁區的資料筆數。關鍵的程序碼如下:
.
movb4(di),*18
.
第三步
接着利用BIOS中斷服務int13h的第0號功能,重置磁盤控制器,使得剛纔的設定發揮功能。
.
xorah,ah
xordl,dl
int0x13
.
第四步
完成重置磁盤控制器之後,bootsect就從磁盤上讀入緊鄰着bootsect的setup程序,也就是以後將會介紹的setup.S,此讀入動作是利用BIOS中斷服務int13h的第2號功能。setup的image將會讀入至程序所指定的內存絕對地址0x90200處,也就是在內存中緊鄰着bootsect所在的位置。待setup的image讀入內存後,利用BIOS中斷服務int13h的第8號功能讀取目前磁盤機的參數。
第五步
再來,就要讀入真正linux的kernel了,也就是你可以在linux的根目錄下看到的"v mlinuz"。在讀入前,將會先呼叫BIOS中斷服務int10h的第3號功能,讀取遊標位置,之後再呼叫BIOS中斷服務int10h的第13h號功能,在螢幕上輸出字符串"Loading",這個字符串在bootlinux時都會首先被看到,相信大家應該覺得很眼熟吧。
linux的kernel將會被讀入至內存絕對地址0x10000處,鍵關的程序碼如下:
.
movax,#SYSSEG
moves,ax
callread_it
callkill_motor
.
其中SYSSEG於程序開頭時定義爲0x1000,先將ES內容設爲0x1000,接着在read_it這個子程序便以ES爲目的地的節地址,將kernel讀入內存中。
第六步
接下來做的事是檢查rootdevice,之後就仿照一開始的方法,利用indirectjump跳至剛剛已讀入的setup部份,程序碼如下:
jmpi0,SETYPSEG
其中SETUPSEG已在先前定義爲0x9020,所以CS:IP會設定爲9020:0000,即跳到絕對地址爲0x90200,也就是setup的起點。而bootsect也大功告成了。