1. 程式人生 > >Linux內核模塊編程與內核模塊LICENSE -《具體解釋(第3版)》預讀

Linux內核模塊編程與內核模塊LICENSE -《具體解釋(第3版)》預讀

dev sdn hack 認識 方式 flags sin arr 整數

Linux內核模塊簡單介紹

Linux內核的總體結構已經很龐大,而其包括的組件或許多。我們如何把須要的部分都包括在內核中呢?
一種方法是把全部須要的功能都編譯到Linux內核。這會導致兩個問題。一是生成的內核會很大,二是假設我們要在現有的內核中新增或刪除功能,將不得不又一次編譯內核。


有沒有一種機制使得編譯出的內核本身並不須要包括全部功能,而在這些功能須要被使用的時候,其相應的代碼被動態地載入到內核中呢?
Linux提供了這樣的一種機制,這樣的機制被稱為模塊(Module)。模塊具有這樣的特點。

  1. 模塊本身不被編譯入內核映像,從而控制了內核的大小。

  2. 模塊一旦被載入,它就和內核中的其它部分全然一樣。
為了使讀者建立對模塊的初步感性認識,我們先來看一個最簡單的內核模塊“Hello World”,如代碼清單4.1所看到的。


代碼清單4.1 一個最簡單的Linux內核模塊

