2012年4月5日 星期四

什是module 以及如何寫一個module

本文出自:http://os.silversand.net 作者: 莊榮城 (2001-08-31 08:00:00)
不知道在什時候,Linux 出現了 module 這種東西,的確,它是 Linux 的一大革新。有了 module 之,寫 device driver 不再是一項惡夢,修改 kernel 也不再是一件痛苦的事了。因為你不需要每次要測試 driver 就重新 compile kernel 一次。那簡直是會累死人。Module 可以允許我們動態的改變 kernel,加載 device driver,而且它也能縮短我們 driver development 的時間。在這篇文章裡,我將要跟各位介紹一下 module 的原理,以及如何寫一個 module。

module 翻譯成中文就是模塊,不過,事實上去翻譯這個字一點都沒意義。在講模塊之前,我先舉一個例子。相信很多人都用過 RedHat。在 RedHat 裡,我們可以執行 sndconfig,它可以幫我們 config 聲卡。config 完之如果捉得到你的聲卡,那你的聲卡馬上就可以動了,而且還不用重新激活計算機。這是怎做的呢 ? 就是靠module。module 其實是一般的程序。但是它可以被動態載到 kernel 裡成為 kernel的一部分。載到 kernel 裡的 module 它具有跟 kernel 一樣的權力。可以 access 任何 kernel 的 data structure。你聽過 kdebug 嗎 ? 它是用來 debug kernel 的。它就是先將它本身的一個 module 載到 kernel 裡,而在 user space 的 gdb 就可以經由跟這個 module 溝通,得知 kernel 裡的 data structure 的值,除此之外,還可以經由載到 kernel 的 module 去更改 kernel 裡 data structure。

我們知道,在寫 C 程序的時候,一個程序只能有一個 main。Kernel 本身其實也是一個程序,它本身也有個 main,叫 start_kernel()。當我們把一個 module 載到 kernel 裡的時候,它會跟 kernel 整合在一起,成為 kernel 的一部分。請各位想想,那 module 可以有 main 嗎 ? 答案很明顯的,是 No。理由很簡單。一個程序只能有一個 main。在使用 module 時,有一點要記住的是 module 是處被動的角色。它是提供某些功能讓別人去使用的。

Kernel 裡有一個變量叫 module_list,每當 user 將一個 module 載到 kernel 裡的時候,這個 module 就會被記錄在 module_list 裡面。當 kernel 要使用到這個 module 提供的 function 時,它就會去 search 這個 list,找到 module,然再使用其提供的 function 或 variable。每一個 module 都可以 export 一些 function 或變量來讓別人使用。除此之外,module 也可以使用已經載到 kernel 裡的 module 提供的 function。這種情形叫做 module stack。比方說,module A 用到 module B 的東西,那在加載 module A 之前必須要先加載 module B。否則 module A 會無法加載。除了 module 會 export 東西之外,kernel 本身也會 export 一些 function 或 variable。同樣的,module 也可以使用 kernel 所 export 出來的東西。由大家平時都是撰寫 user space 的程序,所以,當突然去寫 module 的時候,會把平時寫程序用的 function 拿到 module 裡使用。像是 printf 之類的東西。我要告訴各位的是,module 所使用的 function 或 variable,要嘛就是自己寫在 module 裡,要嘛就是別的 module 提供的,再不就是 kernel 所提供的。你不能使用一般 libc 或 glibc所提供的 function。像 printf 之類的東西。這一點可能是各位要多小心的地方。(也許你可以先 link 好,再載到 kernel,我好象試過,但是忘了)

剛才我們說到 kernel 本身會 export 出一些 function 或 variable 來讓 module 使用,但是,我們不是萬能的,我們怎知道 kernel 有開放那裡東西讓我們使用呢 ? Linux 提供一個 command,叫 ksyms,你只要執行 ksyms -a 就可以知道 kernel 或目前載到 kernel 裡的 module 提供了那些 function 或 variable。底下是我的系統的情形:

c0216ba0 drive_info_R744aa133
c01e4a44 boot_cpu_data_R660bd466
c01e4ac0 EISA_bus_R7413793a
c01e4ac4 MCA_bus_Rf48a2c4c
c010cc34 __verify_write_R203afbeb
. . . . .

