Linux 動態載入module介紹

前言

提到Linux中的Device Driver各位對於Module一定不陌生。Linux中關於Device Driver的寫作,在2.0.x2.1.x版的Kernel之間有些微的不同,各位可以在”Linux Device Drivers”這本書中,得到不少寫作上的資訊。
本文希望可以為各位介紹Linux Module載入系統的過程,及Linux Kernel2.0.x2.1.x版之間,Kernel載入Device Driver的不同處。文章的最後,則是著墨於module stack的機制。
雖然筆者盡力使這篇文章內容無誤,如果仍有筆者疏忽之處,還希望Linux界的前輩們可以給予筆者指教。撰寫本文的原意即是在讓更多人了解Linux的系統運作,希望可以為各位盡一份心力,謝謝。
Module 的檔案格式
Linux中,我們常見到的檔案格式為ELF(Executable and Linkable Format)。由下圖(),為執行檔的ELF Header,可以由e_type來得知此檔為”Executable File”。若由圖(),來看moduleELF Header,可以看到e_type”Relocatable File”

(),執行檔的ELF header


(),目的檔的ELF header
其實,”Relocatable File”即為ELF目的檔(Object File)的格式。也因此在module中包含了許多尚未經過連結的Symbol。讀者如果有在Linux中透過”insmod”載入module的話,想必偶爾會遇到unresolved symbol的問題。因為,每當module在載入到系統時,它會進行動態連結的動作,把此時在module中還未解決的symbol,透過動態連結的方式,與目前存在系統中的symbol做連結,如果所要求的symbol並不存在此時系統中,便會發生unresolved symbol的錯誤訊息。
當然囉,一旦新的module載入到系統後,新的module中所包含的symbol,亦會加入目前kernel所擁有的symbol中。此後,我們若要載入其它的module時,便多了上回module所載入的symbol可供我們連結。
我們可以透過 nm這個程式來查看module中尚未連結的symbol,如下圖()為執行 ”nm msdos.o” 的結果



(),透過nm察看msdos.o的結果
我們可以看到 “printk_R2gig_1b7d4074” 前面有一個符號 “U”,這即表示printk這個symbol尚未經過連結。而這些將透過動態載入的過程中,一一解決。我們可以透過/proc/ksyms來得知目前已存在kernelsymbol,如果我們所要載入的module含有目前kernel不存在的symble,則“unresolved symbol”的錯誤,就會發生囉….^_^
此外,“printk_R2gig_1b7d4074”中,函式”printk”之後的” R2gig_1b7d4074”字串,為Linux解決kernel 版本問題,而在各kernel symbol之後附加的32位元的CRC(Cyclic Redundancy Code)。當module載入到系統時,insmod會去比對所載入module使用的symbol CRC值是否與目前kernel所提供的CRC值一致。兩者如果一樣的話,表示此函式與載入module所要呼叫的函式相同,並未有版本相容的問題。除了CRC值的確認外,亦可透過取得module中所紀錄的 ”kernel_version”,與目前的kernel版本做比較。有關這部分的說明,讀者可以參考本文有關 ”insmod” 的運作說明。
2.0.x 2.1.x以後的Module的差異
2.0.x版本的Linux中,主要是透過一個User Mode的 ”Kerneld” Daemon,處理來自Kernel要求載入module的工作,並經由執行 “modprobe” 指令載入所需的Module。到了2.1.x以後的版本,這份工作則交由 “kmod” 這個Kernel Mode程式,透過產生一個kernel thread來執行”modprobe”指令載入module
其實,筆者也曾質疑為何要把kernel 2.0.xUser ModeKerneld,換成kernel 2.1.xKernel Modekmod。後來,我在Linux Source Code “/Document/kmod.txt” 檔案中找到了我想要的資料,也算是給了我一個還算滿意的答案。以下就是這份文件的部份內容:
(1)kerneld used SysV IPC, which can now be made into a module. Besides,
SysV IPC is ugly and should therefore be avoided (well, certainly for
kernel level stuff)
(雖然 kerneld 所使用的 SysV IPC 已經可以在 module 中直接呼叫,
不過,SysV IPC 的設計仍不夠完美,應盡量避免在 kernel level 中使用。 )
(2)both kmod and kerneld end up doing the same thing (calling modprobe),
so why not skip the middle man?
(kmodkerneld最後都會去呼叫modprobe,為何不跳過kerneld這個中間人呢?)
(3) removing kerneld related stuff from ipc/msg.c made it 40% smaller
(移除kerneld相關的程式,使得 ipc/msg.c這個檔案小了40%)
(4) kmod reports errors through the normal kernel mechanisms, which avoids
the chicken and egg problem of kerneld and modular Unix domain sockets
(kmod會透過kernel的函式來回報錯誤,可以避免kerneld引發的雞生蛋,蛋生雞的問題。)
lsmod在相舊版本的不同
指令 “lsmod” ,在Linux中可以用來查看目前有哪些module已載入到系統中。如下圖():

