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











