Linux內(nèi)核品讀 /基礎(chǔ)組件/ 模塊機(jī)制快速入門(mén)

哈嘍,我是杰克吳,繼續(xù)記錄我的學(xué)習(xí)心得。
一、關(guān)于興趣的幾點(diǎn)思考
1. 享受不是興趣,愿意付出才是:
興趣很容易跟享受混淆。享受是被動(dòng)的,無(wú)需付出;而興趣則要求你甘愿為了這件事情付出努力。
2.任何事情,接觸皮毛的時(shí)候不要談興趣:
在我開(kāi)始公眾號(hào)寫(xiě)文章之前,只是粗淺地覺(jué)得這個(gè)事不難我可以嘗試一下,而事實(shí)上,持續(xù)寫(xiě)作的難度和意義超乎大多數(shù)人的想象。
任何事情,先做到 60 分,再談是否喜歡。
3. 興趣和愛(ài)好不太一樣:
區(qū)別在于你是否需要且愿意通過(guò)刻意練習(xí)以收獲這個(gè)興趣,以及這件事是否能給你帶來(lái)持續(xù)的成就感。
吃喝玩樂(lè)(旅游,逛街,買(mǎi)買(mǎi)買(mǎi))是愛(ài)好,不是興趣。純粹的看電影是愛(ài)好,但是認(rèn)真地寫(xiě)影評(píng)(經(jīng)歷了思考與分享)則算是興趣。表面看上去都是同一件事,但是不同人會(huì)發(fā)展成不一樣的結(jié)果。
最開(kāi)始時(shí)可能只是愛(ài)好,但是隨著你的持續(xù)思考和投入,可能會(huì)發(fā)展為你的理想職業(yè)。
4. 興趣可以帶有功利性:
那些看似功利的標(biāo)準(zhǔn)(例如高考、面試),存在很多偏差的部分,但不可否認(rèn),在絕大多數(shù)情況下,它們提供了較為高效和正確的努力方向。
把自己熱愛(ài)的事情用來(lái)掙錢(qián),非常好。只憑自己的興致去做,確實(shí)會(huì)有更多愉悅,但這也是最廉價(jià)、最輕易的喜歡了,問(wèn)題是,你很難真正做得好。你真的喜歡這個(gè)事,你會(huì)主動(dòng)爭(zhēng)取做好,贏得市場(chǎng)才會(huì)給你帶來(lái)更長(zhǎng)久的愉悅感。
二、模塊機(jī)制快速入門(mén) (1)
目錄:
1.?內(nèi)核模塊的使用
2.?內(nèi)核模塊的文件格式
3. EXPORT_SYMBOL 是如何實(shí)現(xiàn)符號(hào)導(dǎo)出的?
4.?相關(guān)參考
基于 Linux-4.14 + Arm-v7。
1. 內(nèi)核模塊的使用
最簡(jiǎn)單的內(nèi)核模塊:
#include?
#include?
static?char?*name?=?"embedded?hacker";
module_param(name,?charp,?S_IRUGO);????//?指定模塊可以接收的參數(shù)
static?void?print_hello(void)
{
????printk(KERN_INFO?"Hello?World,?%s\n",?name);
}
static?int?__init?hello_init(void)
{
????printk(KERN_INFO?"Hello?World?init\n");
????print_hello();
????return?0;
}
module_init(hello_init);
static?void?__exit?hello_exit(void)
{
????printk(KERN_INFO?"Hello?World?exit\n?");
}
module_exit(hello_exit);
EXPORT_SYMBOL(print_hello);???//?導(dǎo)出符號(hào)?print_hello
MODULE_AUTHOR("es-hacker");???//?指定作者
MODULE_LICENSE("GPL?v2");?????//?指定?license
MODULE_DESCRIPTION("A?simple?Hello?World?Module");??//?指定模塊的描述信息
MODULE_ALIAS("a?simplest?module");??//?指定模塊的別名
運(yùn)行效果:
$?insmod?hello.ko???//?加載模塊
Hello?World?init????//?加載模塊時(shí),module_init()?里的函數(shù)被調(diào)用
Hello?World,?embedded?hacker
$?rmmod?hello???????//?卸載模塊
Hello?World?exit????//?卸載模塊時(shí),module_exit()?里的函數(shù)被調(diào)用
$?insmod?hello.ko?name=Jack?//?指定模塊參數(shù)
Hello?World?init
Hello?World,?Jack
$?rmmod?hello
Hello?World?exit
到此,內(nèi)核模塊的使用方法就介紹完畢了,非常簡(jiǎn)單易用。
接下來(lái)是痛苦的部分:探索一下背后的實(shí)現(xiàn)機(jī)制。
2. 內(nèi)核模塊的文件格式
可以用 file 命令確定一個(gè)文件的格式:
$?file?hello.ko?
hello.ko:?ELF?32-bit?LSB?relocatable,?ARM,?EABI5?version?1?(SYSV),?BuildID[sha1]=2feb2cb1328c0a9113658d6e90ac20d7e4c56384,?not?stripped
內(nèi)核模塊的格式為 ELF ( Executable and Linkable Format ):
目前不需要全面了解 ELF 文件格式的所有技術(shù)細(xì)節(jié),只需要結(jié)合 Linux 源碼中定義的 ELF 相關(guān)數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)單了解一下 ELF 的構(gòu)造即可。
靜態(tài)的 ELF 文件視圖總體上可分為 3 部分:

頭部的 ELF header;
中間的 Section;
尾部的 Section header table
1) ELF header 部分:
作用:描述整個(gè) ELF 文件。
組成:Linux 內(nèi)核里的數(shù)據(jù)結(jié)構(gòu)定義如下,注釋部分為內(nèi)核模塊機(jī)制相關(guān)的的成員。
typedef?struct?elf32_hdr{
??unsigned?char?e_ident[EI_NIDENT];
??/*?文件類(lèi)型?*/
??Elf32_Half?e_type;
??Elf32_Half?e_machine;
??Elf32_Word?e_version;
??/*?Entry?point?*/
??Elf32_Addr?e_entry;
??Elf32_Off?e_phoff;
??/*?Section?header?table?在文件中的偏移量?*/
??Elf32_Off?e_shoff;
??Elf32_Word?e_flags;
??Elf32_Half?e_ehsize;
??Elf32_Half?e_phentsize;
??Elf32_Half?e_phnum;
??/*?Section?header?table?中?entry?的大小?*/
??Elf32_Half?e_shentsize;
??/*?Section?header?table?中有多少個(gè)?entry?*/
??Elf32_Half?e_shnum;
??Elf32_Half?e_shstrndx;
}?Elf32_Ehdr;
實(shí)踐:
$?#?readelf?hello.ko?-h???????#?[-h|--file-header]
ELF?Header:
??Magic:???7f?45?4c?46?01?01?01?00?00?00?00?00?00?00?00?00?
??Class:?????????????????????????????ELF32
??Data:??????????????????????????????2's?complement,?little?endian
??Version:???????????????????????????1?(current)
??OS/ABI:????????????????????????????UNIX?-?System?V
??ABI?Version:???????????????????????0
??Type:??????????????????????????????REL?(Relocatable?file)
??Machine:???????????????????????????ARM
??Version:???????????????????????????0x1
??Entry?point?address:???????????????0x0
??Start?of?program?headers:??????????0?(bytes?into?file)
??Start?of?section?headers:??????????59648?(bytes?into?file)
??Flags:?????????????????????????????0x5000000,?Version5?EABI
??Size?of?this?header:???????????????52?(bytes)
??Size?of?program?headers:???????????0?(bytes)
??Number?of?program?headers:?????????0
??Size?of?section?headers:???????????40?(bytes)
??Number?of?section?headers:?????????52
??Section?header?string?table?index:?51
2) Section 部分:
作用:對(duì)應(yīng)人們常說(shuō)的各種數(shù)據(jù)段、代碼段等,術(shù)語(yǔ)是 section。
組成:ELF 文件的主體,位于文件視圖中間部分的一個(gè)連續(xù)區(qū)域中。但是當(dāng)模塊被內(nèi)核加載時(shí),會(huì)根據(jù)各自屬性被重新分配到新的內(nèi)存區(qū)域。
3) Section header table 部分:
作用:每一個(gè)條目(術(shù)語(yǔ)叫 entry) 就是一個(gè) Section header,負(fù)責(zé)描述 Section;
組成:由若干個(gè) Section header entry 組成,Linux 內(nèi)核里的數(shù)據(jù)結(jié)構(gòu)定義如下 (注釋部分為內(nèi)核模塊機(jī)制相關(guān)的的成員):
typedef?struct?elf32_shdr?{
??Elf32_Word?sh_name;
??Elf32_Word?sh_type;
??Elf32_Word?sh_flags;
??/*?對(duì)應(yīng)的 section 在內(nèi)存中的實(shí)際地址。初始值為0,當(dāng)模塊被內(nèi)核加載時(shí),會(huì)被修改為 section 在內(nèi)存中的實(shí)際地址?*/
??Elf32_Addr?sh_addr;
??/*?section?在文件視圖中的偏移量?*/
??Elf32_Off?sh_offset;
??/*?section?在文件視圖中的大小?*/
??Elf32_Word?sh_size;
??Elf32_Word?sh_link;
??Elf32_Word?sh_info;
??Elf32_Word?sh_addralign;
??Elf32_Word?sh_entsize;
}?Elf32_Shdr;
實(shí)踐:
$?readelf?hello.ko?-S?????#?[-S|--section-headers|--sections]
There?are?52?section?headers,?starting?at?offset?0xe900:
Section?Headers:
??[Nr]?Name??????????????Type????????????Addr?????Off????Size???ES?Flg?Lk?Inf?Al
??[?0]???????????????????NULL????????????00000000?000000?000000?00??????0???0??0
??[?1]?.note.gnu.build-i?NOTE????????????00000000?000034?000024?00???A??0???0??4
??[?2]?.text?????????????PROGBITS????????00000000?000058?000000?00??AX??0???0??1
??[...]
??[?5]?.init.text????????PROGBITS????????00000000?000070?00001c?00??AX??0???0??4
??[...]
??[?7]?.exit.text????????PROGBITS????????00000000?00008c?00000c?00??AX??0???0??4
??[...]
??[?9]?__ksymtab?????????PROGBITS????????00000000?000098?000008?00???A??0???0??4
??[...]
??[25]?__ksymtab_strings?PROGBITS????????00000000?0001f1?00000c?00???A??0???0??1
??[26]?__param???????????PROGBITS????????00000000?000200?000014?00???A??0???0??4
??[27]?.rel__param???????REL?????????????00000000?00b9e4?000020?08???I?49??26??4
??[28]?__versions????????PROGBITS????????00000000?000214?000100?00???A??0???0??4
??[29]?.data?????????????PROGBITS????????00000000?000314?000004?00??WA??0???0??4
??[...]
??[48]?.ARM.attributes???ARM_ATTRIBUTES??00000000?00b21a?000031?00??????0???0??1
??[49]?.symtab???????????SYMTAB??????????00000000?00b24c?000520?10?????50??75??4
??[50]?.strtab???????????STRTAB??????????00000000?00b76c?0001cd?00??????0???0??1
??[51]?.shstrtab?????????STRTAB??????????00000000?00e6e4?00021b?00??????0???0??1
Key?to?Flags:
??W?(write),?A?(alloc),?X?(execute),?M?(merge),?S?(strings)
??I?(info),?L?(link?order),?G?(group),?T?(TLS),?E?(exclude),?x?(unknown)
??O?(extra?OS?processing?required)?o?(OS?specific),?p?(processor?specific)
這里只截取模塊加載相關(guān)的部分 section header,現(xiàn)在有個(gè)初步印象就好,后續(xù)使用到了相關(guān)的 secition header,再做進(jìn)一步的研究分析。
內(nèi)核模塊自身并不會(huì)使用到上述數(shù)據(jù)結(jié)構(gòu) (elf32_hdr、elf32_shdr),它們是給內(nèi)核模塊加載器在加載模塊時(shí)使用的。
3. EXPORT_SYMBOL() 是如何實(shí)現(xiàn)符號(hào)導(dǎo)出的?
EXPORT_SYMBOL() 系列宏用來(lái)向外界導(dǎo)出一個(gè)符號(hào)。內(nèi)核和內(nèi)核模塊通過(guò)符號(hào)表的形式向外部世界導(dǎo)出符號(hào)的相關(guān)信息。
為什么要導(dǎo)出符號(hào)?
如果沒(méi)有獨(dú)立存在的內(nèi)核模塊,作為單一的 Linux 內(nèi)核映像,就沒(méi)必要導(dǎo)出符號(hào)了。對(duì)于靜態(tài)編譯鏈接而成的內(nèi)核映像而言,所有的符號(hào)引用都會(huì)在靜態(tài)鏈接階段完成。
有了內(nèi)核模塊之后,獨(dú)立編譯鏈接的內(nèi)核模塊要使用到內(nèi)核提供的基礎(chǔ)設(shè)施(即調(diào)用內(nèi)核函數(shù),例如 printk)的話,就必須要解決符號(hào)引用問(wèn)題 (unresolved symbol)。
可以用
nm命令來(lái)查看一個(gè)模塊中出現(xiàn)的未定義符號(hào):
$?nm?hello.o?-u?????????#?[-u|--undefined-only]
?????????U?__aeabi_unwind_cpp_pr0
?????????U?param_ops_charp
?????????U?printk
?????????U?__this_module
處理 unresolved symbol 問(wèn)題的本質(zhì)是在模塊加載期間找到該符號(hào)在內(nèi)存中的實(shí)際地址。
從全局上看,EXPORT_SYMBOL 的完整實(shí)現(xiàn)包括 3 部分:
EXPORT_SYMBOL 的定義部分
鏈接腳本鏈接器部分
使用導(dǎo)出符號(hào)部分
EXPORT_SYMBOL 的定義:
//?include/linux/export.h
#define?EXPORT_SYMBOL(sym)?__EXPORT_SYMBOL(sym,?"")
/*?For?every?exported?symbol,?place?a?struct?in?the?__ksymtab?section?*/
#define?___EXPORT_SYMBOL(sym,?sec)?????\
?extern?typeof(sym)?sym;??????\
?__CRC_SYMBOL(sym,?sec)??????\
?static?const?char?__kstrtab_##sym[]????\
?__attribute__((section("__ksymtab_strings"),?aligned(1)))?\
?=?VMLINUX_SYMBOL_STR(sym);?????\
?static?const?struct?kernel_symbol?__ksymtab_##sym??\
?__used????????\
?__attribute__((section("___ksymtab"?sec?"+"?#sym),?used))?\
?=?{?(unsigned?long)&sym,?__kstrtab_##sym?}
以 hello.ko 為例,EXPORT_SYMBOL(print_hello) 本質(zhì)上就是定義了 2 個(gè)變量:
static?const?char?__kstrtab_print_hello[]?=?"print_hello"
static?const?struct?kernel_symbol?__ksymtab_print_hello?=?{
??(unsigned?long)&print_hello,
??__kstrtab_print_hello,
};
變量1: char []
用于保存符號(hào)名; 被放置在名為 "__ksymtab_strings" 的 section 中; 變量2: struct kernel_symbol
用于保存符號(hào)名與地址; 被放置在名為 "___ksymtab+print_hello" 的 section 中;
根據(jù) scripts/module-common.lds 里的定義:
SECTIONS?{
??[...]
?__ksymtab??0?:?{?*(SORT(___ksymtab+*))?}
??[...]
}
"___ksymtab+print_hello" 會(huì)被轉(zhuǎn)換為 "__ksymtab",這樣就跟我們用 readelf hello.ko -S 查看到的 section 對(duì)應(yīng)上了。
為了讓內(nèi)核可以通過(guò)上述 __ksymtab section 找到被導(dǎo)出的符號(hào),鏈接器必須導(dǎo)出 section 的地址:

include/asm-generic/vmlinux.lds.h
?/*?Kernel?symbol?table:?Normal?symbols?*/???\
?__ksymtab?????????:?AT(ADDR(__ksymtab)?-?LOAD_OFFSET)?{??\
??VMLINUX_SYMBOL(__start___ksymtab)?=?.;???\
??KEEP(*(SORT(___ksymtab+*)))????\
??VMLINUX_SYMBOL(__stop___ksymtab)?=?.;???\
?}?
?/*?Kernel?symbol?table:?strings?*/????\
?__ksymtab_strings?:?AT(ADDR(__ksymtab_strings)?-?LOAD_OFFSET)?{?\
??*(__ksymtab_strings)?????\
?}?
在 kernel/module.c 中,可以看到下列聲明:
/*?Provided?by?the?linker?*/
extern?const?struct?kernel_symbol?__start___ksymtab[];
extern?const?struct?kernel_symbol?__stop___ksymtab[];
[...]
這些變量會(huì)在內(nèi)核或者內(nèi)核模塊查找某個(gè)符號(hào)時(shí)被使用。
EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 導(dǎo)出符號(hào)的可見(jiàn)性:

從這里開(kāi)始重頭戲模塊加載的分析了,鑒于大多數(shù)人的注意力無(wú)法在一篇文章里上集中太久,更多的內(nèi)容將放在后面的文章里。建議大家可以先自行閱讀相關(guān)書(shū)籍,不是自己理解到的東西是消化不了的。
4. 相關(guān)參考
Linux 設(shè)備驅(qū)動(dòng)開(kāi)發(fā)詳解,第 4 章節(jié)
深入 Linux 設(shè)備驅(qū)動(dòng)程序內(nèi)核機(jī)制,第 1 章節(jié)
深入 Linux 內(nèi)核架構(gòu),第 7 章節(jié)
深入理解 Linux 內(nèi)核,第20 章節(jié)、附錄2
5. 更多值得關(guān)注的知識(shí)點(diǎn)
模塊的加載
模塊的參數(shù)傳遞機(jī)制
模塊之間的依賴(lài)關(guān)系
模塊中的版本控制機(jī)制
...
三、思考技術(shù),也思考人生
要學(xué)習(xí)技術(shù),更要學(xué)習(xí)如何生活。
你和我各有一個(gè)蘋(píng)果,如果我們交換蘋(píng)果的話,我們還是只有一個(gè)蘋(píng)果。但當(dāng)你和我各有一個(gè)想法,我們交換想法的話,我們就都有兩個(gè)想法了。