(),透過lsmod指令,列出已載入的module
kernel 2.0.x時,指令”lsmod”是去開啟檔案 “/proc/modules” 來得知系統中,已載入哪些Module。不過到了kernel 2.1.x以後,系統提供了函式” query_module”。因此,此時”lsmod”的實作便是透過呼叫query_module來取得系統已載入module的相關資料。有關lsmod的實作,可以參考modutils-2.1.85中的lsmod.c
既然提到了kernel 2.1.x之後所提供的函式”query_module”,我們以/arch/i386/kernel/entry.s這檔案來比較kernel 2.0.352.2.12sys_call_table的不同。其中,2.0.35共有0—166System Call (亦即 80 號中斷的服務),而2.2.12則有0—190System Call,其中筆者所提到的函式”query_module”則為第167個函式。在Linux中,System Call隨版本的更新,不斷的擴充。筆者隨著各版本的更新使用至今,實在是很敬佩那群為Linux付出貢獻的前輩們。

 
   
.long SYMBOL_NAME(sys_sched_get_priority_min) /* 160 */
.long SYMBOL_NAME(sys_sched_rr_get_interval)
.long SYMBOL_NAME(sys_nanosleep)
.long SYMBOL_NAME(sys_mremap)
.long SYMBOL_NAME(sys_setresuid)
.long SYMBOL_NAME(sys_getresuid) /* 165 */
.long SYMBOL_NAME(sys_vm86)
.long SYMBOL_NAME(sys_query_module)
.long SYMBOL_NAME(sys_poll)
.long SYMBOL_NAME(sys_nfsservctl)
.long SYMBOL_NAME(sys_setresgid) /* 170 */
.long SYMBOL_NAME(sys_getresgid)
.long SYMBOL_NAME(sys_prctl)
.long SYMBOL_NAME(sys_rt_sigreturn)
.long SYMBOL_NAME(sys_rt_sigaction)
























Kerneld的運作
在此,筆者將先介紹在kernel 2.0.x中所使用的 “kerneld”。在Linux的文件計劃中(LDP),有一篇很不錯的文章 “kerneld HowTo”,讀者們若是希望更多方面的了解kerneld,倒是可以從那篇文章中學到不少有用的資訊。
首先,由下圖()我們可以看到kerneld程式運作的簡圖,kerneld在啟動時會把自己初始化為一個user modedaemon,在程式一切準備就緒後,便開始接收系統所發出的IPC(Internal Process Comunication) Message。若kernel要把某個module載入系統時,便會透過發出IPC Message的方式,來通知kerneld載入該module。它所能辨識的Message Type有數種,我在圖()中僅列出其中的兩種。
如果收到的Message Type為 KERNELD_REQUEST_MODULE,則透過外部呼叫,執行 “modprobe”這隻程式,來把所要求的module載入系統中。若收到的Message TypeKERNELD_RELEASE_MODULE,亦透過”modprobe”來移除 module

