歡迎您光臨本站 註冊首頁

最簡單的內核模塊

←手機掃碼閱讀     火星人 @ 2014-03-26 , reply:0

2.1. Hello, World(part 1):最簡單的內核模塊

當第一個洞穴程序員在第一台洞穴計算機的牆上上鑿寫第一個程序時, 這是一個在羚羊皮上輸出`Hello, world'的字元串。羅馬的編程書籍上是以 `Salut, Mundi'這樣的程序開始的。 我不明白人們為什麼要破壞這個傳統,但我認為還是不明白為好。我們將從編寫一系列的`Hello, world'模塊開始, 一步步展示編寫內核模塊的基礎的方方面面。
這可能是一個最簡單的模塊了。先別急著編譯它。我們將在下章模塊編譯的章節介紹相關內容。
----------------------------
Example 2-1. hello-1.c
PLAIN TEXT
1. /*
2. * hello-1.c - The simplest kernel module.
3. */
4. #include /* Needed by all modules */
5. #include /* Needed for KERN_ALERT */
6.
7. int init_module(void)
8. {
9. printk("<1>Hello world 1.\n");
10.
11. /*
12. * A non 0 return means init_module failed; module can't be loaded.
13. */
14. return 0;
15. }
16.
17. void cleanup_module(void)
18. {
19. printk(KERN_ALERT "Goodbye world 1.\n");
20. }
----------------------------
一個內核模塊應該至少包含兩個函數。一個「開始」(初始化)的函數被稱為init_module() 還有一個「結束」 (干一些收尾清理的工作)的函數被稱為cleanup_module() ,當內核模塊被rmmod卸載時被執行。實際上,從內核版本2.3.13開始這種情況有些改變。 你可以為你的開始和結束函數起任意的名字。你將在以後學習如何實現這一點Section 2.3。 實際上,這個新方法時推薦的實現方法。但是,許多人仍然使init_module()和 cleanup_module()作為他們的開始和結束函數。
一般,init_module()要麼向內核註冊它可以處理的事物,要麼用自己的代碼替代某個內核函數(代碼通常這樣做然後再去調用原先的函數代碼)。函數 cleanup_module()應該撤消任何init_module()做的事,從而 內核模塊可以被安全的卸載。
最後,任一個內核模塊需要包含linux/module.h。 我們僅僅需要包含 linux/kernel.h當需要使用 printk()記錄級別的宏擴展時KERN_ALERT,相關內容將在Section 2.1.1中介紹。
--------------------------------------------------------------------------------
2.1.1. 介紹printk()