01 /*
02  * a simple kernel module: hello
03  *
04  * Copyright (C) 2014 Barry Song  ([email protected])
05  *
06  * Licensed under GPLv2 or later.
07  */
08
09 #include <linux/init.h>
10 #include <linux/module.h>
11
12 static int __init hello_init(void)
13 {
14     printk(KERN_INFO "Hello World enter\n");
15     return 0;
16 }
17 module_init(hello_init);
18
19 static void __exit hello_exit(void)
20 {
21     printk(KERN_INFO "Hello World exit\n ");
22 }
23 module_exit(hello_exit);
24
25 MODULE_AUTHOR("Barry Song <[email protected]
/* */>"); 26 MODULE_LICENSE("GPL v2"); 27 MODULE_DESCRIPTION("A simple Hello World Module"); 28 MODULE_ALIAS("a simplest module");
這個最簡單的內核模塊僅僅包括內核模塊載入函數、卸載函數和對GPL v2許可權限的聲明以及一些描寫敘述信息。位於本書配套源碼的/kernel/drivers/hello文件夾。編譯它會產生hello.ko目標文件,通過“insmod ./hello.ko”命令能夠載入它,通過“rmmod hello”命令能夠卸載它,載入時輸出“Hello World enter”,卸載時輸出“Hello World exit”。


內核模塊中用於輸出的函數是內核空間的printk()而非用戶空間的printf()。printk()的使用方法和printf()基本類似,但前者可定義輸出級別。printk()可作為一種最主要的內核調試手段,在Linux驅動的調試章節中將具體解說這個函數。
在Linux中。使用lsmod命令能夠獲得系統中載入了的全部模塊以及模塊間的依賴關系,比如:

Module                  Size  Used by
hello                   9 472  0
nls_iso8859_1          12 032  1
nls_cp437              13 696  1
vfat                   18 816  1
fat                    57 376  1 vfat
...

lsmod命令實際上讀取並分析“/proc/modules”文件。與上述lsmod命令結果相應的“/proc/modules”文件例如以下:
$ cat /proc/modules
hello 12393 0 - Live 0xe67a2000 (OF)
nls_utf8 12493 1 - Live 0xe678e000
isofs 39596 1 - Live 0xe677f000
vboxsf 42561 2 - Live 0xe6767000 (OF)
...
內核中已載入模塊的信息也存在於/sys/module文件夾下。載入hello.ko後,內核中將包括/sys/module/hello文件夾。該文件夾下又包括一個refcnt文件和一個sections文件夾,在/sys/module/hello文件夾下執行“tree –a”得到例如以下文件夾樹:
[email protected]:/sys/module/hello# tree -a
.
├── coresize
├── holders
├── initsize
├── initstate
├── notes
│   └── .note.gnu.build-id
├── refcnt
├── sections
│   ├── .exit.text
│   ├── .gnu.linkonce.this_module
│   ├── .init.text
│   ├── .note.gnu.build-id
│   ├── .rodata.str1.1
│   ├── .strtab
│   └── .symtab
├── srcversion
├── taint
└── uevent

3 directories, 15 files

modprobe命令比insmod命令要強大。它在載入某模塊時,會同一時候載入該模塊所依賴的其它模塊。

使用modprobe命令載入的模塊若以“modprobe -r filename”的方式卸載將同一時候卸載其依賴的模塊。模塊之間的依賴關系上存放在根文件系統的/lib/modules/<kernel-version>/modules.dep文件裏,實際上是在總體編譯內核的時候由depmod工具生成的,它的格式很easy:

kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

使用modinfo <模塊名>命令能夠獲得模塊的信息,包括模塊作者、模塊的說明、模塊所支持的參數以及vermagic:
# modinfo hello.ko
filename:       hello.ko
alias:          a simplest module
description:    A simple Hello World Module
license:        GPL v2
author:         Barry Song <[email protected]>
srcversion:     081230411494509792BD4A3
depends:        
vermagic:       3.8.0-39-generic SMP mod_unload modversions 686

Linux內核模塊程序結構

一個Linux內核模塊主要由例如以下幾個部分組成。
(1)模塊載入函數
當通過insmod或modprobe命令載入內核模塊時。模塊的載入函數會自己主動被內核執行,完畢本模塊的相關初始化工作。
(2)模塊卸載函數
當通過rmmod命令卸載某模塊時,模塊的卸載函數會自己主動被內核執行,完畢與模塊卸載函數相反的功能。
(3)模塊許可證聲明
許可證(LICENSE)聲明描寫敘述內核模塊的許可權限,假設不聲明LICENSE,模塊被載入時,將收到內核被汙染 (kernel tainted)的警告。
在Linux內核模塊領域。可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”和“Proprietary”(關於模塊能否夠採用非GPL許可權如“Proprietary”,這個在學術界和法律界都有爭議)。
大多數情況下,內核模塊應遵循GPL兼容許可權。

Linux內核模塊最常見的是以MODULE_LICENSE( "GPL v2" )語句聲明模塊採用GPL v2。
(4)模塊參數(可選)。
模塊參數是模塊被載入的時候能夠被傳遞給它的值,它本身相應模塊內部的全局變量。
(5)模塊導出符號(可選)。
內核模塊能夠導出符號(symbol。相應於函數或變量),這樣其它模塊能夠使用本模塊中的變量或函數。
(6)模塊作者等信息聲明(可選)。

模塊載入函數

Linux內核模塊載入函數一般以_ _init標識聲明,典型的模塊載入函數的形式如代碼清單4.2所看到的。


代碼清單4.2 內核模塊載入函數

1     static int _ _init initialization_function(void)
2     {    
3         /* 初始化代碼 */
4     }
5     module_init(initialization_function);

模塊載入函數則以“module_init(函數名)”的形式被指定。它返回整型值,若初始化成功,應返回0。而在初始化失敗時,應該返回錯誤編碼。

在Linux內核裏,錯誤編碼是一個接近於0的負值,在<linux/errno.h>中定義。包括-ENODEV、-ENOMEM之類的符號值。

總是返回相應的錯誤編碼是種很好的習慣。由於僅僅有這樣,用戶程序才幹夠利用perror等方法把它們轉換成有意義的錯誤信息字符串。
在Linux內核中,能夠使用request_module(const char *fmt, …)函數載入內核模塊,驅動開發者能夠通過調用
request_module(module_name);
這樣的靈活的方式載入其它內核模塊。


在Linux中,全部標識為_ _init的函數假設直接編譯進入內核,成為內核鏡像的一部分,在連接的時候都放在.init.text這個區段內。
#define _ _init _ _attribute_ _ ((_ _section_ _ (".init.text")))
全部的_ _init函數在區段.initcall.init中還保存了一份函數指針,在初始化時內核會通過這些函數指針調用這些_ _init函數,並在初始化完畢後,釋放init區段(包括.init.text、.initcall.init等)的內存。
除了函數以外,數據也能夠被定義為_ _initdata,對於僅僅是初始化階段須要的數據,內核在初始化完後,也能夠釋放它們占用的內存。比如,以下的代碼中將hello_data定義為__initdata。

static int hello_data __initdata = 1;

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, world %d\n", hello_data);
    return 0;
}
module_init(hello_init);

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);