()kerneld的流程
筆者在kernel 2.0.35版本Source Code的檔案“/Drivers/Block/md.c”中,找到函式 ”static int do_md_run (int minor, int repart)”,它實作了以下的程式碼:

 
   
if (!pers[pnum])
{
#ifdef CONFIG_KERNELD
char module_name[80];
sprintf (module_name, “md-personality-%d”, pnum);
request_module (module_name);
if (!pers[pnum])
#endif
return -EINVAL;
}


















request_module便是在kernel mode中要求載入module時,所呼叫的函式,之後在/include/linux/kerneld.h我們可以得知此函式的內容如下:

 
   
/*
* Request that a module should be loaded.
* Wait for the exit status from insmod/modprobe.
* If it fails, it fails… at least we tried…
*/
static inline int request_module(const char *name)
{
return kerneld_send(KERNELD_REQUEST_MODULE,0 | KERNELD_WAIT, strlen(name), name, NULL);
}























此函式會呼叫kerneld_send來傳送Messagekerneld daemon,以完成載入module的動作。在 “/IPC/msg.c” 裡, 函式“kerneld_send”的實作,有以下這樣一段程式碼:

 
   
int kerneld_send(int msgtype, int ret_size, int msgsz,const char *text, const char *ret_val)
{
。。。。。。。。。。。。。。。。。。
status = real_msgsnd(kerneld_msqid, (struct msgbuf *)&kmsp, msgsz, msgflg);
if ((status >= 0) && (ret_size & KERNELD_WAIT))
{
ret_size &= ~KERNELD_WAIT;
kmsp.text = (char *)ret_val;
status = real_msgrcv(kerneld_msqid, (struct msgbuf *)&kmsp,
KDHDR + ((ret_val)?ret_size:0),
kmsp.id, msgflg);
if (status > 0) /* a valid answer contains at least a long */
status = kmsp.id;
。。。。。。。。。。。。。。。
}























在檔案 “/ipc/msg.c” 中的real_msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg)函式,可透過IPCkernel mode中,所要求的Message傳給在User Modekerneld daemon,而在User Modekerneld 則透過函式msgrcv()來取得kernel所送過來的Message,再來載入或移除kernel所要求的Module
緊接著,筆者簡單介紹kernel 2.0.x “kerneld” 的運作之後。我將以kernel 2.1.x之後的 ”kmod” ,作為我們所要討論的主題。在這版本中,kernel mode要載入module時,無須再透過user modedaemon來間接處理,將以在kernel中直接呼叫執行 ”modprobe” 的方式,來簡化整個流程。
Kmod的運作
我們可以在/kernel/kmod.c中看到kmod.c中看包括3個主要的函式,
(1) use_init_file_context(void)
(2) exec_modprobe(void * module_name)
(3) request_module(const char * module_name)
kernel 2.1.x之後,當kernel需要載入某個module時,在kmod的機制下,無須由kernel發出IPC Messageuser mode kerneld,可以經由kernel mode中函式request_module(),來達成直接執行 ”modprobe”載入module的目的。
request_module()中,會呼叫函式 ”kernel_thread()” 產生kernel thread來執行函式 “exec_modprobe()”,在函式 ”exec_modprobe()” 中則透過函式 ”execve()” 以便在kernel mode裡外部執行modprobe。而modprobe_path儲存了modprobe在系統中的路徑。
看到這我們不難發現,兩種不同kernel載入module的機制,不過最終都是透過外部執行 ”modprobe” 來得以順利的載入modulekmod的建立,便是為了讓這整個流程可以更有效率的運作。
如圖(),為kmod運作的一個簡要流程圖。其中有一段在函式 ”use_init_file_context()” 的文字是我不甚瞭解的部分,由於沒有很深的體會,對此我也不敢妄加評論,因此我把它列在圖中,讓各位作一個參考。


