簡介: 為保持 Linux 內核的穩定與可持續發展,內核在發展過程中引進了可裝載模塊這一特性。內核可裝載模塊就是可在內核運行時加載到內核的一組代碼。通常 , 我們會在兩個版本不同的內核上裝載同一模塊失敗,即使是在兩個相鄰的補丁級(Patch Level)版本上。這是因為內核在引入可裝載模塊的同時,對模塊采取了版本信息校驗。這是一個與模塊代碼無關,卻與內核相連的機制。該校驗機制保證了內核裝載的模塊是用戶認可的,且安全的。本文將從內核模塊發布者的角度思考模塊版本檢查機制,並從開發者與授權 root 用戶的角度去使用及理解該機制。
內核可裝載模塊概述
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. 裝載模塊失敗
# insmod ./hello/hello.ko insmod: error inserting './hello/hello.ko': -1 Invalid module format # dmesg | grep hello [ 9206.599843] hello: disagrees about version of symbol module_layout
清單 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
/* kernel/module.c */ #ifdef CONFIG_MODVERSIONS void module_layout(struct module *mod, struct modversion_info *ver, struct kernel_param *kp, struct kernel_symbol *ks, struct tracepoint * const *tp) { } EXPORT_SYMBOL(module_layout); #endif
清單 3. 結構體 modversion_info
/* include/linux/module.h */ struct modversion_info { unsigned long crc; char name[MODULE_NAME_LEN]; };
正如您所想,函數 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 信息
# uname – r 2.6.38-10-generic # modinfo ./hello/hello.ko filename: ./hello/hello.ko license: Dual BSD/GPL srcversion: 31FE72DA6A560C890FF9B3F depends: vermagic: 2.6.38-9-generic SMP mod_unload modversions
通常,內核與模塊的導出符號的 CRC 值被保存在文件 Module.symvers 中,該文件需在開啟內核配置選項 CONFIG_MODVERSIONS 之後並完全編譯內核獲得(或者您也可在編譯外部模塊後獲得該文件,保存的是模塊的導出符號的 CRC 信息),其信息的保存格式如清單 5 所示。
清單 5. 導出符號的 CRC 值
0x1de386dd module_layout vmlinux EXPORT_SYMBOL <CRC> <Symbol> <module>
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
/* kernel/module.c */ static int check_modinfo(struct module *mod, struct load_info *info) { const char *modmagic = get_modinfo(info, "vermagic"); ... } else if (!same_magic(modmagic, vermagic, info->index.vers)) { ... } ... return 0; }
操作系統須負責程序的獨立操作並保護資源不受非法訪問,而這一功能在現代 CPU 中以設計不同的操作模式(級別)來實現。內核運行在 CPU 的最高級別,即內核態���也被稱為超級用戶態;而應用程序則運行在最低級別,即用戶態。由此系統內存在 Linux 中可分為兩個不同的區域:內核空間與用戶空間。模塊運行在內核空間裡,而應用程序則運行在對應的用戶空間中。
須指出的是模塊的 vermagic 信息來自內核頭文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了內核版本信息(見 清單 7)。與其關聯的頭文件 include/generated/utsrelease.h 需經內核預編譯生成,即通過命令 make 或 make modules_prepare 等。
清單 7. 宏 VERMAGIC_STRING
/* kernel/module.c */ static const char vermagic[] = VERMAGIC_STRING; /* include/linux/vermagic.h */ #define VERMAGIC_STRING \ UTS_RELEASE " " \ MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \ MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \ MODULE_ARCH_VERMAGICLINE