從拿到Linux3.1.1版內核源碼並搭建好閱讀環境開始,到現在大約已經徘徊了兩個多月的時間,期間google了大大小小的文章,纔剛剛理清了些許思路並找到了閱讀的切入口。對於內核初學者來說一個好的指導比什麼都重要,有關Linux內核學習的方法論可以參考fudan_abc寫的Linux內核修煉之道,作者以其深厚的內核功底加上詼諧幽默的文字對讀者娓娓道來,這樣的感染力使得我幾乎是一口氣不斷的看完了整個專欄,相信對於任何對內核有強烈興趣的學習者一定有很多助益。另外,對初學者來說光有方法論是不夠的,特別是對於Linux
Kernel如此龐大的迷宮,所以一份好的地圖在內核學習中同樣舉足輕重,Kconfig Makefile正扮演瞭如此重要的角色,特別是對於想要重點研究某一模塊或是子系統的學習者來說更是如此,詳細參見以上專欄的具體文章——Kernel地圖:Kconfig與Makefile,而對於Makefile
Kbuild體系介紹的更加具體的可以參見雲鬆寫的Makefile預備知識/Kbuild體系,當然最權威的信息自然來自內核文檔了。根據我自己的實際情況,其實我認爲並沒有必要去詳細分析每個makefile文件,因爲最後的結果是顯然的——一個內核映像由成百乃至上千個文件組成,這樣的解剖工作量無疑是巨大的,特別是對於像我這樣還沒有構建大型系統,甚至寫的最大的makefile文件都僅有寥寥數行的初學者來說,無疑是一個巨大的挑戰,因此通過google搜索前人所寫的關於某一模塊所依賴的源文件的文章可以讓我們把時間放在更加重要的源代碼剖析上。
Kbuild/Makefile/Kconfig
根據個人的親身體會,閱讀Linux內核對於新手來說首先要過的第一道坎便是源文件中大大小小的CONFIG_XXXX標識,這對於廣大的像我一樣沒有接觸過驅動開發/文件系統/網絡協議的學習者們無疑是對自信心的首個重大打擊,不過幸運的是,Linux內核的發行版提供了豐富的文檔,在內核學習的過程中碰到很多自己不熟悉的東西是很常見的現象,因此學會查找合適的文檔對於學習將會事半功倍。
有關Kbuild/Makefile/Kconfig的文檔可參見目錄Documentation\kbuild,以下列出了該文檔中有關這三者的簡要概述:
- Makefile
Makefile總共包含五個部分,分別爲:①頂層Makefile文件,②內核配置文件,③在各個體系結構下的makefile,在目錄arch/$(ARCH)中,④一系列用於kbuild Makefile的通用規則,這些文件主要在scripts目錄中,⑤kbuild Makefiles,這類文件大約有500個左右。
頂層Makefile讀取.config文件,該文件主要在內核的配置過程中生成。有關內核的具體配置可參見這裏。頂層的Makefile主要用來構造兩個最主要的文件:vmlinux——固定內核映像,以及任意的模塊,這種構造過程是通過遞歸進入內核源代碼樹的子目錄而完成的。需要訪問的子目錄列表依據內核的配置而定。頂層Makefile包含一個具體體系結構下的Makefile文件,該文件主要向頂層Makefile提供指定體系結構的信息。
每一個子目錄都包含kbuild Makefile,其用來執行由上級目錄傳遞下來的一些命令。kbuild Makefile通過使用.config文件中包含的信息來構造各種各樣的文件清單,最終kbuild根據這些文件清單構造任一內置的或模塊化的目標。
而scripts目錄中包含的一些Makefile.*文件中則包含了一系列的定義或是規則,這些規則根據kbuild makefile構造內核。 - Kbuild
這是從Linux2.6版本內核開始採用的編譯系統。簡言之,就是根據makefile生成的文件列表構造內置的或模塊化的目標。 - Kconfig
kconfig文件就是用來存放組織成樹形結構的一系列配置選項的配置數據庫。這些配置選項被組織成菜單條目的形式,一個簡單的例子如下:- config MODVERSIONS
- bool "Set version information on all module symbols"
- depends on MODULES
- help
- Usually, modules have to be recompiled whenever you switch to a new
- kernel. ...
這是一個簡單的配置菜單條目,不過這也說明了所有的配置條目所應有的特徵:"config"引出一個新的配置選項,接下來的行定義了一系列的屬性,這些屬性的類型可以是配置選項,輸入提示,依賴關係,幫助文檔或是默認值,需要注意的是同一個配置菜單條目可以被定義多次,但每一次定義都僅有一個輸入提示,並且類型定義不能衝突。絕大多數的條目都定義了一個配置選項,而其他的條目爲這些條目充當依賴關係。鑑於Kconfig文件重要性,這裏列出一些其常用語法:config MODVERSIONS bool "Set version information on all module symbols" depends on MODULES help Usually, modules have to be recompiled whenever you switch to a new kernel. ...
——類型定義 :"bool"/"tristate"/"string"/"hex"/"int",表示用戶配置該選項時的輸入類型。這裏有兩種最基本的類型:tristate和string類型,其他類型則基於這兩者。類型定義允許給出輸入提示,比如:- bool "Networking support"
等價於bool "Networking support"
- bool
- prompt "Networking support"
兩者都提示該配置菜單的輸入類型爲bool型,輸入之前將有一條提示爲“Networking support”。bool prompt "Networking support"
——默認值: "default" <expr> ["if" <expr>]
一個配置選項可以有任意數量的默認值。如果多個默認值是可見的,那麼當且僅當第一個定義的默認值處於活動狀態,即如果用戶不配置該選項那麼該默認值將被選擇。默認值將不受所在菜單項的限制,這意味着默認值可以被定義在任何其他地方或被先前的定義所覆蓋。可以通過添加"if"條件語句增加指定條件下存在的默認值。
——類型定義+默認值:"def_bool"/"def_tristate" <expr> ["if" <expr>]
這是類型定義和默認值的簡寫方式。同樣可以通過增加"if"語句添加指定條件下的類型及默認值。
——依賴:"depends on" <expr>
這爲本菜單項定義了一個依賴條件,如果有多個依賴,那麼它們可以通過符號"&&"連接。依賴應用於本菜單項從其定義處開始的所有其他屬性。例如:- bool "foo" if BAR
- default y if BAR
等價於bool "foo" if BAR default y if BAR
- depends on BAR
- bool "foo"
- default y
depends on BAR bool "foo" default y
——幫助文檔:"help"或者"---help---"
該屬性定義了一個幫助文檔,幫助文檔的末尾取決於其縮進層次,換言之,比幫助文檔開始的第一行有更小縮進的一行指示整個幫助文檔結束。"---help---"與"help"在作用上並無差別,只不過"---help---"幫助開發者將配置選項從整個菜單項中清晰的分離出來。
以上是對Kbuild/Makefile/Kconfig的簡要介紹,其實理解這三者的關鍵在於:因爲構建的軟件是通過對源文件編譯得到的,而Linux內核支持多種CPU架構,這使得整個內核包含成千上萬個每個架構下各自所需的源文件,然而在形成內核映像的過程中只需要從源文件中抽取出一部分針對某一種具體體系結構的源文件,以及所有體系結構下都需要的源文件(例如內存管理/進程調度/網絡等)進行編譯即可,Kbuild Makefile中的一部分實現的正是這樣的功能,然而事實是Makefile中構建每個目標所需的源文件之間的相互依賴關係錯綜複雜,並且上層的Makefile還需要調用下層的Makefile,這還有可能導致目標覆蓋,期間還可能需要scripts目錄中的腳本文件輔助編譯,還有Makefile中一大堆預定義的對於初學者來說格式奇怪的環境變量/自動化變量以及內建函數等等,最後鏈接順序對於某些模塊來講具有嚴格的遞進關係,否則還有可能導致硬件的損壞,可以看到,僅僅內核映像所需源文件的剖析過程就將耗費大量的精力,所以這一部分工作建議通過搜索引擎完成。而與我們的源文件有關的Kconfig則應該適當瞭解,否則正如上文所講,將對代碼的具體剖析過程造成極大的困擾。以Page_32_types.h頭文件中定義的宏__PAGE_OFFSET中使用到的變量CONFIG_PAGE_OFFSET爲例,遇到這類配置類型的變量首先在對應體系結構下的目錄中(arch\x86)找到Kconfig文件,其中有關CONFIG_PAGE_OFFSET的菜單項如下所示:
- config PAGE_OFFSET
- hex
- default 0xB0000000 if VMSPLIT_3G_OPT
- default 0x80000000 if VMSPLIT_2G
- default 0x78000000 if VMSPLIT_2G_OPT
- default 0x40000000 if VMSPLIT_1G
- default 0xC0000000
- depends on X86_32
config PAGE_OFFSET
hex
default 0xB0000000 if VMSPLIT_3G_OPT
default 0x80000000 if VMSPLIT_2G
default 0x78000000 if VMSPLIT_2G_OPT
default 0x40000000 if VMSPLIT_1G
default 0xC0000000
depends on X86_32
可以看到該菜單項是一個類型爲hex,即以16進制格式顯示的整型且不可視(non-visible)變量(因爲沒有任何輸入提示,這表現爲hex之後沒有帶雙引號的字符串,也沒有prompt輸入提示符),如果預定義了表達式VMSPLIT_3G_OPT那麼該值則爲0xB000 0000,後接三個默認值均爲在指定條件下PAGE_OFFSET可取的值,最後一個沒有任何條件的默認值爲0xC000 0000,即當我們使用默認配置時PAGE_OFFSET的取值即爲0xC000
0000,最後看到depends on X86_32語句,說明該菜單項對X86_32配置選項具有依賴關係,但我們看到其後並未定義其他屬性,因此直接忽略該依賴即可。源文件中的PAGE_OFFSET的含義是內核在單個進程的線性地址空間中的偏移量,這表明了在默認情況下,Linux內核將佔用x86架構下0~4G虛擬內存中最高1G的線性地址空間,同時也表明了在任意進程的頁目錄項中從0x300(即768)開始的項均對應的是內核地址空間。以上是有關Kbuild/Makefile/Kconfig的簡要介紹,想要更加詳細的瞭解這一部分的內容可參考之前所列的資料。可以看到與我們的源代碼剖析聯繫最緊密的就是我們的Kconfig配置文件,因此能夠大致理解Kconfig文件是閱讀內核源碼的最基本要求,另外在x86\configs目錄下的i386_defconfig文件中也有對一部分CONFIG__XXXX標識的默認配置。以上介紹的內容僅僅只是我們內核之旅開始的前奏,接下來就將正式進入我們的內核世界中。
加電啓動
首先需要知道,內存(DRAM)是一類易失性存儲設備,從微觀上表現爲,泄漏電流的各種因素會導致DRAM單元在10~100毫秒時間內失去電荷,因此存儲系統必須週期性地通過讀出然後寫回來刷新存儲器的每個位,這與SRAM不同,只要有電,SRAM中存儲的信息就不需要刷新,SRAM比之DRAM的優點還有存取速度快,對光和電的噪音干擾不敏感,所有這些優點的代價是SRAM單元比DRAM單元使用更多的晶體管,更貴且功耗更大;從宏觀上來講,其易失性則表現爲一旦斷電,那麼內存中所存儲的信息都將丟失。因此我們的內核必須存儲在一類永久性的介質中,即使斷電也照樣可以保存信息,在PC上這樣的介質就是我們的硬盤,它是通過將信息轉換爲電信號,再將電信號轉換爲磁場去磁化材料,這樣就將信息永久地保存下來,當然還可以是其他非易失性存儲設備如U盤。
然而我們的程序在執行之前都必須先被載入內存,因爲在CPU上執行的指令都是在內存中進行尋址的,繼而這就要求存在某種外力,在開機啓動時能夠將內核“放入”內存並執行相應的初始化工作,其後將控制權轉移給內核,向上對用戶提供所需的服務,向下則可以有效的管理硬件資源使得整個精彩紛呈的機器世界有效運轉,而這種“外力”就是我們的基本輸入/輸出系統(Basic Input/Output System,BIOS),之所以稱其爲“基本”輸入\輸出系統是因爲其包含幾個中斷驅動的低級過程。所有操作系統在啓動時都需要藉助這些過程對計算機硬件設備進行初始化。因爲BIOS過程只能在實模式下運行,所以Linux一旦進入保護模式就不再使用BIOS,而是爲計算機上的每個硬件設備提供各自的設備驅動程序。
在開始啓動時,有一個特殊的硬件電路在CPU的一個引腳上產生一個RESET邏輯值,CPU在識別出RESET信號後將數據總線設爲高阻抗狀態,地址線強行設爲1,並禁用中斷。在這之後就將處理器的一些寄存器設成固定的值,其中最重要的兩個寄存器——CS段寄存器被置爲0xf000,EIP指令指針寄存器爲0x0000 fff0,因爲此時CR0寄存器中的PE位還未被置位,因此處於實模式狀態下,對指令的地址計算是通過在16位的CS段寄存器內容最右邊補齊一個0從而形成一個小段,再加上IP指令指針寄存器中的內容(EIP寄存器的低16位),用一個形象的公式即表示爲:CS*16+IP,最終得到地址值爲0xf
fff0。注意這個地址尋址的第一條指令存放在BIOS中,而又因爲BIOS被固化在ROM中,並且對ROM中的指令進行尋址則需要使用32位地址(原因請參考PC存儲器一文),聯繫到之前所介紹的,CPU將地址線強行設爲1,最終形成的物理地址即爲0xffff fff0。形成該地址之後,CPU將其交給MCH(南橋芯片-相當於一個分配器,根據不同的地址映射分配給不同的設備去處理),MCH會決定這個地址要分配給ICH(北橋芯片-解碼器),最終這個地址被解析到BIOS的ROM裏面的地址。
而BIOS實際上大致執行以下4個操作:
- 對硬件執行一系列測試,用來檢測現在有哪些設備以及這些設備是否正常運轉,這個階段被稱爲POST(Power-On-Self-Test,加電自檢)。
- 初始化硬件設備,這個階段保證所有的硬件設備操作不會引起IRQ(中斷請求)線與I/O端口的衝突,最後顯示系統中安裝的所有PCI設備的一個列表。
- 搜索一個操作系統來啓動。這個過程可以根據用戶設置的順序依次進行訪問系統中的軟盤、硬盤以及CD-ROM的第一個扇區(即引導扇區),通常是在開機時按del鍵進入BIOS的設置界面,但也可能是其他鍵,視各個具體的PC而定。
- 按上述訪問次序找到一個有效設備後,即將第一個扇區的內容拷貝到RAM中物理地址0x0000 7c00處,隨後跳轉到該地址開始執行剛剛加載的代碼。
有關BIOS啓動過程更詳細的介紹可以參考BIOS啓動過程——硬件檢測及初始化淺析一文。
引導裝入過程(boot loader)
引導裝入程序是由BIOS裝載,且用來把操作系統的內核映像裝載到RAM中所執行的一個程序,該可執行過程存放在硬盤的第0個磁道第0個扇區上,由於總共只需佔用較少(512字節)的存儲空間,故也可稱之爲引導記錄,除此之外該引導記錄還包括64字節的分區表以及2個字節標識有效引導記錄結尾的標籤(0x55 0xAA)。但從Linux2.6開始不再執行這樣的引導裝入程序,這一點可以通過從header.S文件剖析得知:
- BOOTSEG = 0x07C0 /* original address of boot-sector */
- SYSSEG = 0x1000 /* historical load address >> 4 */
- .code16 /*表示16位模式*/
- .section ".bstext", "ax" /*將以下代碼放在.bstext節區,"ax"表示該節區可分配並且可執行*/
- .global bootsect_start /*定義全局標號bootsect_start*/
- bootsect_start:
- # Normalize the start address
- ljmp $BOOTSEG, $start2
- start2:
- movw %cs, %ax
- movw %ax, %ds
- movw %ax, %es
- movw %ax, %ss
- xorw %sp, %sp
- sti
BOOTSEG = 0x07C0 /* original address of boot-sector */
SYSSEG = 0x1000 /* historical load address >> 4 */
.code16 /*表示16位模式*/
.section ".bstext", "ax" /*將以下代碼放在.bstext節區,"ax"表示該節區可分配並且可執行*/
.global bootsect_start /*定義全局標號bootsect_start*/
bootsect_start:
# Normalize the start address
ljmp $BOOTSEG, $start2
start2:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti
在全局標號bootsect_start後執行一個長跳轉指令ljmp $BOOTSEG,$start2,因爲BOOTSEG的值爲0x07C0,並且當前仍處於實模式下,因此該指令表示轉移到段基址爲0x7C00(seg*16),偏移爲start2地址處,聯繫到開機啓動時第一個扇區的內容被BIOS拷貝到物理地址0x0000 7C00處,所以該長跳轉指令實際表示轉移到本段start2標號處繼續執行,可以看到在start2標號後的指令將cs段寄存器中的內容依次填充至ds/es/ss寄存器中,接着將sp堆棧指針寄存器內容清零並啓用中斷。- cld /*清除方向標誌,使用在串傳送指令中(在本例中爲lodsb),表示在完成傳送後將di寄存器自動增1*/
- movw $bugger_off_msg, %si /*將bugger_off_msg的首地址移入si寄存器*/
- msg_loop:
- lodsb /*將內存地址ds:si存放的內容讀入al寄存器中*/
- andb %al, %al /*測試al寄存器中的內容是否爲0*/
- jz bs_die /*若是則跳轉到標號bs_die處繼續執行*/
- movb $0xe, %ah
- movw $7, %bx
- int $0x10 /*調用BIOS0x10號中斷,該中斷的作用是顯示字符*/
- jmp msg_loop
cld /*清除方向標誌,使用在串傳送指令中(在本例中爲lodsb),表示在完成傳送後將di寄存器自動增1*/
movw $bugger_off_msg, %si /*將bugger_off_msg的首地址移入si寄存器*/
msg_loop:
lodsb /*將內存地址ds:si存放的內容讀入al寄存器中*/
andb %al, %al /*測試al寄存器中的內容是否爲0*/
jz bs_die /*若是則跳轉到標號bs_die處繼續執行*/
movb $0xe, %ah
movw $7, %bx
int $0x10 /*調用BIOS0x10號中斷,該中斷的作用是顯示字符*/
jmp msg_loop
可以發現上述代碼段的作用即是調用BIOS中斷例程在屏幕上顯示標識爲bugger_off_msg的字符串內容,bugger_off_msg的定義如下:- bugger_off_msg:
- .ascii "Direct booting from floppy is no longer supported.\r\n"
- .ascii "Please use a boot loader program instead.\r\n"
- .ascii "\n"
- .ascii "Remove disk and press any key to reboot . . .\r\n"
- .byte 0
bugger_off_msg:
.ascii "Direct booting from floppy is no longer supported.\r\n"
.ascii "Please use a boot loader program instead.\r\n"
.ascii "\n"
.ascii "Remove disk and press any key to reboot . . .\r\n"
.byte 0
該字符串告知用戶“已不再支持從軟盤直接啓動的方式,請代之以使用一個boot loader程序,移除磁盤並按任意鍵重啓”。接着顯示完該字符後跳轉到bs_die標號處繼續執行,該代碼段如下所示:- bs_die:
- # Allow the user to press a key, then reboot
- xorw %ax, %ax /*將ax寄存器清零*/
- int $0x16 /*從鍵盤讀入字符,即等待用戶按鍵*/
- int $0x19 /*重新啓動系統*/
- # int 0x19 should never return. In case it does anyway,
- # invoke the BIOS reset code...
- ljmp $0xf000,$0xfff0 /*長跳轉到0xf fff0處,即重新執行BIOS中的一系列初始化過程*/
bs_die:
# Allow the user to press a key, then reboot
xorw %ax, %ax /*將ax寄存器清零*/
int $0x16 /*從鍵盤讀入字符,即等待用戶按鍵*/
int $0x19 /*重新啓動系統*/
# int 0x19 should never return. In case it does anyway,
# invoke the BIOS reset code...
ljmp $0xf000,$0xfff0 /*長跳轉到0xf fff0處,即重新執行BIOS中的一系列初始化過程*/
以上代碼等待用戶按鍵之後即刻重啓。實際上雖然header.S文件中自帶bootloader,但如果內核從硬盤啓動該段代碼無論如何都不會被執行,而內核的維護者也不再維護這段引導記錄,關於這一點也可以從鏈接腳本文件的設置中看出。
- OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
- OUTPUT_ARCH(i386)
- ENTRY(_start) /*指定入口標號*/
- SECTIONS
- {
- . = 0;
- .bstext : { *(.bstext) }
- .bsdata : { *(.bsdata) }
- . = 497;
- .header : { *(.header) }
- .entrytext : { *(.entrytext) }
- .inittext : { *(.inittext) }
- .initdata : { *(.initdata) }
- __end_init = .;
- ......
- }
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start) /*指定入口標號*/
SECTIONS
{
. = 0;
.bstext : { *(.bstext) }
.bsdata : { *(.bsdata) }
. = 497;
.header : { *(.header) }
.entrytext : { *(.entrytext) }
.inittext : { *(.inittext) }
.initdata : { *(.initdata) }
__end_init = .;
......
}
以上文件爲arch\x86\boot目錄下的setup.ld,可以發現鏈接腳本指定的入口爲_start標號而非bootsect_start,這就好比在C/C++中指定main()函數爲入口點。事實上當Linux內核從_start標號處開始執行後便再也不會跳入bootsect_start標號執行。在這裏簡單提一下鏈接腳本的語法:在SECTIONS{}內設置最終生成的文件中各個節區的屬性,.
= 0表示.bstext以及.bsdata節區中的代碼及數據被加載至偏移0處。每個節區的描述格式都是“節區名 : { 組成 }”,例如.bstext : { *(.bstext) }
,左邊表示最終生成的文件的.bstext
段,右邊表示所有目標文件的.bstext
段,意思是最終生成的文件的.bstext節區
由各目標文件的.bstext節區
組合而成,有關鏈接腳本更詳細的介紹參見GNU-ld鏈接腳本淺析。聯繫上述引導裝入程序中開始處的.section
".bstext", "ax"指令,我們知道上述代碼位於被載入內存的整個內核映像的起始處,注意雖然這些指令不會被執行,但其仍然將會被載入內存,並且在這個512字節的引導記錄中存放的數據仍是有用的,這些參數將被用來初始化setup_header結構體(位於arch\x86\include\asm\Bootparam.h文件中),如下所示:
- .section ".header", "a"
- .globl hdr
- hdr:
- setup_sects: .byte 0 /* Filled in by build.c */
- root_flags: .word ROOT_RDONLY
- syssize: .long 0 /* Filled in by build.c */
- ram_size: .word 0 /* Obsolete */
- vid_mode: .word SVGA_MODE
- root_dev: .word 0 /* Filled in by build.c */
- boot_flag: .word 0xAA55
- # offset 512, entry point
.section ".header", "a"
.globl hdr
hdr:
setup_sects: .byte 0 /* Filled in by build.c */
root_flags: .word ROOT_RDONLY
syssize: .long 0 /* Filled in by build.c */
ram_size: .word 0 /* Obsolete */
vid_mode: .word SVGA_MODE
root_dev: .word 0 /* Filled in by build.c */
boot_flag: .word 0xAA55
# offset 512, entry point
這些參數位於.header節區中,由之前的鏈接腳本文件得知這個節區在整個引導記錄的偏移量爲497,而其中的變量將總共佔用1+2+4+2*4=15個字節的空間,因而最終構成512字節的引導記錄。另外從標識整個有效引導記錄結尾的標籤的彙編指令boot_flag: .word 0xAA55以及最後一行的註釋也可看出。另外要注意,由於x86體系結構採用小端法存放數據,因此0xAA55最後存放的形式從低到高依次爲0x55,0xAA,與我們之前所描述的並不衝突。而這些參數的填充從註釋中可以看出是由build.c文件完成的,我們將放在後文介紹這種參數的具體設置。前面提到過,bootloader由BIOS加載,並用於負責將內核映像裝入內存,然而Linux2.6開始不再執行該代碼,那麼裝載內核映像的重大責任由誰來承擔?針對不同的體系結構,Linux所使用的或是衆所周知的LInux LOader(LILO)或是廣泛使用的GRand Unified Bootloader(GRUB)。而在x86架構下,則是由其中之一的GRUB來裝載內核映像。
事實上,由於硬盤容量的飛速發展,通常是將一個硬盤劃分成若干個“分區”,從而可以把一個物理硬盤看成是若干個邏輯磁盤。這樣在每個邏輯磁盤上都可以存放一個操作系統映像,並且每個邏輯磁盤的第一個扇區仍然作爲引導扇區(boot sector),上文我們所剖析的Linux內核自帶的但已不再執行的bootloader便存放在該引導扇區中。顯然這些引導扇區在物理上已經不再是整個硬盤的第一個扇區了(即整塊硬盤上的第0個磁道第0個扇區),在這種情況下,整個硬盤的第一個扇區被獨立出來,不屬於任何一個邏輯磁盤。但BIOS在執行POST操作以及初始化後還是從這個扇區引導,因而在該扇區中存放的引導程序又被稱爲主引導記錄(Master
Boot Record,MBR)。有關MBR更詳細的信息可以參考操作系統裝載過程及BootSector的彙編語言實現一文。
結合前述分析,由於在一個物理硬盤上可能存在多個操作系統映像,因此GRUB的執行過程又具體細分爲如下兩個部分:
- 首先執行基本的引導裝載過程,該程序通常位於主引導記錄(MBR)中,大小爲512字節,由BIOS將其裝入RAM中物理地址0x0000 7c00處,而它的任務則是建立實模式棧並利用BIOS指令將第二引導加載過程裝入內存。
- 第二引導加載過程(又稱次引導過程)隨後從磁盤讀取可用操作系統的映射表,並提供給用戶一個提示符,當用戶選擇需要被載入的操作系統,或是經過一段時間的延遲自動選擇一個默認值之後,次引導過程便將相應分區下的內核映像以及initrd裝載到內存中,而前述的映射表是GRUB通過讀取/boot/grub/grub.conf文件中所設置的內容生成的。另外次引導過程還包括對特定文件系統(如ext2,ext3等)的支持以及對內核啓動代碼的初始化等職責,這就決定了次引導過程將佔用較大的存儲空間——連續多個扇區,從而無法裝進單個扇區中,因此GRUB通常將該過程放在特定的文件系統中(通常是boot所在的根分區)。
可以看到,整個GRUB是由MBR中的基本引導過程以及特定文件系統中的次引導過程兩部分構成。這裏要詳細說明的是,次引導過程拷貝到內存的目標中包括一個名爲initrd的文件,該文件的全稱爲boot loader initialized RAM disk,即bootloader初始化的內存盤。因爲在Linux內核的啓動過程中將會加載位於硬盤上的根文件系統,與硬盤相應的設備驅動程序又被存儲在該文件系統中,這就導出一個矛盾:加載根文件系統需要使用設備驅動程序,而設備驅動程序在根文件系統加載之前又無法載入內存運行。當然解決該矛盾最簡單的方法便是將設備驅動程序編譯進內核,然而如今的Linux內核支持多種硬件架構,因此根文件系統可能被存儲在IDE、SCSI、SATA、U盤等多種介質中,如果將所有的硬件驅動均編譯進內核無疑將使得內核臃腫不堪,引入initrd正是爲了解決如上問題,它主要用於實現一些模塊的加載以及文件系統的安裝等功能。在次引導過程完成相應文件的加載之後將會執行一個長跳轉指令,該指令跳過實模式內核代碼的前512個字節,也即跳到由前述鏈接腳本所指定的執行入口_start處開始執行,而所跳過的512字節正是我們之前剖析的Linux內核自帶的bootloader引導程序,整個的銜接過程可謂天衣無縫。
最後簡單總結GRUB的引導加載過程如下:
- 調用一個BIOS例程顯示”Loading“信息。
- 調用BIOS例程裝入內核映像的初始部分,將內核映像的第一個512字節(即爲存放在boot sector中已不再運行的bootloader)從地址0x0009 0000處開始裝入內存。
- 同樣通過調用BIOS例程裝載其餘的內核映像,並把內核映像放入從低地址0x0001 0000(小內核映像)或者從高地址0x0010 0000(大內核映像)開始的RAM中。
完成內核映像的裝載之後,控制權即正式轉交給內核。欲知後事如何,且聽下回分解。