()kmod的流程
如同我們在介紹kerneld時,所舉的例子 “/drivers/block/md.c”,其中的函式 ”do_md_run()” 在新版的kernel中,動態載入module的部份程式碼如下:

 
   
if (!pers[pnum])
{
#ifdef CONFIG_KMOD
char module_name[80];
sprintf (module_name, “md-personality-%d”, pnum);
request_module (module_name);
if (!pers[pnum])
#endif
return -EINVAL;
}

















其實,同樣都是呼叫函式“request_module”,只是函式request_module內部的運作已跟過去版本的kernel有所不同囉!
Modprobe

之前我們看到kernel中載入module,到了後來都會去執行modprobe這個程式來載入module,因此在這我們便進一步的來探討modprobe的運作情形。
如圖()


()modprobe流程
modprobe會根據所要載入的module是否有用到其他的module而作必要的載入。如上圖()所示,modprobe當察知所要載入的module 有用到目前尚未載入module的函式時,便會透過函式 ”system()” 呼叫insmod來載入該module
如下圖()所示,當我們透過指令 ”insmod”載入msdos.o時,由於msdos.o使用到了fat.o的函式,因此在insmod載入過程中會發生”unresolved symbol”的錯誤,

()
當然囉!如果我們如下圖(),透過insmod先把fat.o載入後,再把msdos.o載入系統中,就不會發生之前的錯誤了。不過這樣做對使用者而言,畢竟是有些麻煩,尤其是面對一個不熟悉的module,若又同時用到了許多目前尚未載入的module時,那使用者可是會手忙腳亂了。

()
因此,透過modprobe我們可以簡便的解決這類 ”module stack” 的問題(稍後會針對這點討論)。如下圖(),我們可以在系統中並沒有fat.o載入的情況下,透過modprobe載入msdos.o,透過之前說過的modprobe運作流程,我們可以知道此時它會去察覺目前載入的module,是否有呼叫到其他module的函式,及他們是否已被載入到系統中,進而循序的透過呼叫 “insmod” 來載入各個所需的module,而幫我們自動把這些問題給處理掉。


()
有關Module的資訊
首先,我們可以在/proc/ksyms中看到如下圖(十一)的資訊,ksyms中所提供的資訊包括kernel中為 ”EXPORT_SYMBOL()” symbol,以及系統目前已載入module所提供的symbol。在下圖中,我們可以看到各函式後方被框起來的部分便是提供該symbolmodule 名稱。每個函式後方的編號,是該函式版本的32位元CRC資料,用來確保函式呼叫的版本正確性。例如,函式ABCD()2.1版的kernel時有3的引數,如:

 
   
ABCD(int A,int B,int C)










不過到了2.2版時,函式ABCD()經過修改後引數成為4個,如:

 
   
ABCD(int A,int B,int C,int D)










透過這類型的檢查,便可以避免這些不必要的錯誤。


(十一)
由下圖(十二),為此時系統中所載入的module列表。我們與圖(十一)比較時,可以看到在/proc/ksyms中,函式後方白線所框起來的module name,為目前系統中所存在的module

(十二)
以下為在/kernel/ksyms.c中的部分程式列表,我們可以看到被以EXPORT_SYMBOL()處理的函式名稱。

 
   
EXPORT_SYMBOL(panic);
EXPORT_SYMBOL(printk);
EXPORT_SYMBOL(sprintf);
EXPORT_SYMBOL(vsprintf);
EXPORT_SYMBOL(kdevname);
EXPORT_SYMBOL(bdevname);
EXPORT_SYMBOL(cdevname);
EXPORT_SYMBOL(simple_strtoul);
EXPORT_SYMBOL(system_utsname); /* UTS data */
EXPORT_SYMBOL(uts_sem); /* UTS semaphore */
EXPORT_SYMBOL(sys_call_table);
EXPORT_SYMBOL(machine_restart);
EXPORT_SYMBOL(machine_halt);




















