Linux核心的配置與編譯
1 核心編譯過程
學習Linux核心除了必備的基礎知識、搭建Linux環境、下載核心原始碼和準備相關資源之外,第一件要做的事情應該就是編譯自己的Linux核心,然後執行編譯出來的核心。核心從配置,到編譯,再到安裝的命令非常簡單,只要按順序執行下面幾個命令就可完成:
1.核心配置:make menuconfig
2.核心編譯:make
3.安裝模組:make modules_install
4.安裝核心:make install
按順序執行完上面的命令後,重啟Linux系統,就是啟動剛編譯好的核心了。雖然Linux核心的編譯很簡單,但若想進一步的學習和定製Linux,就必須進一步瞭解核心編譯過程及原理。下面詳細的介紹Linux核心的編譯過程。
雖然Linux核心按預設的配置和編譯很簡單,但若想進一步的學習、定製和開發核心,就必須進一步瞭解核心編譯過程及原理。下面按核心的編譯順序進行詳細的介紹。
1.1 核心配置
Linux核心在下載解壓後需要先配置然後才能編譯。原始碼樹的每個目錄下都有一個檔案Kconfig,這個分佈到各目錄的Kconfig構成了一個分散式的核心配置資料庫,每個Kconfig分別描述了所屬目錄源文件相關的核心配置選單。在執行核心配置命令時,從Kconfig中讀出選單的資訊供使用者進行選擇,配置的命令如下:
1)make config:基於文字的最為傳統的一行一行的配置命令配置,不推薦使用。
2)make menuconfig:基於文字選單的配置介面,字元終端下推薦使用。
3)make xconfig:基於圖形視窗模式的配置介面,Xwindow下推薦使用。
以上命令會在scripts/kconfig/目錄下生成一個小工具,如:make menuconfig會生成mconf,然後用這個小工具根據各級目錄的Kconfig檔案在介面上展現選項供使用者選擇,執行完後生成一個.config檔案儲存使用者的核心配置選項,選擇相應的配置時,有三種選擇,它們分別代表的含義如下:
Y--將該功能編譯進核心。
N--不將該功能編譯進核心。
M--將該功能編譯成可以在需要時動態插入到核心中的模組。
make menuconfig命令執行介面就是由mconf小工具顯示的,介面如下:
根據自己的需要選擇適當的配置項,如果沒有特別的目的,按預設值配置即可,配置程式結束將在原始碼主目錄生成一個.config檔案,在核心編譯時,主Makefile呼叫這個.config就知道了使用者的選擇了。
1.2 編譯核心
從Linux核心2.6開始,Linux核心的編譯採用Kbuild系統,這和過去的編譯系統有很大的不同,尤其對於Linux核心模組的編譯。在新的系統下,Linux編譯系統會兩次掃描Linux的Makefile:首先編譯系統會讀取Linux核心頂層的Makefile,然後根據讀到的內容第二次讀取Kbuild的Makefile來編譯Linux核心。
1.2.1 生成vmlinux
頂層 Makefile 有兩個主要的目標,第一個目標就是生成vmlinux核心映像。頂層 Makefile 包含一個體繫結構 Makefile,由 arch/$(ARCH)/Makefile 指定,為頂層 Makefile 提供了特定體系結構的資訊。每個子目錄各有一個 Kbuild檔案和Makefile 檔案用來執行從上層傳遞下來的命令。Kbuild和Makefile利用.config 檔案中的資訊來構造得到沒有壓縮的核心vmlinux。
vmlinux是未壓縮的、原始的核心映像,編譯出來後放在原始碼主目錄裡,此檔案是ELF格式的檔案,通過readelf -S vmlinux命令可以檢視裡面的各部分的細節。“vm”代表“Virtual Memory”,Linux 支援虛擬記憶體,不像老的作業系統比如DOS有640KB記憶體的限制,Linux能夠使用硬碟空間作為虛擬記憶體,因此得名“vm”。
1.2.2 生成System.map
未壓縮的vmlinux生成後,make程式呼叫nm命令生產System.map。System.map是核心符號表檔案。Linux核心是一個很複雜的程式碼塊,有許許多多的全域性符號,Linux核心不使用符號名,而是通過變數或函式的地址來識別變數或函式名,比如像c0343f20這樣引用一個變數。雖然核心本身並不真正使用System.map,對於進行程式設計來說,人們更喜歡使用那些像size_t BytesRead這樣的名字,而不喜歡像c0343f20這樣的地址,所以編譯器/聯結器允許我們編碼時使用符號名,而核心執行時使用地址。在有的情況下,我們需要知道符號的地址,或者需要知道地址對應的符號,比如klogd,lsof和ps等軟體需要一個正確的System.map,如果你使用錯誤的或沒有System.map,klogd的輸出將是不可靠的,這對於排除程式故障會帶來困難。另外少數驅動需要System.map來解析符號,沒有為你當前執行的特定核心建立的System.map它們就不能正常工作。所以System.map對需要知道核心符號與地址對應關係的程式來說是相當重要的。
1.3 生成可引導的核心映像
未壓縮的vmlinux映像編譯出來後可能有100+MB,這麼大的檔案是需要通過壓縮才能生成能被Bootloader載入的核心映像。編譯時會先壓縮vmlinux映像,然後生成setup載入程式,最後把壓縮的vmlinux映像和setup載入程式打包成可引導的核心映像檔案。
1.3.1 壓縮vmlinux
未壓縮的vmlinux編譯出來後放在原始碼的主目錄下,壓縮的vmlinux編譯出來後放在arch/x86/boot/compressed/目錄下,兩個檔案是不一樣的,別搞混淆了。具體的壓縮步驟如下圖所示:
(1)通過objcopy命令把原始的vmlinux檔案中的除錯等資訊去掉生產vmlinux.bin檔案,此檔案是還沒有壓縮的ELF檔案,通過readelf命令可以比較出它和原始vmlinux檔案的區別。
(2)編譯一個小程式relocs,把原始的vmlinux中的需要重定位值的地址取出寫入vmlinux.relocs檔案,然後,將vmlinux.bin和vmlinux.relocs通過gzip壓縮成vmlinux.bin.gz。
(3)編譯一個小程式mkpiggy,計算vmlinux.bin.gz的壓縮前大小,解壓後大小,解壓偏移地址等資訊,生產一個小彙編檔案piggy.S。此檔案通過.incbin偽指令直接將vmlinux.bin.gz二進位制檔案插入到目標檔案中的input_data下標處,編譯此彙編檔案將會生產一個包含有vmlinux.bin.gz二進位制資料的piggy.o檔案。
(4)把vmlinux.lds(連結描述檔案,指明入口點為startup_32,各節的VMA等)、head_32.o(包含入口點startup_32的程式碼,然後呼叫解壓程式)、misc.o(包含解壓演算法的程式碼)、piggy.o檔案一起連結生成壓縮後的vmlinux映像。
壓縮後的vmlinux映像也是ELF檔案通常只有幾MB,裡面包含了自解壓的程式碼,用readelf -S vmlinux命令可以看到裡面有一個.rodata.compressed節,存放著壓縮的核心映像資料。
1.3.2 生成setup程式
光有壓縮vmlinux核心映像是不夠的,因為Bootloader執行在真實模式下功能及定址能力都很有限,所以需要加入一個程式用來接管Bootloader的控制權、初始化系統、開啟保護模式等,這個程式就是核心setup程式。編譯時make程式把setup.ld、及arch/x86/boot/目錄下的main.c、pm.c、pmjump.S、video.c等原始檔編譯生成setup.elf程式。
1.3.3 生成bzImage
setup.elf程式和壓縮的vmlinux都是ELF格式的檔案。因為ELF檔案附加了很多如檔案頭、符號表、重定位表等資訊,這些資訊對Bootloader載入核心是沒有意義的,所以make程式通過objcopy命令提取setup.elf和壓縮的vmlinux中有效的裸二進位制資料生成setup.bin和vmlinux.bin兩個檔案,然後編譯一個小程式build,把setup.bin和vmlinux.bin合在一起生成bzImage。
bzImage是可引導的、壓縮的核心映像。雖然它裡面包含了壓縮的vmlinux,但不能通過gunzip 或 gzip解壓,只能通過Bootloader引導和解壓。bzImage是編譯核心時通過命令make bzImage建立,需要注意,bzImage不是用bzip2壓縮的,bzImage中的bz容易引起誤解,bz表示“big zImage”的意思,它解壓縮到高階記憶體(1M以上)。與bzImage對應的是zImage,不過現在PC中很少用到一般用於嵌入式系統,編譯時通過“make zImage”建立的,適用於小核心的情況,它解壓縮到低端記憶體(第一個640K)。bzImage和zImage兩種方式引導的系統執行時是相同的,如果核心比較小,那麼可以採用zImage 或bzImage之一,大的核心必須採用bzImage,不能採用zImage。
1.4 編譯及安裝模組
Linux核心是由一個個模組組成的一個集合,這些模組可以在編譯核心時一起編譯到核心中,也可以在執行時動態地載入到核心中,核心模組是Linux向外部提供的一個介面,其全稱是:動態可載入核心模組(Loadable Kernel Module,LKM),簡稱模組。Linux之所以提供模組機制,是因為Linux核心本身是一個單核心(Monolithic Kernel)結構,最大的優點就是效率高,因為所有內容都整合在一起,但其缺點是可擴充套件性和可維護性較差,模組機制就是為了彌補這一缺陷而設計的。模組是具有獨立功能的程式,它可以被單獨編譯,但是不能單獨執行,因為它沒有單獨的main()函式,只有初始化函式,模組在執行時被連結到核心並作為核心的一部分在核心空間中執行,這與執行在使用者空間下的程序是不同的,模組通常由一組函式和資料結構組成,用來實現一種檔案系統、一個驅動程式、一個作業系統服務、增加一個新的系統呼叫,或其它核心上層的功能。總之,模組是一個為核心或其他核心模組提供使用功能的程式碼塊。
頂層 Makefile 編譯出vmlinux後將會編譯 modules(所有模組檔案),首先生成組成模組的對應 .o 檔案和 .mod 檔案,然後用 scripts/mod/modpost 來生成 .mod.c 檔案,並將其編譯成 .mod.o 物件檔案,最後將 .mod.o 連同前面的 .o 一起連結成 .ko 模組檔案。另外,在生成vmlinux的過程中,會在核心頂層目錄中生成一個 Modules.symvers,裡面存放基本核心匯出的、供模組使用的符號以及CRC校驗和。
編譯完所以模組後,核心的編譯任務就算完成了,接下來就是呼叫make modules_install安裝模組,安裝時呼叫指令碼/sbin/installkernel根據在核心配置、編譯階段生成的核心模組以及模組依賴關係/lib/modules/<version>/modules.dep制模組檔案到/lib/modules/<version>目錄下,安裝完成。
1.5 安裝核心
安裝完模組後接著安裝核心,make install命令呼叫了核心目錄中的install.sh的shell指令碼完成安裝任務。
1.5.1 生成initramfs
當Linux核心啟動系統時,它必須找到並執行第一個使用者程式通常被稱為init,方能成功開機。由於使用者程式是儲存在檔案系統中的,因此Linux核心必須先找到並掛上第一個“根”檔案系統。通常,可用的檔案系統列在檔案/etc/fstab裡,以便mount能夠找到它們。但/etc/fstab本身就是一個儲存在檔案系統中檔案中。所以找到第一個檔案系統成為雞生蛋蛋生雞的問題。
為了解決這個問題,核心開發者建立核心命令列選項 “root=”,用來指定root檔案系統在哪個裝置上。十五年前,“root=”所指的裝置很容易找到,因為它不是在軟盤上就是硬碟的分割槽上。如今root檔案系統可以儲存在大量各種不同型別的硬體(SCSI, SATA, flash MTD,USB)上,甚至是由不同型別硬體所建立的RAID上。root檔案系統還可以存在主機外部的網路伺服器上(核心找到“root”前得先取得DHCP地址,完成DNS lookup,並 使用帳號及密碼 登入到遠端伺服器)。 root檔案系統的位置很可能每次啟動都在不同的位置。除了位置策略,root檔案系統的儲存方式也存在策略需要,比如,root檔案系統也可能被壓縮(如何?),被加密(用什麼keys?),或 loopback掛接(哪裡?)……不管處理root的策略如何,記住核心最終的目標還是找到根檔案系統並執行其上的第一個init程式,完成開機。
面對如此多的動態策略,單一命令列引數(root=)遠遠不能滿足了。即使將所有特殊案例的行為(裝置列舉,加金鑰,或網路登入)都硬編碼入進核心也無濟於事,因為特殊的系統太多了。更糟的是,替核心加入這些複雜行為的工作,就像是用匯編語言寫web軟體:即便可以做到,但也不能很划算很容易的通過使用適當工具完成。隨著核心的發展,為了解決這個無休止甚至在不斷增加複雜度的問題,核心開發者決定收回現有解決方案,重新尋求更好的整體的解決方案。
Linux 2.6核心將一個小的ram-based initial root filesystem(initramfs)連線入核心,initramfs一個壓縮過的cpio格式的打包檔案。當核心啟動時,會從這個打包檔案中匯出檔案到核心的rootfs檔案系統, 然後核心檢查rootfs中是否包含有init檔案,如果有則執行它,作為PID為1的第一個程序。這個init程序負責啟動系統後續的工作,包括定位、 掛載“真正的”根檔案系統裝置(如果有的話)。如果核心沒有在rootfs中找到init檔案,則核心會按以前版本的方式定位、掛載根分割槽,然後執行 /sbin/init程式完成系統的後續初始化工作。
安裝核心時make install命令呼叫了核心目錄中的install.sh的shell指令碼來完成安裝任務。install.sh呼叫/sbin/installkernel指令碼,/sbin/installkernel最終呼叫/sbin/dracut把initramfs安裝到/boot目錄下。
1.5.2 生成vmlinuz
vmlinuz是最終安裝後的Linux核心,放在/boot目錄下,並不會出現在原始碼目錄中,檔名可能帶有版本字尾等資訊,它其實是bzImage或zImage安裝後的別名。 安裝核心命令呼叫了核心目錄中的install.sh的shell指令碼。該指令碼首先將bzImage、System.map複製到/boot目錄,並將這兩個檔案依次改名為vmlinuz-<version>,System.map-<version>。接著修改/boot/grub/grub.conf檔案、新增新的引導選單,安裝完成。
核心編譯安裝完成之後,就可以重啟系統了。