模塊卸載函數


Linux內核模塊載入函數一般以_ _exit標識聲明,典型的模塊卸載函數的形式如代碼清單4.3所看到的。
代碼清單4.3 內核模塊卸載函數
1    static void _ _exit cleanup_function(void)
2    {
3          /* 釋放代碼 */
4    }
5    module_exit(cleanup_function);

模塊卸載函數在模塊卸載的時候執行,不返回不論什麽值,必須以“module_exit(函數名)”的形式來指定。通常來說,模塊卸載函數要完畢與模塊載入函數相反的功能。


我們用__exit來修飾模塊卸載函數,能夠告訴內核假設相關的模塊被直接編譯進內核(即built-in),則cleanup_function() 函數會被省略直接不連接進最後的鏡像。既然模塊被built-in了。就不可能卸載它了。卸載函數也就沒有存在的必要了。除了函數以外,僅僅是退出階段採用的數據也能夠用__exitdata來形容。

模塊參數


我們能夠用“module_param(參數名,參數類型,參數讀/寫權限)”為模塊定義一個參數。比例如以下列代碼定義了1個整型參數和1個字符指針參數:
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

在裝載內核模塊時,用戶能夠向模塊傳遞參數。形式為“insmode(或modprobe)模塊名 參數名=參數值”,假設不傳遞,參數將使用模塊內定義的缺省值。假設模塊被built-in。就無法insmod了,可是bootloader能夠通過在bootargs裏設置“模塊名.參數名=值”的形式給該built-in的模塊傳遞參數。
參數類型能夠是byte、short、ushort、int、uint、long、ulong、charp(字符指針)、bool或invbool(布爾的反),在模塊被編譯時會將module_param中聲明的類型與變量定義的類型進行比較。推斷是否一致。
除此之外,模塊也能夠擁有參數數組,形式為“module_param_array(數組名,數組類型,數組長,參數讀/寫權限)”。
模塊被載入後,在/sys/module/文件夾下將出現以此模塊名命名的文件夾。當“參數讀/寫權限”為0時。表示此參數不存在sysfs文件系統下相應的文件節點。假設此模塊存在“參數讀/寫權限”不為0的命令行參數,在此模塊的文件夾下還將出現parameters文件夾。包括一系列以參數名命名的文件節點,這些文件的權限值就是傳入module_param()的“參數讀/寫權限”,而文件的內容為參數的值。
執行insmod或modprobe命令時,應使用逗號分隔輸入的數組元素。
如今我們定義一個包括兩個參數的模塊(如代碼清單4.4。位於本書源碼/kernel/drivers/param文件夾),並觀察模塊載入時被傳遞參數和不傳遞參數時的輸出。


代碼清單4.4 帶參數的內核模塊