Insmod Module
我以2.1.85版本的insmod.c來做一個說明,當我們執行此一個命令時”insmod ModuleA”,它會去執行以下的流程
(1) 取得ModuleA的檔案位置, 可透過函式search_module_path(),或由使用者指定位置。
(2) 取得檔案位置後,由fopen()開檔。開檔成功後,則由obj_load()ModuleA載入,對Module不熟悉的讀者可能會想說為何是obj_load()其實,ModuleLinux中是以Obj檔的形式存在,在載入到系統後,再與Kernel各部份連結,而成為Kernel的一部份。
(3) 透過函式get_kernel_version()取得Kernel版本資訊。在取得Module版本資訊時,會先透過new_get_module_version(),如果傳回錯誤,則再透過old_get_module_version()取得,如此可使載入Module的動作,相容於兩個版本的Linux環境。
(4) 比對Kernel 版本及由Module取出的版本是否一致,若有設 ”Force loading” (ex: insmod -f),則不論Kernel版本是否一致,都將繼續執行。否則,便結束載入的動作。
(5) 接著執行 ”query_module(NULL, 0, NULL, 0, NULL)” 來得知是否為支援query_module 系統函式的Kernel。若是,則上述呼叫將傳回0,否則傳回非0值。(ex:有關sys_query_module 函式的Source請參考\kernel\module.c,在System Call Table的位置可參考\arch\i386\kernel\entry.s,得知可透過Int 80h ax=A7h呼叫)
(6) 在得知Kernel新舊版本後,則分別執行” new_get_kernel_symbols()new_is_kernel_checksummed()” 及 “old_get_kernel_symbols()old_is_kernel_checksummed()”。我們以新版本的new_get_kernel_symbols()來說明,如下圖


(十三)
呼叫new_get_kernel_symbols()後。接著呼叫new_is_kernel_checksummed(),這個函式會尋找Symbol Name”Using_Versions”kernel symble,並把該值傳回給k_crcs
(7) 接著取得我們所要載入的moduleusing_checksums欄位的值,存入m_crcs。比對KernelModule CRC值是否吻合。

 
   
if (m_crcs != k_crcs)
obj_set_symbol_compare(f, ncv_strcmp, ncv_symbol_hash);











(8) 接著呼叫add_kernel_symbols,把載入的module中未定義的symbol,與目前kernel及已載入的modulesymbol做連結。

(十四)
呼叫new_create_this_module old_create_mod_use_count 依不同的版本建立此ModuleSymbol Table String Table…….等。
(9) 呼叫obj_check_undefineds,確認並未定義過同一個Module
(10)obj_allocate_commons建立Common Symbol
(11)把載入Module時使用者所加入的引數傳給Module,同樣亦包含了new_process_module_argumentsold_process_module_arguments兩個新舊不同的版本。
(12)呼叫new_create_module_ksymtab建立新的ModuleSymbol Table
(13)透過obj_load_size取得Module最終的大小,並透過create_module函式把Module真正建立起來,此時create_module會傳回一個記憶體位址,此記憶體位址會在下一步用到。
(14)呼叫obj_relocateModule重新更動到呼叫create_module時,所傳回來的記憶體位址。
(15)呼叫new_init_moduleold_init_module 去初始化該Module
(16)最後檢查如果發生錯誤,則透過函式delete_moduleModule移除。

Module Stack