不管你可能怎麼想,printk()並不是設計用來同用戶交互的,雖然我們在 hello-1就是出於這樣的目的使用它!它實際上是為內核提供日誌功能, 記錄內核信息或用來給出警告。因此,每個printk() 聲明都會帶一個優先順序,就像你看到的<1>和KERN_ALERT 那樣。內核總共定義了八個優先順序的宏,所以你不必使用晦澀的數字代碼,並且你可以從文件 linux/kernel.h查看這些宏和它們的意義。如果你不指明優先順序,默認的優先順序DEFAULT_MESSAGE_LOGLEVEL將被採用。
閱讀一下這些優先順序的宏。頭文件同時也描述了每個優先順序的意義。在實際中, 使用宏而不要使用數字,就像<4>。總是使用宏,就像 KERN_WARNING。
當優先順序低於int console_loglevel,信息將直接列印在你的終端上。如果同時 syslogd和klogd都在運行,信息也同時添加在文件 /var/log/messages,而不管是否顯示在控制台上與否。我們使用像 KERN_ALERT這樣的高優先順序,來確保printk()將信息輸出到 控制台而不是只是添加到日誌文件中。當你編寫真正的實用的模塊時,你應該針對可能遇到的情況使用合 適的優先順序。
--------------------------------------------------------------------------------
2.2. 編譯內核模塊
內核模塊在用gcc編譯時需要使用特定的參數。另外,一些宏同樣需要定義。 這是因為在編譯成可執行文件和內核模塊時, 內核頭文件起的作用是不同的。以往的內核版本需要我們去在Makefile中手動設置這些設定。儘管這些Makefile是按目錄分層次安排的,但是這其中有許多多餘的重複並導致代碼樹大而難以維護。幸運的是,一種稱為kbuild的新方法被引入,現在外部的可載入內核模塊的編譯的方法已經同內核編譯統一起來。想了解更多的編譯非內核代碼樹中的模塊(就像我們將要編寫的)請參考幫助文件linux/Documentation/kbuild/modules.txt。
現在讓我們看一個編譯名字叫做hello-1.c的模塊的簡單的Makefile文件:
Example 2-2. 一個基本的Makefile
----------------------------
obj-m += hello-1.o
----------------------------
現在你可以通過執行命令 make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules 編譯模塊。 你應該得到同下面類似的屏幕輸出:
----------------------------
[root@pcsenonsrv test_module]# make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules
make: Entering directory `/usr/src/linux-2.6.x
CC [M] /root/test_module/hello-1.o
Building modules, stage 2.
MODPOST
CC /root/test_module/hello-1.mod.o
LD [M] /root/test_module/hello-1.ko
make: Leaving directory `/usr/src/linux-2.6.x
----------------------------
請注意2.6的內核現在引入一種新的內核模塊命名規範:內核模塊現在使用.ko的文件後綴(代替以往的.o後綴),這樣內核模塊就可以同普通的目標文件區別開。更詳細的文檔請參考 linux/Documentation/kbuild/makefiles.txt。在研究Makefile之前請確認你已經參考了這些文檔。
現在是使用insmod ./hello-1.ko命令載入該模塊的時候了(忽略任何你看到的關於內核污染的輸出 顯示,我們將在以後介紹相關內容)。 所有已經被載入的內核模塊都羅列在文件/proc/modules中。cat一下這個文件看一下你的模塊是否真的成為內核的一部分了。如果是,祝賀你!你現在已經是內核模塊的作者了。當你的新鮮勁過去后,使用命令 rmmod hello-1.卸載模塊。再看一下/var/log/messages文件的內容是否有相關的日誌內容。
這兒是另一個練習。看到了在聲明 init_module()上的註釋嗎? 改變返回值非零,重新編譯再載入,發生了什麼?
--------------------------------------------------------------------------------
2.3. Hello World (part 2)
在內核Linux 2.4中,你可以為你的模塊的「開始」和「結束」函數起任意的名字。它們不再必須使用 init_module()和cleanup_module()的名字。這可以通過宏 module_init()和module_exit()實現。這些宏在頭文件linux/init.h定義。唯一需要注意的地方是函數必須在宏的使用前定義,否則會有編譯 錯誤。下面就是一個例子。
----------------------------
Example 2-3. hello-2.c
PLAIN TEXT
1. /*
2. * hello-2.c - Demonstrating the module_init() and module_exit() macros.
3. * This is preferred over using init_module() and cleanup_module().
4. */
5. #include /* Needed by all modules */
6. #include /* Needed for KERN_ALERT */
7. #include /* Needed for the macros */
8.
9. static int __init hello_2_init(void)
10. {
11. printk(KERN_ALERT "Hello, world 2\n");
12. return 0;
13. }
14.
15. static void __exit hello_2_exit(void)
16. {
17. printk(KERN_ALERT "Goodbye, world 2\n");
18. }
19.
20. module_init(hello_2_init);
21. module_exit(hello_2_exit);
----------------------------
現在我們已經寫過兩個真正的模塊了。添加編譯另一個模塊的選項十分簡單,如下:
Example 2-4. 兩個內核模塊使用的Makefile
----------------------------
obj-m += hello-1.o
obj-m += hello-2.o
----------------------------
現在讓我們來研究一下linux/drivers/char/Makefile這個實際中的例子。就如同你看到的, 一些被編譯進內核 (obj-y),但是這些obj-m哪裡去了呢?對於熟悉shell腳本的人這不難理解。這些在Makefile中隨處可見的obj-$(CONFIG_FOO)的指令將會在CONFIG_FOO被設置后擴展為你熟悉的obj-y或obj-m。這其實就是你在使用 make menuconfig編譯內核時生成的linux/.config中設置的東西。
--------------------------------------------------------------------------------
2.4. Hello World (part 3): 關於__init和__exit宏