01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 static char *book_name = "dissecting Linux Device Driver";
05 module_param(book_name, charp, S_IRUGO);
06
07 static int book_num = 4000;
08 module_param(book_num, int, S_IRUGO);
09
10 static int __init book_init(void)
11 {
12     printk(KERN_INFO "book name:%s\n", book_name);
13     printk(KERN_INFO "book num:%d\n", book_num);
14     return 0;
15 }
16 module_init(book_init);
17
18 static void __exit book_exit(void)
19 {
20     printk(KERN_INFO "book module exit\n ");
21 }
22 module_exit(book_exit);
23
24 MODULE_AUTHOR("Barry Song <[email protected]>");
25 MODULE_LICENSE("GPL v2");
26 MODULE_DESCRIPTION("A simple Module for testing module params");
27 MODULE_VERSION("V1.0");

對上述模塊執行“insmod book.ko”命令載入,相應輸出都為模塊內的默認值。通過查看“/var/log/messages”日誌文件能夠看到內核的輸出:
# tail -n 2 /var/log/messages
Jul  2 01:03:10 localhost kernel:  <6> book name:dissecting Linux Device Driver
Jul  2 01:03:10 localhost kernel:  book num:4000

當用戶執行“insmod book.ko book_name=‘GoodBook‘ book_num=5000”命令時。輸出的是用戶傳遞的參數:
# tail -n 2 /var/log/messages
Jul  2 01:06:21 localhost kernel:  <6> book name:GoodBook
Jul  2 01:06:21 localhost kernel:  book num:5000
Jul  2 01:06:21 localhost kernel:  book num:5000
另外,在/sys文件夾下,也能夠看到book模塊的參數:
[email protected]:/sys/module/book/parameters$ tree
.
├── book_name
└── book_num

導出符號

Linux的“/proc/kallsyms”文件相應著內核符號表,它記錄了符號以及符號所在的內存地址。
模塊能夠使用例如以下宏導出符號到內核符號表:
EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);
導出的符號將能夠被其它模塊使用。使用前聲明一下就可以。EXPORT_SYMBOL_GPL()僅僅適用於包括GPL許可權的模塊。代碼清單4.5給出了一個導出整數加、減運算函數符號的內核模塊的樣例。
代碼清單4.5 內核模塊中的符號導出
01 #include <linux/init.h>
02 #include <linux/module.h>
03
04 int add_integar(int a, int b)
05 {
06     return a + b;
07 }
08 EXPORT_SYMBOL_GPL(add_integar);
09
10 int sub_integar(int a, int b)
11 {
12     return a - b;
13 }
14 EXPORT_SYMBOL_GPL(sub_integar);
15
16 MODULE_LICENSE("GPL v2");

從“/proc/kallsyms”文件裏找出add_integar、sub_integar相關信息:
# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar    [export_symb]
e679403c r __kstrtab_sub_integar    [export_symb]
e6794038 r __kcrctab_sub_integar    [export_symb]
e6794024 r __ksymtab_add_integar    [export_symb]
e6794048 r __kstrtab_add_integar    [export_symb]
e6794034 r __kcrctab_add_integar    [export_symb]
e6793000 t add_integar    [export_symb]
e6793010 t sub_integar    [export_symb]

模塊聲明與描寫敘述


在Linux內核模塊中,我們能夠用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分別聲明模塊的作者、描寫敘述、版本號、設備表和別名,比如:
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

對於USB、PCI等設備驅動,一般會創建一個MODULE_DEVICE_TABLE。表明該驅動模塊所支持的設備,如代碼清單4.6所看到的。
代碼清單4.6 驅動所支持的設備列表
1 /* 相應此驅動的設備表 */
2 static struct usb_device_id skel_table [] = {
3 { USB_DEVICE(USB_SKEL_VENDOR_ID,
4       USB_SKEL_PRODUCT_ID) },
5     { } /* 表結束 */
6 };
7
8 MODULE_DEVICE_TABLE (usb, skel_table);

此時。並不須要讀者理解MODULE_DEVICE_TABLE的作用,興許相關章節會有具體介紹。

模塊的使用計數


Linux 2.4內核中。模塊自身通過MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏來管理自己被使用的計數。
Linux 2.6以後的內核提供了模塊計數管理接口try_module_get(&module)和module_put (&module),從而代替Linux 2.4內核中的模塊使用計數管理宏。

