前言對于編譯內(nèi)核而言,一條make命令就足夠了。構(gòu)建內(nèi)核最困難的地方不是編譯,而是編譯前的配置。配置內(nèi)核時,通常我們都能找到一些參考。 比如,對于桌面系統(tǒng),可以參考主流發(fā)行版的內(nèi)核配置,比如,對于嵌入式系統(tǒng),BSP(Board Support Package)中通常也提供內(nèi)核,但他們通常也僅是個可以工作的內(nèi)核而已,如果要一個占用空間更小、運(yùn)行更快的內(nèi)核,就需要開發(fā)人員手動配置內(nèi)核,也確實(shí)存在著在某些情況下,我們找不到任何合適的參考,這時我們只能以手動方式從零開始配置。 構(gòu)建內(nèi)核 內(nèi)核的構(gòu)建系統(tǒng)kbuild基于GNUMake,是一套非常復(fù)雜的系統(tǒng)。我們本無意著太多筆墨來分析kbuild,因為作為開發(fā)者可能永遠(yuǎn)不需要去改動內(nèi)核映像的構(gòu)建過程,但是了解這一過程,無論是對學(xué)習(xí)內(nèi)核,還是進(jìn)行內(nèi)核開發(fā)都有諸多幫助。所以在構(gòu)建內(nèi)核之前,本章首先討論了內(nèi)核的構(gòu)建過程。 3.1 內(nèi)核映像的組成 在討論內(nèi)核構(gòu)建前,我們先來簡單了解一下內(nèi)核映像的組成,如圖3-1所示。 如果將內(nèi)核的映像比作航天器,則setup.bin部分就類似于火箭的一級推進(jìn)子系統(tǒng),負(fù)責(zé)將內(nèi)核加載進(jìn)內(nèi)存,并為后面內(nèi)核保護(hù)模式的運(yùn)行建立基本的環(huán)境。加載內(nèi)核的功能被分離到Bootloader中,setup.bin則退化為輔助Bootloader將內(nèi)核加載到內(nèi)存,包圍在32位保護(hù)模式部分外的是非解壓縮部分??梢钥醋魇腔鸺亩壨七M(jìn)子系統(tǒng),將壓縮的內(nèi)核解壓到合適的位置,并進(jìn)行內(nèi)核重定位,在完成這個環(huán)節(jié)后,其從內(nèi)核映像脫離。 內(nèi)核的32位保護(hù)模式部分vmlinux。相當(dāng)于航天器的有效載荷,最后運(yùn)行的衛(wèi)星或者宇宙飛船,只有留在軌道內(nèi)(內(nèi)存中)運(yùn)行。內(nèi)核構(gòu)建時,將對有效載荷vmlinux進(jìn)行壓縮,然后與二級推進(jìn)系統(tǒng)裝配為vmlinux.bin。 下面我們就來看看內(nèi)核映像的各個組成部分。 3.1.1 一級推進(jìn)系統(tǒng)-setup.bin 在進(jìn)行內(nèi)核初始化時,需要一些信息,如顯示信息、內(nèi)存信息等。曾經(jīng),這些信息由工作在實(shí)模式下的setup.bin通過BIOS獲取,保存在內(nèi)核中的變量boot_params中,變量boot_params 是結(jié)構(gòu)體 boot params 的一個實(shí)例。如setup.bin 中收集顯示信息的代碼如下: 1inux-3.7.4/arch/x86/boot/video.c: static void store_video_mode(void) ( struct biosregs ireg, oreg; initregs(&ireg); ireg.ah=0x0f; intcall(0x10,&ireg,&oreg);
boot_params.screen_info.orig_video_mode=oreg.al&0x7f;
boot_params.screen_info.orig_video_page=oreg.bh; store_video_mode首先調(diào)用函數(shù)intcall獲取顯示方面的信息,并將其保存在boot_params的screen_info中。intcall是調(diào)用BIOS中斷的封裝,0x10是BIOS提供的顯示服務(wù)(Video Service)的中斷號,代碼如下:
linux-3.7.4/arch/x86/boot/bioscall.s: intcall: /* Self-modify the INT instruction. Ugly, but works. */ cmpb gal, 3f je 1f movb gal, 3f jmp 1f /* Synchronize pipeline */ 1: ... .byte Oxcd /* INT opcode */ 3: .byte 在代碼中我們并沒有看到熟悉的調(diào)用BIOS中斷的身影,如“int$0x10”,但是我們看到了一個特殊的字符——Oxcd。正如其后面的注釋所言,Oxcd就是x86匯編指令I(lǐng)NT的機(jī)器碼,如表3-1所示。
 根據(jù)x86的INT指令說明,Oxcd后面跟著的1字節(jié)就是BIOS中斷號,這就是上面代碼中標(biāo)號為3處分配1字節(jié)的目的。 0 函數(shù)intcall的開頭,比較寄存器al中的值與標(biāo)號3處占用的1字節(jié),若直接向前跳轉(zhuǎn)至標(biāo)號1處,否則將寄存器al中的值復(fù)制到標(biāo)號3處的1個字節(jié)空間。那么寄存器al中保存的是什么呢? 默認(rèn)情況下,GCC使用樹來傳遞參數(shù)??梢允褂谩癬attribute_(regparm(n)”修飾函數(shù),或者通過向GCC傳遞命令行參數(shù)“-mregparm=n”來指定GCC使用寄存器傳遞參數(shù),其中n表示使用寄存器傳遞參數(shù)的個數(shù)。在編譯setup.bin時,kbuild使用了后者,編譯腳本如下所示: linux-3.7.4/arch/x86/boot/Makefile: KBUILD_CFLAGS :=...-mregparm=3... 如此,函數(shù)的第一個參數(shù)通過寄存器eax/ax傳遞,第二個參數(shù)通過ebx/bx傳遞,等等,而不是通過樹傳遞了。因此,上面的寄存器al中保存的是函數(shù)intcall的第一個參數(shù),即BIOS中斷號。 在完成信息收集后,setup.bin將CPU切換到保護(hù)模式,并跳轉(zhuǎn)到內(nèi)核的保護(hù)模式部分執(zhí)行。如我們前面討論的,setup.bin作為一級推進(jìn)系統(tǒng),即將結(jié)束歷史使命,所以內(nèi)核將setup.bin收集的保存在setup.bin的數(shù)據(jù)段的變量boot_params復(fù)制到vmlinux的數(shù)據(jù)段中。
隨著BIOS標(biāo)準(zhǔn)的出現(xiàn),尤其是EFI的出現(xiàn),為了支持這些新標(biāo)準(zhǔn),開發(fā)者們制定了32位啟動協(xié)議(32-bit boot protocol)。在32位啟動協(xié)議下,由Bootloader實(shí)現(xiàn)收集這些信息的功能,內(nèi)核啟動時不再需要首先運(yùn)行實(shí)模式部分(即setup.bin),而是直接跳轉(zhuǎn)到內(nèi)核的保護(hù)模式部分。因此,在32位啟動協(xié)議下,不再需要setup.bin收集內(nèi)核初始化時需要的相關(guān)信息。但是這是否意味著可以徹底放棄setup.bin呢? 二級推進(jìn)系統(tǒng)-內(nèi)核非壓縮部分 內(nèi)核經(jīng)過壓縮,因此運(yùn)行前需要解壓縮,但是誰來負(fù)責(zé)內(nèi)核映像的解壓呢?解鈴還須系鈴人,既然內(nèi)核在構(gòu)建時自己壓縮了自己,當(dāng)然解壓縮也要由內(nèi)核映像自己完成。 除了解壓以外,非壓縮部分還負(fù)責(zé)內(nèi)核重定位。內(nèi)核可以配置為可重定位的(relocatable),所謂可重定位即內(nèi)核可以被Bootloader加載到內(nèi)存任何位置。但是在鏈接內(nèi)核時,鏈接器需要假定一個加載地址,然后以這個假定地址為參考,為各個符號分配運(yùn)行時地址。顯然,如果加載地址和鏈接時假定的地址不同,那么需要對符號的地址進(jìn)行重新修訂,這就是內(nèi)核重定位。 內(nèi)核非壓縮部分工作在保護(hù)模式下,其占用的內(nèi)存在完成使命后將會被釋放。 有效載荷-vmlinux kbuild分別構(gòu)建內(nèi)核各個子目錄中的目標(biāo)文件,然后將它們鏈接為vmlinux。為了縮小內(nèi)核體積,kbuild刪除了vmlinux中一些不必要的信息,并將其命名為vmlinux.bin,最后將vmlinux.bin壓縮為vmlinux.bin.gz。那么為什么內(nèi)核要進(jìn)行壓縮呢? 1.最初,因為在某些體系架構(gòu)上,特別是1386,系統(tǒng)啟動時運(yùn)行于實(shí)模式狀態(tài),可以尋址空間只能在1MB以下,內(nèi)核尺寸過大,將無法正常加載,因此,對內(nèi)核進(jìn)行了壓縮。 2.另外一個原因是,2.4及更早版本的內(nèi)核,需要可以容納在一張軟盤上,所以內(nèi)核也要進(jìn)行壓縮。 映像的格式 Linux作為操作系統(tǒng)的hosted environment環(huán)境下,二進(jìn)制文件使用ELF格式,操作系統(tǒng)也提供ELF文件的加載器。但是,操作系統(tǒng)本身確是工作在freestanding environment 環(huán)境下。操作系統(tǒng)顯然不能強(qiáng)制要求 Bootloader 也提供ELF加載器。 但是,從Linux 2.6.26版本開始,內(nèi)核的壓縮部分,即有效載荷部分,采用了ELF格式。至于為什么采用ELF格式,Patch的提交者給出了原因: This allows other boot loaders such as the Xen domain builder the opportunity to extract the ELF file. 當(dāng)內(nèi)核映像不是裸二進(jìn)制格式時,我們需要有一個ELF加載器來將ELF格式的內(nèi)核映像轉(zhuǎn)化為裸二進(jìn)制格式。那么誰來充當(dāng)這個ELF加載器呢? 正所謂“蜘蜂捕蟬,黃雀在后”。內(nèi)核的非壓縮部分調(diào)用函數(shù)decompress解壓內(nèi)核后,緊接著就調(diào)用了函數(shù)parse_elf來處理ELF格式的內(nèi)核映像,代碼如下: 11nux-3.7.4/arch/x86/boot/compressed/misc.c:agml inkage void decompress kernel (...)decompress (input_data, input_len, ...); parse_elf(output); static void parse_elf (void *output) for (i - 0; i < ehdr.e_phnum; i++) { phdr = &phdrs [i]; switch (phdr->p_type) { case PT_LOAD: #ifdef CONFIG RELOCATABLE dest = output; dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR); #else dest = (void *) (phdr->p_paddr); #endif memcpy (dest, output + phdr->p_offset, phdr->p_filesz); break; default:/*Ignore other PT_**/break; HZ BOOKS free(phdrs); 在ELF文件中,存放代碼和數(shù)據(jù)的段的類型是PT_LOAD,因此,僅處理這個類型的段即可。在函數(shù)parse_elf中,對于類型是PT_LOAD的段,其按照Program Header Table中的信息,將它們移動到鏈接時指定的物理地址處,即p_paddr。當(dāng)然,如果內(nèi)核是可重定位的,還要考慮內(nèi)核實(shí)際加載地址與編譯時指定的加載地址的差值。 如果Bootloader不是所謂的“the Xen domain builder”,我們完全沒有必要保留內(nèi)核的壓縮部分為ELF格式,并略去啟動時進(jìn)行的“parse_elf”。具體方法如下: (1)將壓縮部分鏈接為裸二進(jìn)制格式 將傳遞給命令objcopy的參數(shù)追加“-Obinary”,如下面使用黑體標(biāo)識的部分 :1inux-3.7.4/arch/x86/boot/compressed/Makefile: OBJCOPYFLAGS_vmlinux.bin:=-R.comment-s-o binary $(obj)/vmlinux.bin:vmlinux FORCE $(call if_changed,objcopy)
總結(jié)
|