這裡展示了內核2.2以後引入的一個新特性。注意在負責「初始化」和「清理收尾」的函數定義處的變化。宏 __init的使用會在初始化完成後丟棄該函數並收回所佔內存,如果該模塊被編譯進內核,而不是動態載入。
宏__initdata同__init 類似,只不過對變數有效。
宏__exit將忽略「清理收尾」的函數如果該模塊被編譯進內核。同宏 __exit一樣,對動態載入模塊是無效的。這很容易理解。編譯進內核的模塊 是沒有清理收尾工作的, 而動態載入的卻需要自己完成這些工作。
這些宏在頭文件linux/init.h定義,用來釋放內核佔用的內存。 當你在啟動時看到這樣的Freeing unused kernel memory: 236k freed內核輸出,上面的 那些正是內核所釋放的。
----------------------------
Example 2-5. hello-3.c
PLAIN TEXT
1. /*
2. * hello-3.c - Illustrating the __init, __initdata and __exit macros.
3. */
4. #include /* Needed by all modules */
5. #include /* Needed for KERN_ALERT */
6. #include /* Needed for the macros */
7.
8. static int hello3_data __initdata = 3;
9.
10. static int __init hello_3_init(void)
11. {
12. printk(KERN_ALERT "Hello, world %d\n", hello3_data);
13. return 0;
14. }
15.
16. static void __exit hello_3_exit(void)
17. {
18. printk(KERN_ALERT "Goodbye, world 3\n");
19. }
20.
21. module_init(hello_3_init);
22. module_exit(hello_3_exit);
--------------------------------------------------------------------------------
2.5. Hello World (part 4): 內核模塊證書和內核模塊文檔說明

如果你在使用2.4或更新的內核,當你載入你的模塊時,你也許注意到了這些輸出信息:
# insmod hello-3.o
Warning: loading hello-3.o will taint the kernel: no license
See http://www.tux.org/lkml/#export-tainted for information about tainted modules
Hello, world 3
Module hello-3 loaded, with warnings
在2.4或更新的內核中,一種識別代碼是否在GPL許可下發布的機制被引入,因此人們可以在使用非公開的源代碼產品時得到警告。這通過在下一章展示的宏 MODULE_LICENSE()當你設置在GPL證書下發布你的代碼時,你可以取消這些警告。這種證書機制在頭文件linux/module.h 實現,同時還有一些相關文檔信息。
/*
* The following license idents are currently accepted as indicating free
* software modules
*
* "GPL" [GNU Public License v2 or later]
* "GPL v2" [GNU Public License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNU Public License v2
* or BSD license choice]
* "Dual MPL/GPL" [GNU Public License v2
* or Mozilla license choice]
*
* The following other idents are available
*
* "Proprietary" [Non free products]
*
* There are dual licensed components, but when running with Linux it is the
* GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL
* is a GPL combined work.
*
* This exists for several reasons
* 1. So modinfo can show license info for users wanting to vet their setup
* is free
* 2. So the community can ignore bug reports including proprietary modules
* 3. So vendors can do likewise based on their own policies
*/
類似的,宏MODULE_DESCRIPTION()用來描述模塊的用途。 宏MODULE_AUTHOR()用來聲明模塊的作者。宏MODULE_SUPPORTED_DEVICE() 聲明模塊支持的設備。
這些宏都在頭文件linux/module.h定義,並且內核本身並不使用這些宏。它們只是用來提供識別信息,可用工具程序像objdump查看。作為一個練習,使用grep從目錄linux/drivers看一看這些模塊的作者是如何 為他們的模塊提供識別信息和檔案的。
----------------------------
Example 2-6. hello-4.c
PLAIN TEXT
1. /*
2. * hello-4.c - Demonstrates module documentation.
3. */
4. #include
5. #include
6. #include
7. #define DRIVER_AUTHOR "Peter Jay Salzman <p@dirac.org>"
8. #define DRIVER_DESC "A sample driver"
9.
10. static int __init init_hello_4(void)
11. {
12. printk(KERN_ALERT "Hello, world 4\n");
13. return 0;
14. }
15.
16. static void __exit cleanup_hello_4(void)
17. {
18. printk(KERN_ALERT "Goodbye, world 4\n");
19. }
20.
21. module_init(init_hello_4);
22. module_exit(cleanup_hello_4);
23.
24. /*
25. * You can use strings, like this:
26. */
27.
28. /*
29. * Get rid of taint message by declaring code as GPL.
30. */
31. MODULE_LICENSE("GPL");
32.
33. /*
34. * Or with defines, like this:
35. */
36. MODULE_AUTHOR(DRIVER_AUTHOR); /* Who wrote this module? */
37. MODULE_DESCRIPTION(DRIVER_DESC); /* What does this module do */
38.
39. /*
40. * This module uses /dev/testdevice. The MODULE_SUPPORTED_DEVICE macro might
41. * be used in the future to help automatic configuration of modules, but is
42. * currently unused other than for documentation purposes.
43. */
44. MODULE_SUPPORTED_DEVICE("testdevice");
--------------------------------------------------------------------------------
2.6. 從命令行傳遞參數給內核模塊

