天天看點

再談核心子產品加載(一)—背景知識

版權聲明:本文為CSDN部落客「[email protected]」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/lidan113lidan/article/details/119743237

更多内容可關注微信公衆号 

再談核心子產品加載(一)—背景知識

  幾年前寫過一篇核心子產品加載的文章<核心子產品的加載> ,最近又基于5.25核心重新讀了一遍子產品加載的流程,此文則結合一些新的認知重新整理而成.

1.核心子產品的編譯分為兩個階段,可分别稱為stage1和stage2:

  • stage1主要負責将子產品源碼(*.c)編譯為對應的目标(x.o)檔案
  • stage2主要負責對每個子產品生成一個.mod.c檔案,其中記錄了ko所需要的其他資訊,将其編譯為.mod.o,最終同子產品的x.o共同連結(ld -r)為一個目标檔案,這個目标檔案即為最終的可加載子產品(x.ko)

  其中 stage2是個批量操作,即不論核心要編譯多少個ko,都會通過一次stage2全部完成;   在stage2中會調用modpost程式,其輸入是所有子產品的*.o和核心的vmlinux,其輸出是為每個*.o生成對應的*.mod.c檔案,且将核心和内部子產品中所有EXPORT_SYMBOL_XXX的符号資訊都輸出到Module.symvers中(如果開啟CONFIG_MODVERSIONS則同時輸出符号的CRC值).    Modules.symvers的作用主要是在外部子產品編譯時告知外部子產品目前核心有哪些導出符号,以及這些導出符号的CRC值.   子產品編譯的細節可參考<Kbuild系統源碼分析(五)—子產品的編譯流程>

2.mod.c檔案的内容

  .mod.c檔案是在Stage2編譯ko時批量生成的,Stage2會調用modpost工具(同時一次傳入所有要處理的子產品的*.o檔案);modpost工具中會為所有輸入的%.o(除了vmlinux.o)生成對應的%.mod.c檔案,其内容包括:   1) 預設的.mod.c檔案頭(main=>add_header)     這裡主要包括三個内容:     * 在.modinfo段增加字元串變量vermagic ,其内容為"vermagic=$(VERMAGIC_STRING)", 記錄核心版本資訊 ,如:

[email protected]:~/qemu_aarch64_device/linux-4.19.109$ strings lib/xxhash.ko |grep vermagic
vermagic=4.19.109-g6c232927e-dirty SMP preempt mod_unload modversions aarch64
           

    * 在.modinfo段增加字元串變量name,其内容為目前子產品名,如:

[email protected]:~/qemu_aarch64_device/linux-4.19.109$ strings ./net/ipv6/ipv6.ko|grep "name="
name=ipv6
           

    * struct module __this_modules,此結構體是一個内置的struct modules,其中記錄着子產品的初始化函數等資訊

      每個子產品(ko)二進制中都有一個記錄其struct modules的段,段名為".gnu.linkonce.this_modules"。     源碼如下:

//*.mod.c檔案都擁有相同的檔案頭,生成此頭檔案的代碼在./scripts/mod/modpost.c中
#include <linux/build-salt.h>                                                                                                                     
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

BUILD_SALT;

//MODULE_INFO(tag,name)宏的作用是在.modinfo段添加變量 字元串變量tag = "tag = info"
//VERMAGIC_STRING為核心版本資訊
MODULE_INFO(vermagic, VERMAGIC_STRING);
//KBUILD_MODNAME是cc時傳入的參數,其在Makefile.lib中定義:
//modname_flags  = -DKBUILD_MODNAME=$(call name-fix,$(modname))
MODULE_INFO(name, KBUILD_MODNAME);

__visible struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
    .arch = MODULE_ARCH_INIT,
};
           

  2) 記錄目前是in-tree/out-tree子產品     已經內建在核心中的子產品為in-tree子產品,外部子產品為out-tree子產品(在不私自改動源碼的情況下),對于in-tree子產品要增加(main=>add_intree_flag):

