解析 Linux 內(nèi)核可裝載模塊的版本檢查機制
Linux 在發(fā)展過程中(即自 Linux 1.2 之后)引進了模塊這一重要特性,該特性提供內(nèi)核可在運行時進行擴展??裳b載模塊(Loadable Kernel Module,即 LKM)也被稱為模塊,就是可在內(nèi)核運行時加載到內(nèi)核的一組目標代碼(并非一個完整的可執(zhí)行程序)。這就意味著在重構(gòu)和使用可裝載模塊時并不需要重新編譯內(nèi)核。模塊依據(jù)代碼編寫與編譯時的位置可分:內(nèi)部模塊和外部模塊,即 in-tree module 和 out-of-tree module,在內(nèi)核樹外部編寫并構(gòu)建的模塊就是外部模塊。如果只是認為可裝載模塊就是外部模塊或者認為在模塊與內(nèi)核通訊時模塊是位于內(nèi)核的外部的,那么這在 Linux 下均是錯誤的。當模塊被裝載到內(nèi)核后,可裝載模塊已是內(nèi)核的一部分。另外,我們使用的 Linux 發(fā)行版在系統(tǒng)啟動過程 initrd 中已使用了必要的模塊,除非我們只討論基礎內(nèi)核(base kernel)。本文主要是對 Linux 2.6 的外部模塊進行討論的。
可裝載模塊在 Linux 2.6 與 2.4 之間存在巨大差異,其最大區(qū)別就是模塊裝載過程變化(如 圖 1所示,在 Linux 2.6 中可裝載模塊在內(nèi)核中完成連接)。其他一些變化大致如下:
模塊的后綴及裝載工具;
對于使用模塊的授權(quán)用戶而言,模塊最直觀的改變應是模塊后綴由原先的 .o 文件(即 object)變成了 .ko 文件(即 kernel object)。同時,在 Linux 2.6 中,模塊使用了新的裝卸載工具集 module-init-tools(工具 insmod 和 rmmod 被重新設計)。模塊的構(gòu)建過程改變巨大,在 Linux 2.6 中代碼先被編譯成 .o 文件,再從 .o 文件生成 .ko 文件,構(gòu)建過程會生成如 .mod.c、.mod.o 等文件。
模塊信息的附加過程;
在 Linux 2.6 中,模塊的信息在構(gòu)建時完成了附加;這與 Linux 2.4 不同,先前模塊信息的附加是在模塊裝載到內(nèi)核時進行的(在 Linux 2.4 時,這一過程由工具 insmod 完成)。
模塊的標記選項。
在 Linux 2.6 中,針對管理模塊的選項做了一些調(diào)整,如取消了 can_unload 標記(用于標記模塊的使用狀態(tài)),添加了 CONFIG_MODULE_UNLOAD 標記(用于標記禁止模塊卸載)等。還修改了一些接口函數(shù),如模塊的引用計數(shù)。
圖 1. 模塊在內(nèi)核中完成連接發(fā)展到 Linux 2.6,內(nèi)核中越來越多的功能被模塊化。這是由于可裝載模塊相對內(nèi)核有著易維護,易調(diào)試的特點??裳b載模塊還為內(nèi)核節(jié)省了內(nèi)存空間,因為模塊一般是在真正需要時才被加載。根據(jù)模塊作用,可裝載模塊還可分三大類型:設備驅(qū)動、文件系統(tǒng)和系統(tǒng)調(diào)用。另須指出的是,雖然可裝載模塊是從用戶空間加載到內(nèi)核空間的,但是并非用戶空間的程序。
模塊的版本檢查Linux 的迅速發(fā)展致使相鄰版本的內(nèi)核之間亦存在較大的差異,即在版本補丁號(Patch Level,即內(nèi)核版本號的第四位數(shù))相鄰的內(nèi)核之間。為此 Linux 的開發(fā)者為了保證內(nèi)核的穩(wěn)定,Linux 在加載模塊到內(nèi)核時對模塊采用了版本校驗機制。當被期望加載模塊的系統(tǒng)環(huán)境與模塊的構(gòu)建環(huán)境相左時,通常會出現(xiàn)如清單 1 所示的裝載模塊失敗。
清單 1. 裝載模塊失敗清單 1 中,模塊 hello.ko 構(gòu)建時的環(huán)境與當前系統(tǒng)不一致,導致工具 insmod 在嘗試裝載模塊 hello.ko 到內(nèi)核時失敗。hello.ko 是一個僅使用了函數(shù) printk 的普通模塊(您可在示例源碼中找到文件 hello/hello.c)。我們通過命令 dmesg(或者您也可以查看系統(tǒng)日志文件如 /var/log/messages 等,如果您啟用了這些系統(tǒng)日志的話)獲取模塊裝載失敗的具體原因,模塊 hello.ko 裝載失敗是由于模塊中 module_layout 的導出符號的版本信息與當前內(nèi)核中的不符。函數(shù) module_layout 被定義在內(nèi)核模塊版本選項 MODVERSIONS(即內(nèi)核可裝載模塊的版本校驗選項)之后。清單 2所示為 module_layout 在內(nèi)核文件 kernel/module.c 中的定義。
清單 2. 函數(shù) module_layout正如您所想,函數(shù) module_layout 的第二個參數(shù) ver 存儲了模塊的版本校驗信息。結(jié)構(gòu)體 modversion_info 中保存了用于模塊校驗的 CRC(Cyclic Redundancy Check,即循環(huán)冗余碼校驗)值(見 清單 3)。Linux 對可裝載模塊采取了兩層驗證:模塊的 CRC 值校驗和 vermagic 的檢查。其中模塊 CRC 值校驗針對模塊(內(nèi)核)導出符號,是一種簡單的 ABI(即 Application Binary Interface)一致性檢查,清單 1中模塊 hello.ko 加載失敗的根本原因就是沒有通過 CRC 值校驗(即 module_layout 的 CRC 值與當前內(nèi)核中的不符);而模塊 vermagic(即 Version Magic String)則保存了模塊編譯時的內(nèi)核版本以及 SMP 等配置信息(見 清單 4,模塊 hello.ko 的 vermagic 信息),當模塊 vermagic 與主機信息不相符時亦將終止模塊的加載。
清單 4. 模塊的 vermagic 信息通常,內(nèi)核與模塊的導出符號的 CRC 值被保存在文件 Module.symvers 中,該文件需在開啟內(nèi)核配置選項 CONFIG_MODVERSIONS 之后并完全編譯內(nèi)核獲得(或者您也可在編譯外部模塊后獲得該文件,保存的是模塊的導出符號的 CRC 信息),其信息的保存格式如清單 5 所示。
清單 5. 導出符號的 CRC 值Linux 內(nèi)核在進行模塊裝載時先完成模塊的 CRC 值校驗,再核對 vermagic 中的字符信息,圖 2展示了內(nèi)核中與模塊版本校驗相關(guān)的函數(shù)的調(diào)用過程(分別在函數(shù) setup_load_info 和 check_modinfo 中完成校驗)。Linux 使用 GCC 中的聲明函數(shù)屬性 __attribute__ 完成對模塊的版本信息附加。構(gòu)建的模塊存在幾個 section,如 .modinfo、.gnu.linkonce.this_module 和 __versions 等,這些 ELF 小節(jié)(即 section)保存了模塊校驗所需的信息(關(guān)于這些 section 信息的附加過程,您可查看模塊構(gòu)建時生成的文件 <module>.mod.c 及工具 modpost,見 清單 8和 清單 15)。
圖 2. 模塊的兩層版本校驗過程為了更好的理解可裝載模塊,我們查看內(nèi)核頭文件 include/linux/module.h,它不僅定義了上述中的 struct modversion_info(見 清單 3)還定義了 struct module 等結(jié)構(gòu)體。模塊 CRC 值校驗查看的是就是模塊 __versions 小節(jié)的內(nèi)容,即是附加的 struct modversion_info 信息。模塊的 CRC 校驗過程在函數(shù) setup_load_info 中完成。Linux 使用 .gnu.linkonce.this_module 小節(jié)來解決模塊對 struct module 信息的附加。文件 kernel/module.c 中的函數(shù) check_modinfo 完成了主機與模塊的 vermagic 值的對比(見 清單 6)。清單 6 中函數(shù) get_modinfo 用于獲取內(nèi)核中的 vermagic 信息,模塊 vermagic 信息則被保存在了 ELF 的 .modinfo 小節(jié)中。
清單 6. 函數(shù) check_modinfo須指出的是模塊的 vermagic 信息來自內(nèi)核頭文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了內(nèi)核版本信息(見 清單 7)。與其關(guān)聯(lián)的頭文件 include/generated/utsrelease.h 需經(jīng)內(nèi)核預編譯生成,即通過命令 make 或 make modules_prepare 等。
清單 7. 宏 VERMAGIC_STRING上述中,我們在裝載模塊時使用了工具 insmod。在 Linux 2.6 中,工具 insmod 被重新設計并作為工具集 module-init-tools 中的一個程序,其通過系統(tǒng)調(diào)用 sys_init_module(您可查看頭文件 include/asm-generic/unistd.h)銜接了模塊的版本檢查,模塊的裝載等功能(如 圖 3所示)。module-init-tools 是為 2.6 內(nèi)核設計的運行在 Linux 用戶空間的模塊裝卸載工具集,其包含的程序 rmmod 用于卸載當前內(nèi)核中的模塊。
圖 3. 模塊的裝卸載值得一提的是在 module-init-tools 中可用于模塊裝卸載的程序 modprobe。程序 modprobe 的內(nèi)部函數(shù)調(diào)用過程正如您所想與 insmod 類似,只是其裝載過程會查找一些模塊裝載的配置文件,且 modprobe 在裝載模塊時可解決模塊間的依賴性,即若有必要,程序 modprobe 會在裝載一個模塊時自動加載該模塊依賴的其他模塊。
其他一些細節(jié)從用戶空間裝載模塊到內(nèi)核時,Linux 還對用戶權(quán)限進行了檢查。模塊的裝載須是獲得 CAP_SYS_MODULE 權(quán)限的超級用戶,這正是模塊裝載時最先檢查的內(nèi)容(見 圖 2)。在 Linux 2.6 中,模塊在構(gòu)建時生成了一些臨時文件,如 .o 文件、.mod.o 文件等。了解這些文件的生成有助于我們更好的理解 Linux 2.6 的內(nèi)核模塊構(gòu)建過程以及版本信息的檢查等內(nèi)容。文件 .o 是模塊代碼(即 .c 文件)經(jīng)編譯后獲得的目標文件,文件 .mod.o 則對應文件 .mod.c。文件 <module>.mod.c 是對 <modulue>.c 的擴展,清單 8展示了文件 kobject-example.mod.c 的內(nèi)容 ( 即模塊 kobject-example.ko 的 .mod.c 文件 ),您可見到與模塊版本檢查相關(guān)三個小節(jié)。
清單 8. 文件 kobject-example.mod.c清單 8 中顯示了模塊 kobject-example.ko 中的三個 section 以及宏 MODULE_INFO,最后一行 srcversion 則需開啟內(nèi)核配置選項 MODULE_SRCVERSION_ALL。經(jīng)上述,我們知道這三個 section 正是模塊版本檢查的附加信息。我們通過工具 objdump 查看 .modinfo 小節(jié)(見 清單 9, 即模塊的 vermagic 信息)。<module>.ko 的附加信息合并自文件 <module>.o 與文件 <module>.mod.o。內(nèi)核工具 modpost 完成了一這步驟,且該工具是 Linux 2.6 內(nèi)核模塊構(gòu)建時所必須的。
清單 9. 使用工具 objdump 查看 .modinfo 小節(jié)經(jīng)上述,我們可知內(nèi)核樹的頂層 Makefile 文件包含了內(nèi)核版本的信息,且該信息經(jīng)編譯后被添加到模塊的(頭文件 include/generated/utsrelease.h 保存的內(nèi)核版本信息來自頂層 Makefile)。表 1中,工具 lsmod 打開文件 /proc/modules 查詢當前內(nèi)核中已裝載的模塊(見清單 10),文件 /proc/modules 還被 rmmod 在卸載模塊時使用。另外,若您在裝載模塊 hello.ko 后沒能在終端下看到相應的字符串輸出,則需檢查文件 /proc/sys/kernel/printk,并重設下消息級別。
清單 10. 工具 lsmod 的使用Linux 2.6 構(gòu)建模塊時工具 modpost 被 scripts/Makefile.modpost 調(diào)用,生成 <module>.mod.c 及文件 Module.symvers(見 清單 15)。在開啟內(nèi)核選項 CONFIG_MODVERSIONS 之后,文件 Makefile.Build 會調(diào)用工具 genksyms(現(xiàn)位于內(nèi)核樹 scripts/genksyms 目錄下,在 Linux 2.4 時是模塊工具集 Modutils 的一部分)生成 CRC 信息(見 清單 11)。其中代碼 call cmd_gensymtypes 就是對工具 genksyms 的調(diào)用。另外一個較為明晰的方式是,使用工具 objdump 或 readelf 查看相關(guān)的 ELF 小節(jié),并使用 make – n 查看模塊構(gòu)建過程。
清單 11. 文件 Makefile.Build 的部分內(nèi)容為內(nèi)核構(gòu)建外部模塊前,我們須準備一顆內(nèi)核源碼樹(kernel source tree)。內(nèi)核源碼樹就是一套包含系統(tǒng)配置及內(nèi)核頭文件的內(nèi)核目錄樹。須指出的是 Linux 2.6 的內(nèi)核源碼樹與 2.4 的不同,先前的內(nèi)核只需一套內(nèi)核頭文件就可以了,但在 2.6 的內(nèi)核源碼樹中還需存在一些目標文件及工具,如 scripts/mod/modpost 等。清單 12 所示是從內(nèi)核源碼進行內(nèi)核模塊預編譯以此生成內(nèi)核樹,當然您也可使用 Linux 發(fā)行版的內(nèi)核源碼樹(系統(tǒng)內(nèi)核樹一般存放在 /lib/modules/<kernel version>/build,如果存在的話)。
清單 12. 預編譯內(nèi)核模塊當然,我們最先須根據(jù)主機的硬件信息產(chǎn)生內(nèi)核配置文件 .config。您可使用命令 make menuconfig 或 make config 等來配置與模塊相關(guān)的選項(清單 13與 清單 14相互對應,顯示了模塊相關(guān)的內(nèi)核配置選項)。設置選項 CONFIG_MODULES=y 以及 CONFIG_MODVERSIONS=y 使內(nèi)核支持模塊的版本檢查。另須注意的是,模塊預編譯并不生成 Module.symvers 文件,即使您開啟了 CONFIG_MODVERSIONS 選項。因此最好的方式是完全編譯 Linux 內(nèi)核。
清單 13. 使用 make menuconfig 配置內(nèi)核模塊選項內(nèi)核 2.6 時,我們常為模塊的構(gòu)建編寫一個 Makefile 文件,但仍可使用類似內(nèi)核 2.4 下的模塊構(gòu)建命令。清單 15 展示了外部模塊構(gòu)建的 make 命令,其中 $KDIR 是內(nèi)核樹的絕對路徑,$MDIR 是期望構(gòu)建的模塊的絕對路徑(若是內(nèi)部模塊則可使用 make CONFIG_EXT2_FS=m …)。
清單 15. 構(gòu)建外部模塊雖然本文已盡量集中描述可裝載模塊的版本檢查機制,但是仍然涉及了非常多的內(nèi)容。您需花一些時間來了解這些看似與模塊不相關(guān)的內(nèi)容,如 /proc、/sys 文件系統(tǒng)、ELF(即 Executable and Linkable Format)格式等,以此更好的全面理解內(nèi)核可裝載模塊以及模塊版本版本檢查機制。另外,由于文章討論的是 Linux 2.6 的外部模塊,沒有清晰的講述內(nèi)核的 Kbuild 系統(tǒng)及 Makefile 文件,但這對于模塊亦是重要的內(nèi)容。
https://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/
*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。