模塊也可以從命令行獲取參數。但不是通過以前你習慣的argc/argv。
要傳遞參數給模塊,首先將獲取參數值的變數聲明為全局變數。然後使用宏MODULE_PARM()(在頭文件linux/module.h)。運行時,insmod將給變數賦予命令行的參數,如同 ./insmod mymodule.o myvariable=5。為使代碼清晰,變數的聲明和宏都應該放在 模塊代碼的開始部分。以下的代碼範例也許將比我公認差勁的解說更好。
宏MODULE_PARM()需要兩個參數,變數的名字和其類型。支持的類型有" b": 比特型,"h": 短整型, "i": 整數型," l: 長整型和 "s": 字元串型,其中正數型既可為signed也可為unsigned。 字元串類型應該聲明為"char *"這樣insmod就可以為它們分配內存空間。你應該總是為你的變數賦初值。 這是內核編程,代碼要編寫的十分謹慎。舉個例子:
int myint = 3;
char *mystr;
MODULE_PARM(myint, "i");
MODULE_PARM(mystr, "s");
數組同樣被支持。在宏MODULE_PARM中在類型符號前面的整型值意味著一個指定了最大長度的數組。 用'-'隔開的兩個數字則分別意味著最小和最大長度。下面的例子中,就聲明了一個最小長度為2,最大長度為4的整形數組。
int myshortArray[4];
MODULE_PARM (myintArray, "3-9i");
將初始值設為預設使用的IO埠或IO內存是一個不錯的作法。如果這些變數有預設值,則可以進行自動設備檢測, 否則保持當前設置的值。我們將在後續章節解釋清楚相關內容。在這裡我只是演示如何向一個模塊傳遞參數。
最後,還有這樣一個宏,MODULE_PARM_DESC()被用來註解該模塊可以接收的參數。該宏 兩個參數:變數名和一個格式自由的對該變數的描述。
----------------------------
Example 2-7. hello-5.c
PLAIN TEXT
1. /*
2. * hello-5.c - Demonstrates command line argument passing to a module.
3. */
4. #include
5. #include
6. #include
7. #include
8. #include
9.
10. MODULE_LICENSE("GPL");
11. MODULE_AUTHOR("Peter Jay Salzman");
12.
13. static short int myshort = 1;
14. static int myint = 420;
15. static long int mylong = 9999;
16. static char *mystring = "blah";
17.
18. /*
19. * module_param(foo, int, 0000)
20. * The first param is the parameters name
21. * The second param is it's data type
22. * The final argument is the permissions bits,
23. * for exposing parameters in sysfs (if non-zero) at a later stage.
24. */
25.
26. module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
27. MODULE_PARM_DESC(myshort, "A short integer");
28. module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
29. MODULE_PARM_DESC(myint, "An integer");
30. module_param(mylong, long, S_IRUSR);
31. MODULE_PARM_DESC(mylong, "A long integer");
32. module_param(mystring, charp, 0000);
33. MODULE_PARM_DESC(mystring, "A character string");
34.
35. static int __init hello_5_init(void)
36. {
37. printk(KERN_ALERT "Hello, world 5\n=============\n");
38. printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort);
39. printk(KERN_ALERT "myint is an integer: %d\n", myint);
40. printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong);
41. printk(KERN_ALERT "mystring is a string: %s\n", mystring);
42. return 0;
43. }
44.
45. static void __exit hello_5_exit(void)
46. {
47. printk(KERN_ALERT "Goodbye, world 5\n");
48. }
49.
50. module_init(hello_5_init);
51. module_exit(hello_5_exit);
----------------------------
我建議用下面的方法實驗你的模塊:
satan# insmod hello-5.o mystring="bebop" mybyte=255 myintArray=-1
mybyte is an 8 bit integer: 255
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: bebop
myintArray is -1 and 420
satan# rmmod hello-5
Goodbye, world 5
satan# insmod hello-5.o mystring="supercalifragilisticexpialidocious" \
> mybyte=256 myintArray=-1,-1
mybyte is an 8 bit integer: 0
myshort is a short integer: 1
myint is an integer: 20
mylong is a long integer: 9999
mystring is a string: supercalifragilisticexpialidocious
myintArray is -1 and -1
satan# rmmod hello-5
Goodbye, world 5
satan# insmod hello-5.o mylong=hello
hello-5.o: invalid argument syntax for mylong: 'h'
--------------------------------------------------------------------------------
2.7. 由多個文件構成的內核模塊

