解析 Linux 內核可裝載模塊的版本檢查機制

轉自:http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/linux

 

爲保持 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 的外部模塊進行討論的。ide

可裝載模塊在 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 標記(用於標記禁止模塊卸載)等。還修改了一些接口函數,如模塊的引用計數。post

圖 1. 模塊在內核中完成鏈接

圖 1. 模塊在內核中完成鏈接

發展到 Linux 2.6,內核中愈來愈多的功能被模塊化。這是因爲可裝載模塊相對內核有着易維護,易調試的特色。可裝載模塊還爲內核節省了內存空間,由於模塊通常是在真正須要時才被加載。根據模塊做用,可裝載模塊還可分三大類型:設備驅動、文件系統和系統調用。另須指出的是,雖然可裝載模塊是從用戶空間加載到內核空間的,可是並不是用戶空間的程序。測試

 

模塊的版本檢查

Linux 的迅速發展導致相鄰版本的內核之間亦存在較大的差別,即在版本補丁號(Patch Level,即內核版本號的第四位數)相鄰的內核之間。爲此 Linux 的開發者爲了保證內核的穩定,Linux 在加載模塊到內核時對模塊採用了版本校驗機制。當被指望加載模塊的系統環境與模塊的構建環境相左時,一般會出現如清單 1 所示的裝載模塊失敗。this

清單 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. 模塊的兩層版本校驗過程

圖 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
 

模塊的裝載與卸載

上述中,咱們在裝載模塊時使用了工具 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. 模塊的裝卸載

圖 3. 模塊的裝卸載

表 1. 工具集 module-init-tools 中的部分程序
名稱 說明
insmod 裝載模塊到當前運行的內核中
rmmod 從當前運行的內核中卸載模塊
lsmod 顯示當前內核已加裝的模塊信息
modinfo 檢查與內核模塊相關聯的目標文件,並打印出全部獲得的信息
modprobe 利用 depmod 建立的依賴關係文件自動加載相關的模塊
depmod 建立一個內核可裝載模塊的依賴關係文件,modprobe 用它來自動加載模塊

 

值得一提的是在 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
 # cat ./kobject/kobject-example.mod.c 
 ... 

 MODULE_INFO(vermagic, VERMAGIC_STRING); 

 struct module __this_module 
 __attribute__((section(".gnu.linkonce.this_module"))) = { 
 ... 
 }; 

 static const struct modversion_info ____versions[] 
 __used 
 __attribute__((section("__versions"))) = { 
 ... 
 }; 

 static const char __module_depends[] 
 __used 
 __attribute__((section(".modinfo"))) = 
"depends="; 

 MODULE_INFO(srcversion, "B06F9B8B7AB52AEED247B9F");

清單 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 小節
 # objdump --section=.modinfo -s hello/hello.o 

 hello/hello.o:     file format elf64-x86-64 

 Contents of section .modinfo: 
 0000 6c696365 6e73653d 4475616c 20425344  license=Dual BSD 
 0010 2f47504c 00                          /GPL. 

 # modinfo hello/hello.o 
 filename:       hello/hello.o 
 license:        Dual BSD/GPL 

 # objdump --section=.modinfo -s hello/hello.mod.o 
 ... 
 # objdump --section=.modinfo -s hello/hello.ko 
 ...

經上述,咱們可知內核樹的頂層 Makefile 文件包含了內核版本的信息,且該信息經編譯後被添加到模塊的(頭文件 include/generated/utsrelease.h 保存的內核版本信息來自頂層 Makefile)。表 1中,工具 lsmod 打開文件 /proc/modules 查詢當前內核中已裝載的模塊(見清單 10),文件 /proc/modules 還被 rmmod 在卸載模塊時使用。另外,若您在裝載模塊 hello.ko 後沒能在終端下看到相應的字符串輸出,則需檢查文件 /proc/sys/kernel/printk,並重設下消息級別。

