Linux内核品读 /基础组件/ 模块机制快速入门
哈嘍,我是杰克吳,繼續記錄我的學習心得。
一、關于興趣的幾點思考
1. 享受不是興趣,愿意付出才是:
興趣很容易跟享受混淆。享受是被動的,無需付出;而興趣則要求你甘愿為了這件事情付出努力。
2.任何事情,接觸皮毛的時候不要談興趣:
在我開始公眾號寫文章之前,只是粗淺地覺得這個事不難我可以嘗試一下,而事實上,持續寫作的難度和意義超乎大多數人的想象。
任何事情,先做到 60 分,再談是否喜歡。
3. 興趣和愛好不太一樣:
區別在于你是否需要且愿意通過刻意練習以收獲這個興趣,以及這件事是否能給你帶來持續的成就感。
吃喝玩樂(旅游,逛街,買買買)是愛好,不是興趣。純粹的看電影是愛好,但是認真地寫影評(經歷了思考與分享)則算是興趣。表面看上去都是同一件事,但是不同人會發展成不一樣的結果。
最開始時可能只是愛好,但是隨著你的持續思考和投入,可能會發展為你的理想職業。
4. 興趣可以帶有功利性:
那些看似功利的標準(例如高考、面試),存在很多偏差的部分,但不可否認,在絕大多數情況下,它們提供了較為高效和正確的努力方向。
把自己熱愛的事情用來掙錢,非常好。只憑自己的興致去做,確實會有更多愉悅,但這也是最廉價、最輕易的喜歡了,問題是,你很難真正做得好。你真的喜歡這個事,你會主動爭取做好,贏得市場才會給你帶來更長久的愉悅感。
二、模塊機制快速入門 (1)
目錄:
1.?內核模塊的使用 2.?內核模塊的文件格式 3. EXPORT_SYMBOL 是如何實現符號導出的? 4.?相關參考基于 Linux-4.14 + Arm-v7。
1. 內核模塊的使用
最簡單的內核模塊:
#include?<linux/init.h> #include?<linux/module.h>static?char?*name?=?"embedded?hacker"; module_param(name,?charp,?S_IRUGO);????//?指定模塊可以接收的參數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);???//?導出符號?print_hello MODULE_AUTHOR("es-hacker");???//?指定作者 MODULE_LICENSE("GPL?v2");?????//?指定?license MODULE_DESCRIPTION("A?simple?Hello?World?Module");??//?指定模塊的描述信息 MODULE_ALIAS("a?simplest?module");??//?指定模塊的別名運行效果:
$?insmod?hello.ko???//?加載模塊 Hello?World?init????//?加載模塊時,module_init()?里的函數被調用 Hello?World,?embedded?hacker$?rmmod?hello???????//?卸載模塊 Hello?World?exit????//?卸載模塊時,module_exit()?里的函數被調用$?insmod?hello.ko?name=Jack?//?指定模塊參數 Hello?World?init Hello?World,?Jack$?rmmod?hello Hello?World?exit到此,內核模塊的使用方法就介紹完畢了,非常簡單易用。
接下來是痛苦的部分:探索一下背后的實現機制。
2. 內核模塊的文件格式
可以用 file 命令確定一個文件的格式:
$?file?hello.ko? hello.ko:?ELF?32-bit?LSB?relocatable,?ARM,?EABI5?version?1?(SYSV),?BuildID[sha1]=2feb2cb1328c0a9113658d6e90ac20d7e4c56384,?not?stripped內核模塊的格式為 ELF ( Executable and Linkable Format ):
目前不需要全面了解 ELF 文件格式的所有技術細節,只需要結合 Linux 源碼中定義的 ELF 相關數據結構,簡單了解一下 ELF 的構造即可。
靜態的 ELF 文件視圖總體上可分為 3 部分:
頭部的 ELF header;
中間的 Section;
尾部的 Section header table
1) ELF header 部分:
作用:描述整個 ELF 文件。
組成:Linux 內核里的數據結構定義如下,注釋部分為內核模塊機制相關的的成員。
typedef?struct?elf32_hdr{unsigned?char?e_ident[EI_NIDENT];/*?文件類型?*/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?中有多少個?entry?*/Elf32_Half?e_shnum;Elf32_Half?e_shstrndx; }?Elf32_Ehdr;實踐:
$?#?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:?????????????????????????????ELF32Data:??????????????????????????????2's?complement,?little?endianVersion:???????????????????????????1?(current)OS/ABI:????????????????????????????UNIX?-?System?VABI?Version:???????????????????????0Type:??????????????????????????????REL?(Relocatable?file)Machine:???????????????????????????ARMVersion:???????????????????????????0x1Entry?point?address:???????????????0x0Start?of?program?headers:??????????0?(bytes?into?file)Start?of?p?headers:??????????59648?(bytes?into?file)Flags:?????????????????????????????0x5000000,?Version5?EABISize?of?this?header:???????????????52?(bytes)Size?of?program?headers:???????????0?(bytes)Number?of?program?headers:?????????0Size?of?p?headers:???????????40?(bytes)Number?of?p?headers:?????????52Section?header?string?table?index:?512) Section 部分:
作用:對應人們常說的各種數據段、代碼段等,術語是 p。
組成:ELF 文件的主體,位于文件視圖中間部分的一個連續區域中。但是當模塊被內核加載時,會根據各自屬性被重新分配到新的內存區域。
3) Section header table 部分:
作用:每一個條目(術語叫 entry) 就是一個 Section header,負責描述 Section;
組成:由若干個 Section header entry 組成,Linux 內核里的數據結構定義如下 (注釋部分為內核模塊機制相關的的成員):
typedef?struct?elf32_shdr?{Elf32_Word?sh_name;Elf32_Word?sh_type;Elf32_Word?sh_flags;/*?對應的 p 在內存中的實際地址。初始值為0,當模塊被內核加載時,會被修改為 p 在內存中的實際地址?*/Elf32_Addr?sh_addr;/*?p?在文件視圖中的偏移量?*/Elf32_Off?sh_offset;/*?p?在文件視圖中的大小?*/Elf32_Word?sh_size;Elf32_Word?sh_link;Elf32_Word?sh_info;Elf32_Word?sh_addralign;Elf32_Word?sh_entsize; }?Elf32_Shdr;實踐:
$?readelf?hello.ko?-S?????#?[-S|--p-headers|--ps] There?are?52?p?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??1Key?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)這里只截取模塊加載相關的部分 p header,現在有個初步印象就好,后續使用到了相關的 secition header,再做進一步的研究分析。
內核模塊自身并不會使用到上述數據結構 (elf32_hdr、elf32_shdr),它們是給內核模塊加載器在加載模塊時使用的。
3. EXPORT_SYMBOL() 是如何實現符號導出的?
EXPORT_SYMBOL() 系列宏用來向外界導出一個符號。內核和內核模塊通過符號表的形式向外部世界導出符號的相關信息。
為什么要導出符號?
如果沒有獨立存在的內核模塊,作為單一的 Linux 內核映像,就沒必要導出符號了。對于靜態編譯鏈接而成的內核映像而言,所有的符號引用都會在靜態鏈接階段完成。
有了內核模塊之后,獨立編譯鏈接的內核模塊要使用到內核提供的基礎設施(即調用內核函數,例如 printk)的話,就必須要解決符號引用問題 (unresolved symbol)。
可以用 nm 命令來查看一個模塊中出現的未定義符號:
處理 unresolved symbol 問題的本質是在模塊加載期間找到該符號在內存中的實際地址。
從全局上看,EXPORT_SYMBOL 的完整實現包括 3 部分:
EXPORT_SYMBOL 的定義部分
鏈接腳本鏈接器部分
使用導出符號部分
EXPORT_SYMBOL 的定義:
//?include/linux/export.h #define?EXPORT_SYMBOL(sym)?__EXPORT_SYMBOL(sym,?"")/*?For?every?exported?symbol,?place?a?struct?in?the?__ksymtab?p?*/ #define?___EXPORT_SYMBOL(sym,?sec)?????\extern?typeof(sym)?sym;??????\__CRC_SYMBOL(sym,?sec)??????\static?const?char?__kstrtab_##sym[]????\__attribute__((p("__ksymtab_strings"),?aligned(1)))?\=?VMLINUX_SYMBOL_STR(sym);?????\static?const?struct?kernel_symbol?__ksymtab_##sym??\__used????????\__attribute__((p("___ksymtab"?sec?"+"?#sym),?used))?\=?{?(unsigned?long)&sym,?__kstrtab_##sym?}以 hello.ko 為例,EXPORT_SYMBOL(print_hello) 本質上就是定義了 2 個變量:
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 []
用于保存符號名;
被放置在名為 "__ksymtab_strings" 的 p 中;
變量2: struct kernel_symbol
用于保存符號名與地址;
被放置在名為 "___ksymtab+print_hello" 的 p 中;
根據 scripts/module-common.lds 里的定義:
SECTIONS?{[...]__ksymtab??0?:?{?*(SORT(___ksymtab+*))?}[...] }"___ksymtab+print_hello" 會被轉換為 "__ksymtab",這樣就跟我們用 readelf hello.ko -S 查看到的 p 對應上了。
為了讓內核可以通過上述 __ksymtab p 找到被導出的符號,鏈接器必須導出 p 的地址:
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[]; [...]這些變量會在內核或者內核模塊查找某個符號時被使用。
EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 導出符號的可見性:
從這里開始重頭戲模塊加載的分析了,鑒于大多數人的注意力無法在一篇文章里上集中太久,更多的內容將放在后面的文章里。建議大家可以先自行閱讀相關書籍,不是自己理解到的東西是消化不了的。
4. 相關參考
Linux 設備驅動開發詳解,第 4 章節
深入 Linux 設備驅動程序內核機制,第 1 章節
深入 Linux 內核架構,第 7 章節
深入理解 Linux 內核,第20 章節、附錄2
5. 更多值得關注的知識點
模塊的加載
模塊的參數傳遞機制
模塊之間的依賴關系
模塊中的版本控制機制
...
三、思考技術,也思考人生
要學習技術,更要學習如何生活。
你和我各有一個蘋果,如果我們交換蘋果的話,我們還是只有一個蘋果。但當你和我各有一個想法,我們交換想法的話,我們就都有兩個想法了。
? 推薦閱讀:
? ??專輯|Linux文章匯總
? ??專輯|程序人生
? ??專輯|C語言
嵌入式Linux
微信掃描二維碼,關注我的公眾號?
總結
以上是生活随笔為你收集整理的Linux内核品读 /基础组件/ 模块机制快速入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: V4L2用户空间和kernel层driv
- 下一篇: 涨疯了,历史总是如此相似