//*.mod.c檔案都擁有相同的檔案頭,生成此頭檔案的代碼在./scripts/mod/modpost.c中
MODULE_INFO(intree, "Y");
           

    也就是在.modinfo段增加了字元串變量intree,其内容為"intree=Y"(對于out-tree的子產品直接沒有此字段)

  3) x86等平台若提供retpoline支援(防禦spectre攻擊),則會增加  MODULE_INFO(retpoline, \"Y\")   4) 對于drivers/staging目錄的子產品(測試中子產品),會增加 MODULE_INFO(staging, \"Y\");   5) 若開啟 CONFIG_MODVERSIONS(CRC校驗), 則在__versions段增加____versions數組記錄所有 導入符号 的CRC     ____versions數組的内容如下:

static const struct modversion_info ____versions[]
__used __attribute__((section("__versions"))) = {
    { 0xe57c29f2, "module_layout" },
    { 0x633475c7, "static_key_enable" },
    { 0x2cdf87a1, "proc_dointvec_minmax" },
    { 0x609f1c7e, "synchronize_net" },
    { 0xa0c9d45, "inet_peer_base_init" },
    ......
};
           

   核心子產品為____versions數組單獨配置設定了一個段__versions,需要注意的是____versions數組中存儲的實際上都是目前子產品中未定義的,但在核心或其他子產品中已定義的符号(若二者都沒定義則報錯了),____versions數組的作用是在子產品裝載時确定其未定義符号(導入符号)是否與核心中此符号的定義比對,故其中并不需要儲存目前子產品的任何其他符号(如導出符号,EXPORT_SYMBOL_XXX)的資訊(導出符号的資訊是儲存在子產品單獨的段中的,參考4)

 6) 通過depends字段記錄子產品的依賴關系    modpost生成.mod.c的過程中會記錄目前子產品引用了哪些其他非vmlilnux中的符号,并将這些符号所在的子產品名記錄到.modinfo段的depends數組中(main => add_depends):

static const char __module_depends[] = "depends=module1,module2,..."
           

  注意這裡的字元串名為 __module_depends,但.modinfo中的字段為depends

3. CRC的生成

  在核心中,并不是所有的符号都是有CRC的,隻有顯示通過EXPORT_SYMBOL[_GPL|_GPL_FUTURE]定義的符号在開啟 CONFIG_MODVERSIONS的情況下才會生成CRC.   這裡以EXPORT_SYMBOL為例,其定義如下:

#define EXPORT_SYMBOL(sym)  __EXPORT_SYMBOL(sym, "")

#define ___EXPORT_SYMBOL(sym, sec)   \
    extern typeof(sym) sym;                        \
    __CRC_SYMBOL(sym, sec)                        \
    static const char __kstrtab_##sym[] __attribute__((section("__ksymtab_strings"), used, aligned(1)))  = #sym;\
    __KSYMTAB_ENTRY(sym, sec)

#if defined(CONFIG_MODULE_REL_CRCS)
......
#else
#define __CRC_SYMBOL(sym, sec)                        \
    asm("    .section \"___kcrctab" sec "+" #sym "\", \"a\"    \n"    \
        "    .weak    __crc_" #sym "                \n"    \
        "    .long    __crc_" #sym "                \n"    \
        "    .previous                    \n");
#endif