清單 10. 工具 lsmod 的使用
 # insmod ./kobject/kobject-example.ko 

 # ls /sys/kernel/kobject_example/ 
 bar  baz  foo 

 # lsmod | grep kobject 
 kobject_example        12857  0 

 # cat /proc/modules | grep kobject 
 kobject_example 12857 0 - Live 0xffffffffa0523000

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 的部份內容
 ifndef CONFIG_MODVERSIONS 
 cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $< 
 else 
 cmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $< 
 cmd_modversions =              \ 
  if $(OBJDUMP) -h $(@D)/.tmp_$(@F) | grep -q __ksymtab; then  \ 
    $(call cmd_gensymtypes, $(KBUILD_SYMTYPES))    \ 
        > $(@D)/.tmp_$(@F:.o=.ver);      \ 
                  \ 
    $(LD) $(LDFLAGS) -r -o $@ $(@D)/.tmp_$(@F)     \ 
      -T $(@D)/.tmp_$(@F:.o=.ver);      \ 
    rm -f $(@D)/.tmp_$(@F) $(@D)/.tmp_$(@F:.o=.ver);  \ 
  else                \ 
    mv -f $(@D)/.tmp_$(@F) $@;        \ 
  fi; 
 endif
 

模塊的構建與測試

爲內核構建外部模塊前,咱們須準備一顆內核源碼樹(kernel source tree)。內核源碼樹就是一套包含系統配置及內核頭文件的內核目錄樹。須指出的是 Linux 2.6 的內核源碼樹與 2.4 的不一樣,先前的內核只需一套內核頭文件就能夠了,但在 2.6 的內核源碼樹中還需存在一些目標文件及工具,如 scripts/mod/modpost 等。清單 12 所示是從內核源碼進行內核模塊預編譯以今生成內核樹,固然您也可以使用 Linux 發行版的內核源碼樹(系統內核樹通常存放在 /lib/modules/<kernel version>/build,若是存在的話)。

清單 12. 預編譯內核模塊
 # make menuconfig 
 # make modules_prepare 
 #

固然,咱們最早須根據主機的硬件信息產生內核配置文件 .config。您可以使用命令 make menuconfig 或 make config 等來配置與模塊相關的選項(清單 13與 清單 14相互對應,顯示了模塊相關的內核配置選項)。設置選項 CONFIG_MODULES=y 以及 CONFIG_MODVERSIONS=y 使內核支持模塊的版本檢查。另須注意的是,模塊預編譯並不生成 Module.symvers 文件,即便您開啓了 CONFIG_MODVERSIONS 選項。所以最好的方式是徹底編譯 Linux 內核。

清單 13. 使用 make menuconfig 配置內核模塊選項
 # make menuconfig 
 --- Enable loadable module support 
    [ ]   Forced module loading 
    [*]   Module unloading 
    [ ]     Forced module unloading 
    [*]   Module versioning support 
    [*]   Source checksum for all modules
清單 14. 模塊相關的內核配置選項
 CONFIG_MODULES 
 CONFIG_MODULE_FORCE_LOAD 
 CONFIG_MODULE_UNLOAD 
 CONFIG_MODULE_FORCE_UNLOAD 
 CONFIG_MODVERSIONS 
 CONFIG_MODULE_SRCVERSION_ALL

內核 2.6 時,咱們常爲模塊的構建編寫一個 Makefile 文件,但仍可以使用相似內核 2.4 下的模塊構建命令。清單 15 展現了外部模塊構建的 make 命令,其中 $KDIR 是內核樹的絕對路徑,$MDIR 是指望構建的模塊的絕對路徑(如果內部模塊則可以使用 make CONFIG_EXT2_FS=m …)。

清單 15. 構建外部模塊
 # make -C $KDIR M=$MDIR [target] 

 # make -C /lib/modules/2.6.38-10-generic/build M=$PWD/hello  modules 
 make: Entering directory `/usr/src/linux-headers-2.6.38-10-generic'
 CC [M] /home/harris/work/samples/hello/hello.o 
 Building modules, stage 2. 
 MODPOST 1 modules 
 CC /home/harris/work/samples/hello/hello.mod.o 
 LD [M] /home/harris/work/samples/hello/hello.ko 
 make: Leaving directory `/usr/src/linux-headers-2.6.38-10-generic'
 

結束語

雖然本文已儘可能集中描述可裝載模塊的版本檢查機制,可是仍然涉及了很是多的內容。您需花一些時間來了解這些看似與模塊不相關的內容,如 /proc、/sys 文件系統、ELF(即 Executable and Linkable Format)格式等,以此更好的全面理解內核可裝載模塊以及模塊版本版本檢查機制。另外,因爲文章討論的是 Linux 2.6 的外部模塊,沒有清晰的講述內核的 Kbuild 系統及 Makefile 文件,但這對於模塊亦是重要的內容。

相關文章
相關標籤/搜索