在 kernel 裡,有一個 symbol table 是用來記錄 export 出去的 function 或 variable。除此之外,也會記錄著那個 module export 那些 function。上面幾行中,表示 kernel 提供了 drive_info 這個 function/variable。所以,我們可以在 kernel 裡直接使用它,等載到 kernel 裡時,會自動做好 link 的動作。由此,我們可以知道,module 本身其實是還沒做 link 的一些 object code。一切都要等到 module 被加載 kernel 之,link 才會完成。各位應該可以看到 drive_info 面還接著一些奇怪的字符串。_R744aa133,這個字符串是根據目前 kernel 的版本再做些 encode 得出來的結果。為什額外需要這一個字符串呢 ?

Linux 不知道從那個版本以來,就多了一個 config 的選項,叫做 Set version number in symbols of module。這是為了避免對系統造成不穩定。我們知道 Linux 的 kernel 更新的很快。在 kernel 更新的過程,有時為了效率起見,會對某些舊有的 data structure 或 function 做些改變,而且一變可能有的 variable 被拿掉,有的 function 的 prototype 跟原來的都不太一樣。如果這種情形發生的時候,那可能以前 2.0.33 版本的 module 拿到 2.2.1 版本的 kernel 使用,假設原來 module 使用了 2.0.33 kernel 提供的變量叫 A,但是到了 2.2.1 由某些原因必須把 A 都設成 NULL。那當此 module 用在 2.2.1 kernel 上時,如果它沒去檢查 A 的值就直接使用的話,就會造成系統的錯誤。也許不會整個系統都死掉,但是這個 module 肯定是很難發揮它的功能。為了這個原因,Linux 就在 compile module 時,把 kernel 版本的號碼 encode 到各個 exported function 和 variable 裡。

所以,剛才也許我們不應該講 kernel 提供了 drive_info,而應該說 kernel 提供了 driver_info_R744aa133 來讓我們使用。這樣也許各位會比較明白。也就是說,kernel 認為它提供的 driver_info_R744aa133 這個東西,而不是 driver_info。所以,我們可以發現有的人在加載 module 時,系統都一直告訴你某個 function 無法 resolved。這就是因為 kernel 裡沒有你要的 function,要不然就是你的 module 裡使用的 function 跟 kernel encode 的結果不一樣。所以無法 resolve。解決方式,要嘛就是將 kernel 裡的 set version 選項關掉,要嘛就是將 module compile 成 kernel 有辦法接受的型式。

那有人就會想說,如果 kernel 認定它提供的 function 名字叫做 driver_info_R744aa133 的話,那我們寫程序時,是不是用到這個 funnction 的地方都改成 driver_info_R744aa133 就可以了。答案是 Yes。但是,如果每個 function 都要你這樣寫,你不會覺得很煩嗎 ? 比方說,我們在寫 driver 時,很多人都會用到 printk 這個 function。這是 kernel 所提供的 function。它的功能跟 printf 很像。用法也幾乎都一樣。是 debug 時很好用的東西。如果我們 module 裡用了一百次 printk,那是不是我們也要打一百次的 printk_Rdd132261 呢 ? 當然不是,聰明的人馬上會想到用 #define printk printk_Rdd132261 就好了嘛。所以,Linux 很體貼的幫我們做了這件事。

如果各位的系統有將 set version 的選項打開的話,那大家可以到 /usr/src/linux/include/linux/modules 這個目錄底下。這個目錄底下有所多的 ..ver檔案。這些檔案其實就是用來做 #define 用的。我們來看看 ksyms.ver 這個檔案裡,裡面有一行是這樣子的 :

#define printk _set_ver(printk)

set_ver 是一個 macro,就是用來在 printk 面加上 version number 的。有興趣的朋友可以自行去觀看這個 macro 的寫法。用了這些 ver ,我們就可以在 module 裡直接使用 printk 這樣的名字了。而這些 ver 檔會自動幫我們做好 #define 的動作。可是,我們可以發現這個目錄有很多很多的 ver 。有時候,我們怎知道我們要呼叫的 function 是在那個 ver 檔裡有定義呢 ? Linux 又幫我們做了一件事。/usr/src/linux/include/linux/modversions.h 這個檔案已經將全部的 ver 檔都加進來了。所以在我們的 module 裡只要 include 這個檔,那名字的問題都解決了。但是,在此,我們奉勸各位一件事,不要將 modversions.h 這個檔在 module 裡 include 進來,如果真的要,那也要加上以下數行:

#ifdef MODVERSIONS
#include
#endif

加入這三行的原因是,避免這個 module 在沒有設定 kernel version 的系統上,將 modversions.h 這個檔案 include 進來。各位可以去試試看,當你把 set version 的選項關掉時,modversions.h 和 modules 這個目錄都會不見。如果沒有上面三行,那 compile 就不會過關。所以一般來講,modversions.h 我們會選擇在 compile 時傳給 gcc 使用。就像下面這個樣子。

gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS main.c \
-include usr/src/linux/include/linux/modversions.h

在這個 command line 裡,我們看到了 -D__KERNEL__,這是說要定義 __KERNEL__ 這個 constant。很多跟 kernel 有關的 header file,都必須要定義這個 constant 才能 include 的。所以建議你最好將它定義起來。另外還有一個 -DMODVERSIONS。這個 constant 我剛才忘了講。剛才我們說要解決 fucntion 或 variable 名字 encode 的方式就是要 include modversions.h,其實除此之外,你還必須定義 MODVERSIONS 這個 constant。再來就是 MODULE 這個 constant。其實,只要是你要寫 module 就一定要定義這個變量。而且你還要 include module.h 這個檔案,因為 _set_ver 就是定義在這裡的。

講到這裡,相信各位應該對 module 有一些認識了,以遇到 module unresolved 應該不會感到困惑了,應該也有辦法解決了。

剛才講的都是使用別人的 function 上遇到的名字 encode 問題。但是,如果我們自己的 module 想要 export 一些東西讓別的 module 使用呢。很簡單。在 default 上,在你的 module 裡所有的 global variable 和 function 都會被認定為你要 export 出去的。所以,如果你的 module 裡有 10 個 global variable,經由 ksyms,你可以發現這十個 variable 都會被 export 出去。這當然是個很方便的事啦,但是,你知道,有時候我們根本不想把所有的 variable 都 export 出去,萬一有個 module 沒事亂改我們的 variable 怎辦呢 ? 所以,在很多時候,我們都只會限定幾個必要的東西 export 出去。在 2.2.1 之前的 kernel (不是很確定) 可以利用 register_symtab 來幫我們。但是,現在更新的版本早就出來了。所以,在此,我會介紹 kernel 2.2.1 裡所提供的。kernel 2.2.1 裡提供了一個 macro,叫做 EXPORT_SYMBOL,這是用來幫我們選擇要 export 的 variable 或 function。比方說,我要 export 一個叫 full 的 variable,那我只要在 module 裡寫:

EXPORT_SYMBOL(full);

就會自動將 full export 出去,你馬上就可以從 ksyms 裡發現有 full 這個變量被 export 出去。在使用 EXPORT_SYMBOL 之前,要小心一件事,就是必須在 gcc 裡定義 EXPORT_SYMTAB 這個 constant,否則在 compile 時會發生 parser error。所以,要使用 EXPORT_SYMBOL 的話,那 gcc 應該要下:

gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS -DEXPORT_SYMTAB \
main.c -include /usr/src/linux/include/linux/modversions.h

如果我們不想 export 任何的東西,那我們只要在 module 裡下

EXPORT_NO_SYMBOLS;

就可以了。使用 EXPORT_NO_SYMBOLS 用不著定義任何的 constant。其實,如果各位使用過舊版的 register_symbol 的話,一定會覺得新版的方式比較好用。至少我是這樣覺得啦。因為使用 register_symbol 還要先定義出自己的 symbol_table,感覺有點麻煩。

當我們使用 EXPORT_SYMBOL 把一些 function 或 variable export 出來之,我們使用 ksyma -a 去看一些結果。我們發現 EXPORT_SYMBOL(full) 的確是把 full export出來了 :

c8822200 full [my_module]
c01b8e08 pci_find_slot_R454463b5
. . .

但是,結果怎跟我們想象中的不太一樣,照理說,應該是 full_Rxxxxxx 之類的東西才對啊,怎才出現 full 而已呢 ? 奇怪,問題在那裡呢 ?