當然囉,ModuleReference到的Symbol也是有可能存在於其它的Module中,若A Module使用了 B Module的函式,當A Module載入到系統時,若B Module此時不存在系統中,便會發生unresolved symbol的錯誤。因此要解決Module Stack所引發的問題,我們可以在insmod A Module前,先把B Module載入到系統中。
或是可以透過modprobe這個程式,來得知A Module 有參考到的Symbol,並自動的幫我們把 B Module載入到系統中。
我覺得有一篇不錯的文章 Loadable Kernel Modules”(請參閱參考資料3),其中對Module Stack有很不錯的說明,如果讀者對這部份的有興趣的話,可以進一步去取的這方面的資料,以下我針對這個部份做一個簡短的說明,希望可以對各位有一些幫助。
如下圖(十五)module Y呼叫了module X所提供的函式,則module Y->deps會指向一個module_ref結構(可參考圖(十九))module_ref結構有三個成員,分別為:
(1)dep:存放被參考module的位址。
(2)ref:存放參考者module的位址。
(3)next_ref:存放下一個在dep欄位中,有同一個被參考module位址的module_ref位址。
由於module X函式被module Y呼叫,所以module X->refs會指向圖(十五)中的module_ref結構。而module Y因為呼叫了其它module的函式,本身並未被其它的module所呼叫,所以module Y->refsNULL。相同的,module X函式被module Y所呼叫,本身並沒有呼叫其它的module,所以module X->depsNULL
接著,我們看圖中的module_ref結構。由於module Y呼叫module X的函式,而需參考圖(十五)中的module_ref,所以module_ref->ref指向module Y。相同的,module_ref依賴module X來提供資訊給module Y,所以module_ref->dep指向module X。最後,由於這是單純module Y呼叫module X函式的module stack,並沒有同一個module函式被一個以上的module呼叫。所以,在圖(十五)中的module_ref->next_refNULL

(十五)module Y呼叫了module X的函式
再來,我們以三個module的例子來做說明。由圖(十五)進一步擴充,如下圖(十六),在載入module Xmodule Y後,接著載入module Zmodule Z同時呼叫了module Xmodule Y的函式。
首先,module Z呼叫module Y的函式,所以module Z->deps指向module_ref Amodule Z並沒有被其它module所呼叫,故module Z->refsNULL。而module Y函式被module Z呼叫,module Y->refs指向module_ref A。如同圖(十五)例子的說明,module_ref A->ref指向module Z,而module_ref A->dep指向module Y
接著,我們再看呼叫module X函式的情形。module Zmodule Y都呼叫了module X的函式,在圖(十六)中我們可以看到module_ref B所在位置為module_ref A之後,而module_ref B->ref指向module Zmodule Z透過參考module_ref B來取得module X資訊。而module_ref B->dep指向module X,值得注意的是module_ref Bnext_ref指向module_ref C。因為module Zmodule Y共同呼叫了module X的函式,所以這兩個module_ref亦建立了關係。在module_ref C中,module_ref C->ref指向module Y,而 module_ref C->dep指向module Xmodule_ref C->next_refNULL。由於module Y呼叫了module X的函式,所以在圖(十六)中,module Y->deps指向module_ref C

(十六)moduleZ呼叫了module Ymodule X的函式

(十九)module_refstruct
結語
最後,讀者可以由以下圖(二十)與圖(二十一)了解兩種載入module機制的不同。筆者在寫這篇文章的過程中收穫實在不少。有許多自己不曾注意過的細節,為了能夠清楚的表達,我都盡可能的自己去走一遍。最後能夠有機會把我的心得與各位分享,真的很開心,希望各位有任何意見的話,可以寫信與我聯絡。
(二十)kerneld載入driver的流程圖


(二十一)kmod載入driver的流程圖
參考資料
1,Alessandro Rubini,”Dynamic Kernels:Modularized Device Driver,” Linux Journal,Issue 23,Mar. 1996
2,Alessandro Rubini,”Dynamic Kernels:Discovery”,Linux Journal,Issue 24,Apr. 1996
3,Juan-Mariano de Goyeneche and Elena Apolinario Fernandez de Sousa,”Loadable Kernel Modules”,IEEE Software,January/February 1999
4,ALESSANDRO RUBINI,”LINUX DEVICE DRIVER”,O’REILLY’,1998
5,Linux 2.0.35 and 2.2.12 kernel source code
//