#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
......
#else
#define __KSYMTAB_ENTRY(sym, sec)                    \
    static const struct kernel_symbol __ksymtab_##sym        \
    __attribute__((section("___ksymtab" sec "+" #sym), used))    \
    = { (unsigned long)&sym, __kstrtab_##sym }

struct kernel_symbol {
    unsigned long value;
    const char *name;
};
#endif
           

   EXPORT_SYMBOL(x)中,x是核心的一個函數或變量,此宏實際上做了三件事情:

  1. 在目标檔案中添加了一個名為 ____kcrctab+x 的段, 此段中用一個.long長度記錄一個弱符号 __crc_x的位址,但這個弱符号并沒有定義.
  2. 在目标檔案中添加(若不存在)一個名為__ksymtab_string 的段(大多數情況下此段已存在), 并在此段中添加一個字元串變量 __kstrtab_x="x" 的定義.
  3. 在目标檔案中生成了一個名為 ____ksymtab+x 的段,并在此段中定義了一個struct kernel_symbol類型的變量 __ksymtab_x = {&x, __kstrtab_x};此變量中記錄了x的位址,和x對應的字元串__kstrtab_x的位址.

   也就是說,對于任何一個EXPORT_SYMBOL定義的符号x,最終編譯出的目标檔案(*.o)中都能看到三個段(這裡x以inet6_getname為例):    * __kcrctab+x    * __ksymtab_string(公用段)    * __kstrtab+x

[email protected]:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.o |grep -E "inet6_getname|ksymtab_string"|grep -v rela
  [27] ___kcrctab+inet6_getname PROGBITS        0000000000000000 042498 000004 00   A  0   0  1
  [29] ___ksymtab+inet6_getname PROGBITS        0000000000000000 0424a0 000008 00   A  0   0  8
  [57] __ksymtab_strings PROGBITS               0000000000000000 047094 0005da 00   A  0   0  1
           

   同時靜态連結符号表也能看到三個符号資訊:    * __crc_x    * __kstrtab_x    * __ksymtab_x

[email protected]:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -s --wide ipv6.o |grep -E "inet6_getname"
   209: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   29 __ksymtab_inet6_getname
   210: 0000000000000066    14 OBJECT  LOCAL  DEFAULT   57 __kstrtab_inet6_getname
  1835: 00000000ae0b6bd1     0 NOTYPE  GLOBAL DEFAULT  ABS __crc_inet6_getname
  2292: 00000000000000c0   296 FUNC    GLOBAL DEFAULT    1 inet6_getname            //原始符号
           

  前面說過了__crc_x是一個未定義符号,這裡ipv6.o中對應的__crc_inet6_getname符号類型應該為UND,但實際上其符号類型卻為ABS,這是因為目前看到的ipv6.o并不是剛編譯完的ipv6.o(這裡稱為ipv6_old.o),而是通過連結腳本.tmp_ipv6.ver再次處理的ipv6.o(這裡稱為ipv6_new.o),這裡的邏輯總結如下:   1) CC編譯ipv6.c => ipv6_old.o //其中__crc_x是未定義的(UND)   2) CC -E ipv6.c |scripts/genksyms/genksyms -r > .tmp_ipv6.ver     genksyms工具會對源碼中所有EXPORT_SYMBOL定義的符号計算CRC,并輸出,其輸出結果類似:

__crc_blk_queue_flag_set = 0x564adbd5;  
__crc_blk_queue_flag_clear = 0x52d55223;
__crc_blk_queue_flag_test_and_set = 0x5474d529;
__crc_blk_queue_flag_test_and_clear = 0xb029eaf1;
           

  3) LD -r ipv6_old.o -o ipv6_new.o -T .tmp_ipv6.ver

    這裡将 .tmp_ipv6.ver當做連結腳本重新連結ipv6.o,因為連結腳本中有符号定義,是以導緻最終生成的ipv6_new.o中__crc_x的定義為ABS   是以說源碼中的導出符号資訊是在源碼編譯為目标檔案時就已經記錄到對應的% .o中了,且導出符号的CRC值也是在此階段生成并記錄在%.o檔案中的 .

4.核心和子產品的導出符号表

  在3中可知,在目标檔案中實際上已經包含了源碼中所有EXPORT_SYMBOL_XXX定義的導出符号的資訊,以及對應的CRC值了,而最終連結的核心鏡像(vmlinux)或子產品(ko)中,導出符号存在的形式和目标檔案有所不同:   * 核心(vmlinux[.o]):     核心編譯時,最終使用的連結腳本是 vmlinux.lds, 其是根據不同平台的vmlinux.lds.S生成的,内容如下:

./include/asm-generic/vmlinux.lds.h
#define RO_DATA(align)  RO_DATA_SECTION(align)

#define RO_DATA_SECTION(align)                        \
......
    __ksymtab         : AT(ADDR(__ksymtab) - LOAD_OFFSET) {        \
        __start___ksymtab = .;                    \
        KEEP(*(SORT(___ksymtab+*)))                \
        __stop___ksymtab = .;                    \
    }    
......
    __kcrctab         : AT(ADDR(__kcrctab) - LOAD_OFFSET) {        \
        __start___kcrctab = .;                    \
        KEEP(*(SORT(___kcrctab+*)))                \
        __stop___kcrctab = .;                    \
    }            
......
     __ksymtab_strings : AT(ADDR(__ksymtab_strings) - LOAD_OFFSET) {    \
        *(__ksymtab_strings)                    \
    }    

./arch/arm64/kernel/vmlinux.lds.S
SECTIONS
{
......
RO_DATA(PAGE_SIZE)
......
}
           

  * 子產品(ko):     子產品最終是通過Stage2的Makefile.modpost中的 cmd_ld_ko_o指令連結出來的,其指定了兩個-T腳本(可累加),分别是module-common.lds和module.lds:

./scripts/module-common.lds
SECTIONS {
    /DISCARD/ : {
        *(.discard)
        *(.discard.*)
    }

    __ksymtab        0 : { *(SORT(___ksymtab+*)) }
    __ksymtab_gpl        0 : { *(SORT(___ksymtab_gpl+*)) }
    __ksymtab_unused    0 : { *(SORT(___ksymtab_unused+*)) }
    __ksymtab_unused_gpl    0 : { *(SORT(___ksymtab_unused_gpl+*)) }
    __ksymtab_gpl_future    0 : { *(SORT(___ksymtab_gpl_future+*)) }
    __kcrctab        0 : { *(SORT(___kcrctab+*)) }
    __kcrctab_gpl        0 : { *(SORT(___kcrctab_gpl+*)) }
    __kcrctab_unused    0 : { *(SORT(___kcrctab_unused+*)) }
    __kcrctab_unused_gpl    0 : { *(SORT(___kcrctab_unused_gpl+*)) }
    __kcrctab_gpl_future    0 : { *(SORT(___kcrctab_gpl_future+*)) }
    .init_array        0 : ALIGN(8) { *(SORT(.init_array.*)) *(.init_array) }
    __jump_table        0 : ALIGN(8) { KEEP(*(__jump_table)) }
}
           

   可以看出,二者實際上都将所有的導出符号表段(___ksymtab+x)合并為一個__ksymtab段,所有的crc段合并為一個__kcrctab段,輸出到最終的vmlinux[.o]或ko中了(gpl等其他段同理,這裡先忽略)。

   這些都是記憶體段,最終會加載到記憶體,唯一的差別就是對于核心來說,其會專門 用 __start___ksymtab等符号來記錄這些段的起始結束位置( 這是因為子產品加載時,核心解析子產品後可以在struct modules中記錄子產品的段資訊,而核心啟動時沒人記錄自身資訊,故其需要特殊标記來找到段首位址)    故和前面對比(*.o),在.ko和核心(vmlinux[.o])中實際上隻是段合并了,但符号資訊并未減少:

//子產品
[email protected]:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.ko |grep -E "inet6_getname|ksymtab_string"|grep -v rela
  [26] __ksymtab_strings PROGBITS        0000000000000000 04745c 0005da 00   A  0   0  1
[email protected]:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -S --wide ipv6.ko |grep -E "ksymtab|crc"|grep -v rela
  [10] __ksymtab         PROGBITS        0000000000000000 03f2d0 000128 00   A  0   0  8
  [12] __ksymtab_gpl     PROGBITS        0000000000000000 03f3f8 000160 00   A  0   0  8
  [14] __kcrctab         PROGBITS        0000000000000000 03f558 000094 00   A  0   0  1
  [16] __kcrctab_gpl     PROGBITS        0000000000000000 03f5ec 0000b0 00   A  0   0  1
  [26] __ksymtab_strings PROGBITS        0000000000000000 04745c 0005da 00   A  0   0  1
[email protected]:~/qemu_aarch64_device/linux-4.19.109/net/ipv6$ readelf -s --wide ipv6.ko |grep -E "inet6_getname|ksymtab_string"|grep -v rela
    57: 0000000000000028     0 NOTYPE  LOCAL  DEFAULT   10 __ksymtab_inet6_getname
    58: 0000000000000066    14 OBJECT  LOCAL  DEFAULT   26 __kstrtab_inet6_getname
  1592: 00000000ae0b6bd1     0 NOTYPE  GLOBAL DEFAULT  ABS __crc_inet6_getname
  2049: 00000000000000c0   296 FUNC    GLOBAL DEFAULT    2 inet6_getname

//核心
[email protected]:~/qemu_aarch64_device/linux-4.19.109$ readelf -S vmlinux|grep -E "ksymtab|crc"
  [ 5] __ksymtab         PROGBITS         ffff000008f90550  00f20550
  [ 6] __ksymtab_gpl     PROGBITS         ffff000008f99d88  00f29d88
  [ 7] __kcrctab         PROGBITS         ffff000008fa4e28  00f34e28
  [ 8] __kcrctab_gpl     PROGBITS         ffff000008fa9a44  00f39a44
  [ 9] __ksymtab_strings PROGBITS         ffff000008faf294  00f3f294
           

5.子產品的導入符号表

  從ELF檔案來看,核心和子產品是沒有導入導出表的,因為其本身就沒有.dynsym段(目标檔案本身就沒有,vmlinux靜态連結本來也沒有,在腳本中又做了dynsym忽略),但核心和子產品都統一通過EXPORT_SYMBOL的方式在其二進制的資料段構造了自己的導出表。   而對于導入表來說,由于核心(vmlinux)中本身是不可以出現未定義符号的,是以其不需要導入表;而對于子產品來說,由于其中會使用核心或其他子產品中定義的符号,故是需要導入表的。   實際上在子產品加載時,是使用其靜态連結重定位表當做導入表的,靜态連結符号表中的UND符号實際上就應該是動态連結符号表中的UND符号,也就是所謂的導出表; 在子產品加載時,核心會先解決子產品靜态連結符号表中所有的未決符号,之後處理子產品的靜态連結重定位表,将子產品動态的靜态連結到目前核心位址空間。   而如果 核心開啟了 CONFIG_MODVERSIONS流程,那麼在Stage2的modpost批處理階段,會在*.mod.c檔案中生成____versions數組,此數組中記錄了子產品%.o的靜态連結符号表中所有未定義的符号(也就是子產品的導出符号)的CRC(若在modpost階段沒找到此符号定義,則說明核心和子產品中都沒有此符号定義,報錯)

6.關于3-5的總結:

  1) 首先對于ELF檔案來說,目标檔案(*.o)和靜态連結可執行檔案(static exe)是預設不生成.dynsym段的   2) 在ELF檔案中,本來是沒有導入導出表這一概念定義的(此概念應該來自PE檔案),但可以套用PE檔案中對應的概念   3) ELF檔案運作時的符号資訊都來自.dynsym(動态連結符号表),而靜态連結符号表(.symtab)本身是不導入記憶體的,故導入導出表的概念正常隻來自     .dynsym中的符号   4) 但正常情況下在靜态連結符号表中的UND符号和動态連結符号表中的UND應該是一樣的,也就是說隻看導入表的話,看靜态連結符号表中的UND應該也可以.   5) 核心和子產品的導出表實際上有多個,不同EXPORT_SYMBOL_XXX導出的函數會放到不同的導出表,核心和子產品中的導出表全部都是手動由EXPORT_SYMBOL_XXX系列函數指定的.   6) 下表為ELF檔案中各個表的位置,正常對于目标檔案來說,尚未連結應該是沒有導入導出表的概念的,但核心和子產品自己重新定義了導入導出表的概念,如下:

檔案格式 DLL/PIE/PDE static PDE relocatable 核心vmlinux[.o](relocatable) 子產品*.ko(relocatable)
靜态連結符号表 .symtab .symtab .symtab .symtab .symtab
動态連結符号表 .dynsym
導入表 .dynsym中非UND符号 正常無UND符号,為空 未連結,無此概念 正常無UND符号,為空 .symtab中的UND符号
導出表 .dynsym中UND符号 未連結,無此概念 __ksymtab __ksymtab
記憶體符号表 kallsyms_names ...... struct module.core_kallsyms struct module.init_kallsyms
記憶體導出表 [__start___ksymtab,__stop___ksymtab] ...... struct module.syms struct module.gpl_syms
導出表通路函數 find_symbol find_symbol
記憶體符号表通路函數 kallsyms_lookup_name kallsyms_lookup_name

繼續閱讀