有時將模塊的源代碼分為幾個文件是一個明智的選擇。在這種情況下,你需要:
只要在一個源文件中添加#define __NO_VERSION__預處理命令。 這很重要因為module.h通常包含 kernel_version的定義,此時一個存儲著內核版本的全局變數將會被編譯。但如果此時你又要包含頭文件 version.h,你必須手動包含它,因為 module.h不會再包含它如果打開預處理選項__NO_VERSION__。
像通常一樣編譯。
將所有的目標文件連接為一個文件。在x86平台下,使用命令ld -m elf_i386 -r -o <1st src file.o> <2nd src file.o>。
此時Makefile一如既往會幫我們完成編譯和連接的臟活。
這裡是這樣的一個模塊範例。
----------------------------
Example 2-8. start.c
PLAIN TEXT
1. /*
2. * start.c - Illustration of multi filed modules
3. */
4.
5. #include /* We're doing kernel work */
6. #include /* Specifically, a module */
7.
8. int init_module(void)
9. {
10. printk("Hello, world - this is the kernel speaking\n");
11. return 0;
12. }
----------------------------
另一個文件:
----------------------------
Example 2-9. stop.c
PLAIN TEXT
1. /*
2. * stop.c - Illustration of multi filed modules
3. */
4.
5. #include /* We're doing kernel work */
6. #include /* Specifically, a module */
7.
8. void cleanup_module()
9. {
10. printk("<1>Short is the life of a kernel module\n");
11. }
----------------------------
最後是該模塊的Makefile:
Example 2-10. Makefile
----------------------------
obj-m += hello-1.o
obj-m += hello-2.o
obj-m += hello-3.o
obj-m += hello-4.o
obj-m += hello-5.o
obj-m += startstop.o
startstop-objs := start.o stop.o
--------------------------------------------------------------------------------
2.8. 為已編譯的內核編譯模塊
很顯然,我們強烈推薦你編譯一個新的內核,這樣你就可以打開內核中一些有用的排錯功能,像強制卸載模塊(MODULE_FORCE_UNLOAD):當該選項被打開時,你可以rmmod -f module強制內核卸載一個模塊,即使內核認為這是不安全的。該選項可以為你節省不少開發時間。
但是,你仍然有許多使用一個正在運行中的已編譯的內核的理由。例如,你沒有編譯和安裝新內核的許可權,或者你不希望重啟你的機器來運行新內核。 如果你可以毫無阻礙的編譯和使用一個新的內核,你可以跳過剩下的內容,權當是一個腳註。
如果你僅僅是安裝了一個新的內核代碼樹並用它來編譯你的模塊,當你載入你的模塊時,你很可能會得到下面的錯誤提示:
insmod: error inserting 'poet_atkm.ko': -1 Invalid module format
一些不那麼神秘的信息被紀錄在文件/var/log/messages中;
Jun 4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686
REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'
換句話說,內核拒絕載入你的模塊因為記載版本號的字元串不符(更確切的說是版本印戳)。版本印戳作為一個靜態的字元串存在於內核模塊中,以 vermagic:。 版本信息是在連接階段從文件init/vermagic.o中獲得的。查看版本印戳和其它在模塊中的一些字元信息,可以使用下面的命令 modinfo module.ko:
----------------------------
[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko
license: GPL
author: Peter Jay Salzman
description: A sample driver
vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3
depends:
----------------------------
我們可以藉助選項--force-vermagic解決該問題,但這種方法有潛在的危險,所以在成熟的模塊中也是不可接受的。 解決方法是我們構建一個同我們預先編譯好的內核完全相同的編譯環境。如何具體實現將是該章後面的內容。
首先,準備同你目前的內核版本完全一致的內核代碼樹。然後,找到你的當前內核的編譯配置文件。通常它可以在路徑 /boot下找到,使用像config-2.6.x的文件名。你可以直接將它拷貝到內核代碼樹的路徑下:
cp /boot/config-`uname -r` /usr/src/linux-`uname -r`/.config
讓我們再次注意一下先前的錯誤信息:仔細看的話你會發現,即使使用完全相同的配置文件,版本印戳還是有細小的差異的,但這足以導致模塊載入的失敗。這其中的差異就是在模塊中出現卻不在內核中出現的custom字元串,是由某些發行版提供的修改過的 makefile導致的。檢查/usr/src/linux/Makefile,確保下面這些特定的版本信息同你使用的內核完全一致:
VERSION = 2
PATCHLEVEL = 6
SUBLEVEL = 5
EXTRAVERSION = -1.358custom
...
像上面的情況你就需要將EXTRAVERSION一項改為-1.358。我們的建議是將原始的makefile備份在 /lib/modules/2.6.5-1.358/build下。 一個簡單的命令cp /lib/modules/`uname -r`/build/Makefile /usr/src/linux-`uname -r`即可。另外,如果你已經在運行一個由上面的錯誤的Makefile編譯的內核,你應該重新執行 make,或直接對應/lib/modules/2.6.x/build/include/linux/version.h從文件 /usr/src/linux-2.6.x/include/linux/version.h修改UTS_RELEASE,或用前者覆蓋後者的。
現在,請執行make來更新設置和版本相關的頭文件,目標文件:
[root@pcsenonsrv linux-2.6.x]# make
CHK include/linux/version.h
UPD include/linux/version.h
SYMLINK include/asm -> include/asm-i386
SPLIT include/linux/autoconf.h -> include/config/*
HOSTCC scripts/basic/fixdep
HOSTCC scripts/basic/split-include
HOSTCC scripts/basic/docproc
HOSTCC scripts/conmakehash
HOSTCC scripts/kallsyms
CC scripts/empty.o
...
如果你不是確實想編譯一個內核,你可以在SPLIT后通過按下CTRL-C中止編譯過程。因為此時你需要的文件 已經就緒了。現在你可以返回你的模塊目錄然後編譯載入它:此時模塊將完全針對你的當前內核編譯,載入時也不會由任何錯誤提示。

[火星人 ] 最簡單的內核模塊已經有698次圍觀

http://coctec.com/docs/linux/show-post-189522.html