模塊的使用計數一般不必由模塊自身管理。並且模塊計數管理還考慮了SMP與PREEMPT機制的影響。



int try_module_get(struct module *module);

該函數用於添加模塊使用計數。若返回為0。表示調用失敗,希望使用的模塊沒有被載入或正在被卸載中。

void module_put(struct module *module);

該函數用於降低模塊使用計數。
try_module_get ()與module_put()的引入與使用與Linux 2.6以後的內核下的設備模型密切相關。

Linux 2.6以後的內核為不同類型的設備定義了struct module *owner域,用來指向管理此設備的模塊。當開始使用某個設備時,內核使用try_module_get(dev->owner)去添加管理此設備的owner模塊的使用計數;當不再使用此設備時。內核使用module_put(dev->owner)降低對管理此設備的owner模塊的使用計數。

這樣,當設備在使用時,管理此設備的模塊將不能被卸載。僅僅有當設備不再被使用時,模塊才同意被卸載。


在Linux 2.6以後的內核下,對於設備驅動而言,很少須要親自調用try_module_get()與module_put()。由於此時開發者所寫的驅動通常為支持某具體設備的owner模塊,對此設備owner模塊的計數管理由內核裏更底層的代碼如總線驅動或是此類設備共用的核心模塊來實現。從而簡化了設備驅動開發。



模塊的編譯

我們能夠為代碼清單4.1的模板編寫一個簡單的Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0


build: kernel_modules

kernel_modules:
      make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules

clean:
      make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

該Makefile文件應該與源碼hello.c位於同一文件夾,開啟當中的EXTRA_CFLAGS=-g -O0能夠得到包括調試信息的hello.ko模塊。執行make命令得到的模塊可直接在PC上執行。
假設一個模塊包括多個.c文件(如file1.c、file2.c),則應該以例如以下方式編寫Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o

模塊與GPL

Linux內核有2種方法導出符號給模塊使用,一種方法是EXPORT_SYMBOL()。第二種EXPORT_SYMBOL_GPL()。這一點和模塊A導出符號給模塊B用是一致的。
內核的Documentation/DocBook/kernel-hacking.tmpl明白表明“the symbols exported by EXPORT_SYMBOL_GPL()can only be seen by modules with a MODULE_LICENSE() that specifies a GPL compatible license.”由此可見內核用EXPORT_SYMBOL_GPL()導出的符號是不能夠被非GPL模塊引用的。
由於相當多的內核符號都是以EXPORT_SYMBOL_GPL()導出的,所以歷史上以前有一些公司的做法是把內核的EXPORT_SYMBOL_GPL()直接改為EXPORT_SYMBOL()。然後將改動後的內核以GPL形式公布。這樣改動內核之後,模塊不再使用內核的EXPORT_SYMBOL_GPL()符號,因此模塊不再須要GPL。對此Linus的回復是:“I think both them said that anybody who were to change a xyz_GPL to the non-GPL one in order to use it with a non-GPL module would almost immediately fall under the "willful infringement" thing, and that it would make it MUCH easier to get triple damages and/or injunctions, since they clearly knew about it”。

因此,這樣的做法可能構成“蓄意侵權(willful infringement)”。
第二種做法是寫一個wrapper內核模塊(這個模塊遵循GPL)。把EXPORT_SYMBOL_GPL()導出的符號封裝一次再次以EXPORT_SYMBOL()形式導出,而其它的模塊不直接調用內核而是調用wrapper函數。如圖4.1所看到的。這樣的做法也具有爭議。
技術分享
圖4.1將EXPORT_SYMBOL_GPL又一次以EXPORT_SYMBOL導出
一般覺得。保守的做法是Linux內核不能使用非GPL許可權。




Linux內核模塊編程與內核模塊LICENSE -《具體解釋(第3版)》預讀