其實,問題就在我們沒有對本身的 module 所 export 出來的 function 或 variable 的名字做 encode。想想,如果在 module 的開頭。我們加入一行

#define full full_Rxxxxxx

之,我們再重新 compile module 一次,載到 kernel 之,就可以發現 ksyms -a 顯示的是

c8822200 full_Rxxxxxx [my_module]
c01b8e08 pci_find_slot_R454463b5
. . . . .

了。那是不是說,我們要去對每一個 export 出來的 variable 和 function 做 define 的動作呢 ? 當然不是。記得嗎,前頭我們講去使用 kernel export 的 function 時,由 include 了一些 .ver 的檔案,以致我們不用再做 define 的動作。現在,我們也要利用 .ver 的檔案來幫我們,使我們 module export 出來的 function 也可以自動加入 kernel version 的 information。也就是變成 full_Rxxxxxx 之類的東西。

Linux 裡提供了一個 command,叫 genksyms,就是用來幫我們產生這種 .ver 的檔案的。它會從 stdin 裡讀取 source code,然檢查 source code 裡是否有 export 的 variable 或 function。如果有,它就會自動為每個 export 出來的東西產生一些 define。這些 define 就是我們之前說的。等我們有了這些 define 之,只要在我們的 module 裡加入這些 define,那 export 出來的 function 或 variable 就會變成上面那個樣子。

假設我們的程序都放在一個叫 main.c 的檔案裡,我們可以使用下列的方式產生這些 define。

gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 > main.ver

gcc 的 -E 參數是指將 preprocessing 的結果 show 出來。也就是說將它 include 的檔案,一些 define 的結果都展開。-D__GENKSYMS__ 是一定要的。如果沒有定義這個 constant,你將不會看到任何的結果。用一個管線是因為 genksyms 是從 stdin 讀資料的,所以,經由管線將 gcc 的結果傳給 genksyms。-k 2.2.1 是指目前使用的 kernel 版本是 2.2.1,如果你的 kernel 版本不一樣,必須指定你的 kernel 的版本。產生的 define 將會被放到 main.ver 裡。產生完 main.ver 檔之,在 main.c 裡將它 include 進來,那一切就 OK 了。有件事要告訴各位的是,使用這個方式產生的 module,其 export 出來的東西會經由 main.ver 的 define 改頭換面。所以如果你要讓別人使用,那你必須將 main.ver 公開,不然,別人就沒辦法使用你 export 出來的東西了。

講了這多,相信各位應該都已經比較清楚 module 在 kernel 中是怎樣一回事,也應該知道為什有時候 module 會無法加載了。除此之外,各位應該還知道如何使自己 module export 出來的東西也具有 kernel version 的 information。

接下來,要跟各位講的就是,如何寫一個 module 了。其實,寫一個 module 很簡單的。如果你了解我上面所說的東西。那我再講一次,再用個例子,相信大家就都會了。要寫一個 module,必須要提供兩個 function。這兩個 function 是給 insmod 和 rmmod 使用的。它們分別是 init_module(),以及 cleanup_module()。

int init_module();
void cleanup_module();

相信大家都知道在 Linux 裡可以使用 insmod 這個 command 來將某個 module 加載。比方說,我有一個 module 叫 hello.o,那使用 insmod hello.o 就可以將 hello 這個 module 載到 kernel 裡。觀察 /etc/modules 應該就可以看到 hello 這個 module 的名字。如果要將 hello 這個 module 移除,則只要使用 rmmod hello 就可以了。insmod 在加載 module 之,就會去呼叫 module 所提供的 init_module()。如果傳回 0 表示成功,那 module 就會被加載。如果失敗,那加載的動作就會失敗。一般來講,我們在 init_module() 做的事都是一些初始化的工作。比方說,你的 module 需要一塊內存,那你就可以在 init_module() 做 kmalloc 的動作。想當然爾。cleanup_module() 就是在 module 要移除的時候做的事。做的事一般來講就是一些善的工作,比方像把之前 kmalloc 的內存 free 掉。

