Linux EXPORT_SYMBOL宏详解
文章目錄
- 前言
- 一、EXPORT_SYMBOL簡介
- 二、EXPORT_SYMBOL源碼詳解
- 三、模塊導出符號
- 總結
- 參考資料
前言
內核模塊被載入后,就會被動態地連接到內核(與用戶空間的動態庫類似,只有函數被顯式的導出為外部函數后,才可以被動態庫調用)。在內核中,導出內核函數需要特殊的指令:EXPORT_SYMBOL宏和EXPORT_SYMBOL_GPL宏。
在內核代碼中我們經常看到EXPORT_SYMBOL宏,用來導出一個內核符號。比如:
EXPORT_SYMBOL宏:
EXPORT_SYMBOL_GPL宏:
// linux-4.10.1/kernel/module.c/** Mutex protects:* 1) List of modules (also safely readable with preempt_disable),* 2) module_use links,* 3) module_addr_min/module_addr_max.* (delete and add uses RCU list operations). */ DEFINE_MUTEX(module_mutex); EXPORT_SYMBOL_GPL(module_mutex);一、EXPORT_SYMBOL簡介
我們都知道Linux 是單內核,作為一個不可分割的靜態執行庫,內核通常以單個靜態的二進制文件形式存放在磁盤中,在一個單獨的內核地址空間上運行,由于內核都處在同一內核地址空間,因此內核可以直接調用函數。如果只是單一的內核映像存在,而沒有內核模塊存在,EXPORT_SYMBOL通常是沒有啥意義的,因為對于靜態編譯鏈接而成的單一的內核映像而言,所有的符號引用都將在靜態鏈接階段完成。
由于Linux支持動態地加載內核模塊,運行內核在運行時根據需要動態地加載模塊,由于有內核模塊的存在,內核模塊通常會使用內核映像中的符號,因此使用EXPORT_SYMBOL將內核映像的符號導出,這樣在內核模塊中就能使用該符號了。
內核模塊是獨立編譯的,然后加載到正在運行的內核中,當編譯內核模塊時,編譯器引用到內核中的符號時,會產生未解決的引用,處理“未解決引用”問題的就要在模塊加載期間找到當前“未解決的引用”符號在內存中的實際目標地址。內核和內核模塊通過符號表的形式向外部導出符號的相關信息,這種導出符號的方式在代碼層面以EXPORT_SYMBOL宏定義的形式存在。
解決模塊中的未解決引用是在模塊的加載階段,用nm命名查看模塊未解決的引用:
nm nf_conntrack.ko輸出中U代表未解決的引用,對于未解決的引用符號,不顯示該符號相對于模塊起始地址的相對偏移地址,即沒有符號值。
查看模塊所有的未定義引用:
nm nf_conntrack.ko | grep '\<U\>'二、EXPORT_SYMBOL源碼詳解
// linux-4.10.1/include/linux/export.hstruct kernel_symbol {unsigned long value;const char *name; };/* 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 }#define EXPORT_SYMBOL(sym) \__EXPORT_SYMBOL(sym, "")#define EXPORT_SYMBOL_GPL(sym) \__EXPORT_SYMBOL(sym, "_gpl")#define EXPORT_SYMBOL_GPL_FUTURE(sym) \__EXPORT_SYMBOL(sym, "_gpl_future")C語言的宏定義中 # 和 ## 運算符:
#運算符是將其后面的宏參數轉化為字符串,用來創建字符串,例如#sym,表示"sym"。
##運算符用來替換粘合兩個不同的符號,例如__ksymtab_##sym,就表示"__ksymtab_sym"。
attribute:__attribute__實際上是GCC的一種編譯器命令,用來指示編譯器執行實現某些高級操作。__attribute__可以設置函數屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute),函數屬性可以幫助開發人員向函數聲明中添加一些特性,這可以使編譯器在錯誤檢查方面增強。
使用EXPORT_SYMBOL(export_function)導出export_function函數,上述宏可以擴展為:
static const char __kstrtab_export_function[] = "export_function"; static const struct kernel_symbol __ksymtab_export_function = {(unsigned long)&export_function, __kstrtab_export_function };第一個變量是一個字符串靜態變量,用來表示導出的符號名稱"export_function"。
第二個變量類型是struct kernel_symbol數據結構,用來表示一個內核符號的實例,struct kernel_symbol的定義為:
其中,value是該符號在內存中的地址,name是符號名。所以,由該數據結構可以知道,用EXPORT_SYMBOL(export_function)來導出符號"export_function",實際上是要通過struct kernel_symbol的一個對象告訴外部關于這個符號的兩點信息:符號名稱和地址。這樣使得內核根據函數的字符串名稱,即可找到匹配的代碼地址,在解決未定義的引用時需要這要做。
因此,由EXPORT_SYMBOL等宏導出的符號,與一般的變量定義并沒有實質性的差異,唯一的不同點在于它們被放在了特定的section中。
上面的符號"export_function"會放在"__ksymtab_strings"的section中,struct kernel_symbol __ksymtab_export_function會放在
“__ksymtab"的section中。對于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE而言,其struct kernel_symbol實例所在的section名稱則分別為”__ksymtab_gpl"和"__ksymtab_gpl_future")。
我已 nf_conntrack.ko 模塊為例子:
readelf -S nf_conntrack.ko
對這些section的使用需要經過一個中間環節,即鏈接腳本與鏈接器部分。鏈接腳本告訴鏈接器把所有目標文件中的名為“__ksymtab”的section放置在最終內核(或者是內核模塊)映像文件的名為“__ksymtab”的section中(對于目標文件中的名為“__ksymtab_gpl”、“__ksymtab_gpl_future”、“__kcrctab”、“__kcrctab_gpl”和“__kcrctab_gpl_future”的section都同樣處理)。
如下所示:
// linux-4.10.1/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: GPL-only symbols */ \__ksymtab_gpl : AT(ADDR(__ksymtab_gpl) - LOAD_OFFSET) { \VMLINUX_SYMBOL(__start___ksymtab_gpl) = .; \KEEP(*(SORT(___ksymtab_gpl+*))) \VMLINUX_SYMBOL(__stop___ksymtab_gpl) = .; \} \......\/* Kernel symbol table: GPL-future-only symbols */ \__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - LOAD_OFFSET) { \VMLINUX_SYMBOL(__start___ksymtab_gpl_future) = .; \KEEP(*(SORT(___ksymtab_gpl_future+*))) \VMLINUX_SYMBOL(__stop___ksymtab_gpl_future) = .; \} \\/* Kernel symbol table: Normal symbols */ \__kcrctab : AT(ADDR(__kcrctab) - LOAD_OFFSET) { \VMLINUX_SYMBOL(__start___kcrctab) = .; \KEEP(*(SORT(___kcrctab+*))) \VMLINUX_SYMBOL(__stop___kcrctab) = .; \} \\/* Kernel symbol table: GPL-only symbols */ \__kcrctab_gpl : AT(ADDR(__kcrctab_gpl) - LOAD_OFFSET) { \VMLINUX_SYMBOL(__start___kcrctab_gpl) = .; \KEEP(*(SORT(___kcrctab_gpl+*))) \VMLINUX_SYMBOL(__stop___kcrctab_gpl) = .; \} \\......\/* Kernel symbol table: GPL-future-only symbols */ \__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - LOAD_OFFSET) { \VMLINUX_SYMBOL(__start___kcrctab_gpl_future) = .; \KEEP(*(SORT(___kcrctab_gpl_future+*))) \VMLINUX_SYMBOL(__stop___kcrctab_gpl_future) = .; \} \\/* Kernel symbol table: strings */ \__ksymtab_strings : AT(ADDR(__ksymtab_strings) - LOAD_OFFSET) { \*(__ksymtab_strings) \} \這里之所以要把所有向外界導出的符號統一放到一個特殊的section里面,是為了在加載其他模塊時用來處理那些“未解決的引用”符號。上述由鏈接腳本定義的幾個變量__start___ksymtab、__stop___ksymtab、__start___ksymtab_gpl、__stop___ksymtab_gpl、__start___ksymtab_gpl_future、__stop___ksymtab_gpl_future等,它們會在對內核或者是某一內核模塊的導出符號表進行查找時用到。
內核源碼中為使用這些鏈接器產生的變量作了如下的聲明:
// linux-4.10.1/kernel/module.c/* Provided by the linker */ extern const struct kernel_symbol __start___ksymtab[]; extern const struct kernel_symbol __stop___ksymtab[]; extern const struct kernel_symbol __start___ksymtab_gpl[]; extern const struct kernel_symbol __stop___ksymtab_gpl[]; extern const struct kernel_symbol __start___ksymtab_gpl_future[]; extern const struct kernel_symbol __stop___ksymtab_gpl_future[]; extern const s32 __start___kcrctab[]; extern const s32 __start___kcrctab_gpl[]; extern const s32 __start___kcrctab_gpl_future[];內核代碼便可以直接使用這些變量而不會引起編譯錯誤。內核模塊的加載器在處理模塊中“未解決的引用”的符號時,會使用到這里定義的這些變量。
三、模塊導出符號
由前面我們可以知道模塊不僅可以使用內核或者其他模塊導出的符號,而且可以向外部導出自己的符號,模塊導出符號使用的宏和內核導出符號所使用的完全一樣:EXPORT_SYMBOL、EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_FUTURE。
內核模塊會把導出的符號分別放到“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section中。如果一個內核模塊向外界導出了自己的符號,那么將由模塊的編譯工具鏈負責生成這些導出符號section,而且這些section都帶有A標志,所以在模塊加載過程中會被搬移到CORE section區域中。如果模塊沒有向外界導出任何符號,那么在模塊的ELF文件中,將不會產生這些section。
備注:A (alloc),表示 Section 的屬性,alloc(SHF_ALLOC) 表示 Section 在模塊運行期間需要占據內存。
沒有SHF_ALLOC標志的section,這樣的section最終不占有實際內存地址。
顯然,內核需要對模塊導出的符號進行管理,以便在處理其他模塊中那些“未解決的引用”符號時能夠找到這些符號。內核對模塊導出的符號的管理使用到了struct module結構中如下的成員變量。
內核中為每一個內核模塊都分配了一個struct module實例:
// linux-4.10.1/include/linux/module.hstruct module {....../* Exported symbols */const struct kernel_symbol *syms; //內核模塊導出的符號所在起始地址const s32 *crcs; //內核模塊導出符號的校驗碼所在起始地址unsigned int num_syms; //核模塊導出的符號的數目 ....../* GPL-only exported symbols. */unsigned int num_gpl_syms;const struct kernel_symbol *gpl_syms;const s32 *gpl_crcs;....../* symbols that will be GPL-only in the near future. */const struct kernel_symbol *gpl_future_syms;const s32 *gpl_future_crcs;unsigned int num_gpl_future_syms;...... }以struct module結構體的 Exported symbols 為例:
const struct kernel_symbol *syms表示:內核模塊導出的符號所在起始地址。
const s32 *crcs表示:內核模塊導出符號的校驗碼所在起始地址。
unsigned int num_syms:表示內核模塊導出的符號的數目。
syms是一個數組,有num_syms個數組項,數組的類型是struct kernel_symbol,負責將表示符name分配到內存地址value。
crcs也是一個有num_syms個數組項的數組,存儲了導出符號的校驗和,用于實現版本控制。
內核通過調用 find_module_sections函數在Section header table的查找,獲得“__ksymtab”、“__ksymtab_gpl”和“__ksymtab_gpl_future”section在CORE section中的地址,將其記錄在mod->syms、mod->crcs、mod->gpl_syms、mod->gpl_crcs、mod->gpl_future_syms、mod->gpl_future_crcs中,其中 syms表示:內核模塊導出的符號所在起始地址,crcs表示:內核模塊導出符號的校驗碼所在起始地址。如下所示:
// linux-4.10.1/kernel/module.c/* Find a module section: 0 means not found. */ static unsigned int find_sec(const struct load_info *info, const char *name) {unsigned int i;for (i = 1; i < info->hdr->e_shnum; i++) {Elf_Shdr *shdr = &info->sechdrs[i];/* Alloc bit cleared means "ignore it." */if ((shdr->sh_flags & SHF_ALLOC)&& strcmp(info->secstrings + shdr->sh_name, name) == 0)return i;}return 0; }/* Find a module section, or NULL. Fill in number of "objects" in section. */ static void *section_objs(const struct load_info *info,const char *name,size_t object_size,unsigned int *num) {unsigned int sec = find_sec(info, name);/* Section 0 has sh_addr 0 and sh_size 0. */*num = info->sechdrs[sec].sh_size / object_size;return (void *)info->sechdrs[sec].sh_addr; }static int find_module_sections(struct module *mod, struct load_info *info) {......mod->syms = section_objs(info, "__ksymtab",sizeof(*mod->syms), &mod->num_syms);mod->crcs = section_addr(info, "__kcrctab");mod->gpl_syms = section_objs(info, "__ksymtab_gpl",sizeof(*mod->gpl_syms),&mod->num_gpl_syms);mod->gpl_crcs = section_addr(info, "__kcrctab_gpl");mod->gpl_future_syms = section_objs(info,"__ksymtab_gpl_future",sizeof(*mod->gpl_future_syms),&mod->num_gpl_future_syms);mod->gpl_future_crcs = section_addr(info, "__kcrctab_gpl_future");...... }這樣內核通過這些變量將可得到模塊導出的符號的所有信息,如圖所示:
總結
以上就是Linux 內核中 EXPORT_SYMBOL宏 詳解,有一部分相關內容在模塊的加載過程才能得到體現,這里沒有描述其過程。
參考資料
深入Linux設備驅動程序內核機制
深入Linux內核架構
總結
以上是生活随笔為你收集整理的Linux EXPORT_SYMBOL宏详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql数据库首次查询缓慢
- 下一篇: 如何查看IP地址是否被占用