由 module 是載到 kernel 使用的,所以,可能別的 module 會使用你的 module,甚至某些 process 也會使用到你的 module,為了避免 module 還有人使用時就被移除,每個 module 都有一個 use count。用來記錄目前有多少個 process 或 module 正在使用這個 module。當 module 的 use count 不等 0 時,module 是不會被移除掉的。也就是說,當 module 的 use count 不等 0 時,cleanup_module() 是不會被呼叫的。

在此,我要介紹三個 macro,是跟 module 的 use count 有關的。

MOD_INC_USE_COUNT
MOD_DEC_USE_COUNT
MOD_IN_USE

MOD_INC_USE_COUNT 是用來增加 module 的 use count,而 MOD_DEC_USE_COUNT 是用來減少 module 的 use count。至 MOD_IN_USE 則是用來檢查目前這個 module 是不是被使用中。也就是檢查 use count 是否為 0。module 的 use count 必須由寫 module 的人自己來 maintain。系統並不會自動為你把 use count 加一或減一。一切都得由自己控制。下面有一個例子,但是,並不會介紹這三個 macro 的使用方法。將來如果有機會,我再來介紹這三個 macro 的用法。

這個例子很簡單。其實只是示范如何使用 init_module() 以及 cleanup_module() 來寫一個 module。當然,這兩個 function 只是構成 module 的基本條件罷了。至 module 裡要提供的功能則是看各人的需要。

main.c
#define MODULE
#include
#include
int full;
EXPORT_SYMBOL(full); /* 將 full export 出去 */
int init_module( void )
{
printk( "<5> Module is loaded\n" );
return 0;
}
void cleanup_module( void )
{
printk( "<5> Module is unloaded\n" );
}

關 printk 是這樣子的,它是 kernel 所提供的一個打印訊息的 function。kernel 有 export 這個 function。所以你可以自由的使用它。它的用法跟 printf 幾乎一模一樣。唯獨訊息的開頭是 <5>,其實,不見得這三個字符啦。也可以是 <4>,<3>,<7> 等等的東西。這是代表這個訊息的 prioirty 或 level。<5> 表示的是跟 KERNEL 有關的訊息。

main.ver:

利用 genksyms 產生出來的。

gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 > main.ver

接下來,就是要把 main.c compile 成 main.o

gcc -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -c \
-I/usr/src/linux/include/linux -include \
/usr/src/linux/include/linux/modversions.h \
-include ./main.ver main.c

好了。main.o 已經成功的 compile 出來了,現在下一個 command,

insmod main.o

檢查看 /proc/modules 裡是否有 main 這個 module。如果有,表示 main 這個 module 已經載到 kernel 了。再下一個指令,看看 full export 出去的結果。

ksyms

結果顯示

Address Symbol Defined by
c40220e0 full_R355b84b2 [main]
c401d04c ne_probe [ne]
c401a04c ei_open [8390]
c401a094 ei_close [8390]
c401a504 ei_interrupt [8390]
c401af1c ethdev_init [8390]
c401af80 NS8390_init [8390]

可以看到 full_R355b84b2,表示,我們已經成功的將 full 的名字加上 kernel version 的 information 了。當我們不需要這個 module 時,我們就可以下一個 command,

rmmod main

這樣 main 就會被移除掉了。再檢查看看 /proc/modules 就可以發現 main 那一行不見了。各位現在可以看一下 /var/log/message 這個檔案,應該可以發現以兩行

Apr 12 14:19:05 host kernel: Module is loaded
Apr 12 14:39:29 host kernel: Module is unloaded

這兩行就是 printk 印出來的。

關 module 的介紹已經到此告一段落了。其實,使用 module 實在是很簡單的一件事。對要發展 driver 或是增加 kernel 某些新功能的人來講,用 module 不啻為一個方便的方式。希望這篇文章對各位能有所幫助。


莊榮城 (J.C. Chuang), cjc86@cs.ccu.edu.tw

沒有留言:

張貼留言

DNSSEC安全技術簡介 作者:游子興 / 臺灣大學計算機及資訊網路中心網路組約聘幹事 DNS 是一套已經廣泛使用的Internet 服務,但因先天的技術限制導致容易成為駭客攻擊的目標。本文主要在介紹DNSSEC 之緣起與技術背景,及其使用的加解密技術如何確保資料的完整...