版權聲明:本文為CSDN部落客「[email protected]」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。
原文連結:https://blog.csdn.net/lidan113lidan/article/details/119813256
更多内容可關注微信公衆号
![]()
再談核心子產品加載(二)—子產品加載流程(上)
核心子產品加載一共有兩個syscall入口,分别為:
* sys_init_module: 使用者态傳入包含子產品代碼的記憶體位址
* sys_finit_module: 使用者态傳入子產品二進制的檔案句柄fd
除了以上差別外,sys_fini_module接口相對更加靈活,其可以傳入額外的flag來忽略子產品版本資訊和CRC以強制加載。二者最終都直接 return load_module函數,真正的子產品加載是在load_module函數中完成的,其聲明如下:
static int load_module(struct load_info *info, const char __user *uargs, int flags);
其中:
* info記錄了子產品的二進制中的所有相關資訊
* uargs是使用者态傳入的參數
* flags是使用者态傳入的flags
後面的部分則全部是load_module的代碼流程分析.
注: 為了區分,後續提及到的
- 子產品ELF檔案: 指的是ELF二進制檔案
- 子產品記憶體ELF檔案: 指的是加載到記憶體中的ELF二進制檔案
- 子產品記憶體布局: 指的是最終核心記憶體配置設定各子產品的運作位址空間
1. 定義子產品結構體 mod
struct module *mod;
在子產品加載中,info結構體記錄的是子產品的二進制資訊,而mod記錄的則是子產品的記憶體資訊。
2. ELF檔案頭檢查
err = elf_header_check(info);
if (err)
goto free_copy;
elf_header_check檢查ELF基本格式是否正确,包括:
* 判斷檔案大小是否小于一個标準ELF檔案頭,若小于則代表ELF檔案格式錯誤
* 檢查ELF檔案頭格式,檔案必須以"\177ELF"開頭,檔案類型必須為Relocatable
* 檢查平台相關的檔案頭檢查,以及确定節區頭部表表項大小是否和目前核心Elf_Shdr類型大小相同
* 檢查整個節區頭部表是否在可執行檔案範圍内
static int elf_header_check(struct load_info *info)
{
//判斷檔案大小是否小于一個标準ELF檔案頭,若小于則代表ELF檔案格式錯誤
if (info->len < sizeof(*(info->hdr)))
return -ENOEXEC;
//檢查ELF檔案頭格式,檔案必須以"\177ELF"開頭,檔案類型必須為Relocatable
//平台相關的檔案頭檢查,以及确定節區頭部表表項大小是否和目前核心Elf_Shdr類型大小相同
if (memcmp(info->hdr->e_ident, ELFMAG, SELFMAG) != 0
|| info->hdr->e_type != ET_REL
|| !elf_check_arch(info->hdr)
|| info->hdr->e_shentsize != sizeof(Elf_Shdr))
return -ENOEXEC;
//檢查整個節區頭部表是否在可執行檔案範圍内
if (info->hdr->e_shoff >= info->len
|| (info->hdr->e_shnum * sizeof(Elf_Shdr) >
info->len - info->hdr->e_shoff))
return -ENOEXEC;
return 0;
}
3. 初始化 info中大部分結構體
err = setup_load_info(info, flags);
if (err)
goto free_copy;
struct load_info info中記錄的是子產品記憶體二進制的頭資訊,setup_load_info函數主要負責初始化info中的資訊,包括:
* 根據ELF檔案頭,确定節區頭部表首位址,記錄到info->sechdrs
* 根據ELF檔案頭,确定節區頭部表字元串表首位址,記錄到info->secstrings
* 周遊節區頭部表,确定".modinfo"節區索引,記錄到info->index.info
* 根據.modinfo段的"name=xxx"字段确定子產品名,記錄到info->name
* 周遊節區頭部表,确定屬性為SHT_SYMTAB的節區索引,記錄到info->index.sym(目标檔案正常應該隻有一個SHT_SYMTAB節區,為靜态符号表.symtab節區)
* 根據靜态連結符号表節區索引(info->index.sym),确定靜态連結字元串表節區索引,記錄到info->index.str
* 周遊節區頭部表,查找名為".gnu.linkonce.this_module"的節區索引,記錄到info->index.mod
* 根據info->index.mod,确定子產品二進制中 __this_module變量在核心記憶體二進制中的位址,記錄到info->mod
* 若.modinfo段沒有定義"name=xxx",則設定info->name為 info->mod->name
* 周遊節區頭部表,查找名稱為"__versions"的節區,記錄到 info->index.vers, 若子產品加載參數指定了MODULE_INIT_IGNORE_MODVERSIONS,那麼vers設定為0
* 周遊節區頭部表,查找名為".data..percpu"的節區,記錄到info->index.pcpu
這裡不列舉具體代碼,隻說明struct load_info結構體各個字段的作用:
/*
load_info中記錄的都是子產品記憶體二進制的相關資訊,或者說是非真正運作的核心代碼相關的資訊,
而真正核心代碼(/資料)記憶體配置設定後的資訊,都記錄在struct module中.
*/
struct load_info {
/*
從.modinfo節區的"name=xxx"字段擷取的子產品名,見setup_load_info
若.modinfo中沒有指定"name=xxx"字段,則直接從 mod.name中擷取子產品名
*/
const char *name;
/* pointer to module in temporary copy, freed at end of load_module() */
/*
指向子產品中".gnu.linkonce.this_module"段的起始位址,同時也是子產品記憶體二進制中變量struct module __this_module的起始位址
(通過字元串".gnu.linkonce.this_module"查找的,見setup_load_info)
*/
struct module *mod;
/*
子產品的二進制複制到核心後,此指針記錄子產品二進制的起始位址,因為ELF一開始的内容就是一個ELF檔案頭結構(ELF_Ehdr),是以
hdr也代表此ELF檔案頭的首位址.
*/
Elf_Ehdr *hdr;
/*
整個ko的長度,是從檔案中讀入的長度
*/
unsigned long len;
/*
子產品記憶體二進制的節區頭部表指針,見setup_load_info
rewrite_section_headers之後,節區頭部表中的内容會指向目前二進制在記憶體的真正位置,如:
* ELF_Shdr[x].sh_addr: 對于所有載入記憶體的節區(屬性中未标記SHT_NOBITS的),其指向此節區x在記憶體的首位址
對于所有非載入記憶體的節區,保持檔案中值不變.
*/
Elf_Shdr *sechdrs;
/*
secstrings是節區名字元串表的起始位址,對應.shstrtab節區
strtab是靜态連結符号表字元串表的起始位址,對應.stratb節區(通過查找唯一擁有SHT_SYMTAB屬性的節區确定,見setup_load_info)
*/
char *secstrings, *strtab;
/*
設四個字段都是在layout_symtab中設定的:
symoffs: 記錄的是子產品的核心符号表在core_layout中的起始偏移
stroffs: 記錄的是子產品的核心符号表字元串表在core_layout中的起始偏移
core_typeoffs: 記錄的是核心符号表的類型表(char數組,每個符号表項一個)在core_layout中的起始偏移
init_typeoffs: 記錄的是靜态連結符号表的類型表(char數組,每個符号表項一個)在init_layout中的起始偏移
在init_layout中還記錄了子產品的整個連結符号表,但info中并沒有記錄其中的symoffs,stroffs,這是因為對于
init_layout來說,這兩個偏移直接記錄到靜态連結符号表/字元串表的節區頭部表symsect->sh_entsize字段中了.
*/
unsigned long symoffs, stroffs, init_typeoffs, core_typeoffs;
struct _ddebug *debug;
unsigned int num_debug;
/*
若子產品簽名檢查通過,則會設定此字段為true,見module_sig_check
*/
bool sig_ok;
#ifdef CONFIG_KALLSYMS
/*
見layout_symtab
init_layout後面插入了一個mod_kallsyms結構體,這個結構體在後面add_kallsyms函數向核心插入子產品符号時候會用到
mod_kallsyms_init_off儲存這個結構體在init_layout的偏移
core/init_layout都需要一個mod_kallsyms結構體記錄其符号表相關資訊:
* 對于core_layout來說, 此結構體直接記錄在 mod->core_kallsyms中
* 對于init_layout來說, 此結構體則隻在mod->kallsyms中記錄了一個指針,而真正的配置設定則是配置設定在了init_layout
的代碼段(因為init之後此結構體就沒用了),而mod_kallsyms_init_off記錄的則是init_layout的那個mod_kallsyms
結構體在init_layout段中的偏移,後面會根據此偏移計算 mod->kallsyms的指針.
*/
unsigned long mod_kallsyms_init_off;
#endif
/*
這些字段都是在setup_load_info函數中初始化的,index.*記錄的都是節區的索引号
sym: 目前ko的靜态連結符号表節區的索引,對應.symtab節區(通過查找唯一擁有SHT_SYMTAB屬性的節區确定)
str: 目前Ko的靜态連結符号表字元串表節區的索引,對應.strtab節區(通過查找唯一擁有SHT_SYMTAB屬性的節區确定,同時記錄到*strtab中)
mod: 目前ko記憶體二進制中的struct modules __this_module結構體的内幕才能位址(通過字元串".gnu.linkonce.this_module"查找的)
vers: 記錄ko中__versions節區的索引号(通過字元串"__versions"查找的);
* 若目前的核心加載時傳入參數MODULE_INIT_IGNORE_MODVERSIONS,則忽略子產品的__versions段,vers索引為0
* 若目前子產品中沒有"__versions"段(說明子產品編譯時沒開CONFIG_MODVERSIONS),那麼vers段索引也為0
info: 目前ko中.modinfo節區的索引号(通過字元串".modinfo"查找的)
pcpu: 目前ko中.data..precpu節區的索引号(通過字元串".data..percpu"查找的)
*/
struct {
unsigned int sym, str, mod, vers, info, pcpu;
} index;
};
4.子產品加載黑名單過濾(為支援核心調試啟動)
if (blacklisted(info->name)) {
err = -EPERM;
goto free_copy;
}
這裡隻是一個簡單的子產品名字元串過濾,在黑名單中的子產品則不會被加載,子產品名來源自info->name,實際上是來自于子產品的二進制(此功能對安全并沒有本質幫助,其設計應該也不是為了安全的目的,而是為了核心啟動調試使用的)
5.驗證子產品簽名
err = module_sig_check(info, flags);
if (err)
goto free_copy;
子產品簽名流程可簡化為下圖:
module_sign_check函數:
1) 先檢查ko是否以字元串"~Module signature appended~"結尾,若非此結尾,則代表此子產品沒有簽名
2)從子產品尾部提取 struct module_signature結構體,其中的sig_len是在module_signature之前的pkcs7簽名資料長度
3)最終校驗的部分是從子產品頭開始,到raw_pkcs7 signature之前(不包括)的所有資料内容
子產品簽名若校驗失敗,是否直接報錯取決于CONFIG_MODULE_SIG_FORCE是否開啟,若開啟則直接報錯,否則隻記錄(後面标記核心為tainted,見10)
6.修正記憶體子產品二進制的節區頭部表(info->sechdrs[])
err = rewrite_section_headers(info, flags);
if (err)
goto free_copy;
此函數主要負責三步操作:
1)修正子產品記憶體二進制中所有檔案中存在的段(非SHT_NOBITS)的Elf_Shdr[x].sh_addr指針,讓節區頭部表能真正指向到記憶體中此節區的記憶體.
2)标記__versions段和.modinfo段不載入記憶體
3)若未開啟CONFIG_MODULE_UNLOAD(子產品解除安裝功能),則.exit段也設定不載入記憶體,此時核心子產品無法解除安裝.
正常dll/exe檔案中,程式頭部表中記錄某個節區是否載入記憶體,但由于子產品是目标檔案,其沒有程式頭部表,故這裡實際上是修改了節區頭部表的SHF_ALLOC來标記此節區後續是否會加載到記憶體的.
static int rewrite_section_headers(struct load_info *info, int flags)
{
unsigned int i;
/*
若節區要加載到記憶體,則sh_addr給出節區第一個位元組應出現的記憶體位置
*/
info->sechdrs[0].sh_addr = 0;
//周遊所有節區頭部表,這裡檢查所有載入記憶體的節區,這些節區的内容(根據偏移比較)必須在ELF長度範圍之内
for (i = 1; i < info->hdr->e_shnum; i++) {
Elf_Shdr *shdr = &info->sechdrs[i];
/*
sh_offset是節區到檔案頭的偏移,sh_size是節區長度;
這裡檢查若此節區占用記憶體中的空間,則目前節區頭部表指向的節區必須在elf檔案範圍内,否則提示越界
*/
if (shdr->sh_type != SHT_NOBITS
&& info->len < shdr->sh_offset + shdr->sh_size) {
pr_err("Module len %lu truncated\n", info->len);
return -ENOEXEC;
}
//sh_addr直接寫成此段在記憶體鏡像中的偏移
shdr->sh_addr = (size_t)info->hdr + shdr->sh_offset;
#ifndef CONFIG_MODULE_UNLOAD
/* Don't load .exit sections */
/*
若沒有指定CONFIG_MODULE_UNLOAD,那麼如果遇到.exit段,則設定其不載入記憶體
也就是說,隻有開啟了CONFIG_MODULE_UNLOAD,子產品才能解除安裝
*/
if (strstarts(info->secstrings+shdr->sh_name, ".exit"))
shdr->sh_flags &= ~(unsigned long)SHF_ALLOC;
#endif
}
//__versions段和.modinfo段不加載入記憶體
info->sechdrs[info->index.vers].sh_flags &= ~(unsigned long)SHF_ALLOC;
info->sechdrs[info->index.info].sh_flags &= ~(unsigned long)SHF_ALLOC;
return 0;
}
7.檢查核心和子產品中module_layout符号的CRC是否相同
此函數負責檢查目前核心module_layout符号(函數)的CRC和目前要加載子產品中記錄的module_layout函數的crc是否比對:
* 若核心或子產品中未找到module_layout符号的CRC則報warning
* 若子產品中沒有__versions字段,則:
- 若核心開啟了CONFIG_MODULE_FORCE_LOAD,則隻報warning
- 若核心未開啟CONFIG_MODULE_FORCE_LOAD,則error
子產品指定加載參數MODULE_INIT_IGNORE_MODVERSIONS也屬于此種情況
* 若二者CRC都存在,但比對失敗,則報error
if (!check_modstruct_version(info, info->mod)) {
err = -ENOEXEC;
goto free_copy;
}
module_layout是核心中的一個空函數,其參數是核心子產品加載時需要和核心完全比對的結構體,定義如下:
#ifdef CONFIG_MODVERSIONS
void module_layout(struct module *mod, struct modversion_info *ver,
struct kernel_param *kp, struct kernel_symbol *ks, struct tracepoint * const *tp)
{
}
EXPORT_SYMBOL(module_layout);
#endif
由于CRC計算時參數類型是完全展開參與運算的,若CRC比對則可以代表函數的參數的類型,以及參數展開定義是比對的,若module_layout的CRC比對,則可以說明目前核心對其參數中結構體的定義,和子產品編譯環境的核心對這些結構體的定義相同,這樣子產品加載時這些結構體的解引用就不會出現問題.
在子產品編譯期間,Stage2 modpost程式會對讀入的每個單/複合目标檔案(%.o)都增加未定義符号"module_layout",後面modpost再未定義符号決議時就會找到目前核心中module_layout函數的CRC(從vmlinux的符号表中擷取),并将其記錄到目前子產品的未定義符号"module_layout"中,這會導緻後續module_layout的CRC被輸出到子產品的導出表對應的CRC表(____versions數組,所有未定義符号的CRC都會輸出到這裡)中,是以子產品中就記錄了其編譯環境中module_layout的CRC,也就是相關結構體的比對CRC.
而核心子產品加載時會比較子產品中module_layout函數的CRC和目前核心module_layout函數的CRC是否比對(後者存儲在核心的__kcrctab數組中,通過find_symbol函數可查到),如果比對則代表子產品可加載到目前核心,否則子產品不應該被加載到目前核心.
8.layout_and_allocate
layout_and_allocate函數的作用是:檢查modinfo中各個字段是否正确,根據配置處理長跳轉(plt),計算并在虛拟記憶體範圍[module_alloc_base,module_alloc_end]内配置設定真正給子產品使用的記憶體(預設是vmalloc配置設定),在計算的過程中會分别得出core_layout/init_layout中的RO/RX/RW/ro_after_init段,并生成core symbols的符号表,最終傳回一個struct module結構體,這和info.mod中的不同,後者是檔案中的this_module,前者傳回是最終子產品記憶體中的this_module,代碼如下:
8.1. 檢查子產品.modinfo節區的vermagic、intree、retpoline、staging、livepatch、license等字段,并确定核心是否為tainted
err = check_modinfo(info->mod, info, flags);
if (err)
return ERR_PTR(err);
其中的檢查内容包括:
- vermagic=:中SMP preempt mod_unload modversions aarch64字段是否一緻,不一緻直接加載失敗(代表核心有本質差異),除非使用者參數手動指定強制加載,若強制加載則此時核心會被标記為tainted.
- intree=: 是否标記了,沒标記代表目前子產品是外部(out-of-tree)子產品,直接标記tainted.
- retpoline=: 代表是否開啟了retpoline特性,若核心開啟了子產品沒開啟則warning
- staging=: 代表代碼來自staging目錄,這裡的代碼品質不保證,核心标記為tainted
- livepatch=: 目前子產品和livepatch有關,核心标記為tainted,細節先pass
- license=: 根據license字段,來判斷是否是GPL相容,不相容核心标記tainted
8.2 計算要為.plt/.init.plt預留多少空間
err = module_frob_arch_sections(info->hdr, info->sechdrs,
info->secstrings, info->mod);
if (err < 0)
return ERR_PTR(err);
module_frob_arch_sections函數本身是個weak函數,若平台沒有相關實作的話此函數為空,在arm64平台是有相關實作的:
int module_frob_arch_sections(Elf_Ehdr *ehdr, Elf_Shdr *sechdrs,
char *secstrings, struct module *mod)
{
unsigned long core_plts = 0;
unsigned long init_plts = 0;
Elf64_Sym *syms = NULL;
Elf_Shdr *pltsec, *tramp = NULL;
int i;
/*
這個函數是在開啟了CONFIG_ARM64_MODULE_PLTS後才調用到的,而開啟此config同時也會
為子產品加傳入連結接腳本module.lds,在此腳本就5行:
SECTIONS {
.plt (NOLOAD) : { BYTE(0) }
.init.plt (NOLOAD) : { BYTE(0) }
.text.ftrace_trampoline (NOLOAD) : { BYTE(0) }
}
.plt等段是空的,且預設不加載到記憶體,這裡的代碼是找到這些節區的索引号,并同時找到靜态連結符号表
*/
for (i = 0; i < ehdr->e_shnum; i++) {
if (!strcmp(secstrings + sechdrs[i].sh_name, ".plt"))
mod->arch.core.plt_shndx = i;
else if (!strcmp(secstrings + sechdrs[i].sh_name, ".init.plt"))
mod->arch.init.plt_shndx = i;
else if (IS_ENABLED(CONFIG_DYNAMIC_FTRACE) &&
!strcmp(secstrings + sechdrs[i].sh_name,
".text.ftrace_trampoline"))
tramp = sechdrs + i;
else if (sechdrs[i].sh_type == SHT_SYMTAB)
//動态/靜态連結符号表都是SHT_SYMTAB類型,但由于子產品本質是目标檔案,故這裡隻能是.symtab
syms = (Elf64_Sym *)sechdrs[i].sh_addr;
}
/*
若子產品init.plt或.plt有一個沒找到,則直接報錯
*/
if (!mod->arch.core.plt_shndx || !mod->arch.init.plt_shndx) {
pr_err("%s: module PLT section(s) missing\n", mod->name);
return -ENOEXEC;
}
/*
若靜态連結符号表沒找到,則直接報錯
*/
if (!syms) {
pr_err("%s: module symtab section missing\n", mod->name);
return -ENOEXEC;
}
/*
這個for循環主要是處理ko(目标檔案)中,有靜态連結重定位資訊的可執行段中的重定位,
正常plt表是連結時生成的,這裡實際上是模拟了靜态連結生成plt表的過程,最終:
* 對ko(目标檔案)中所有可執行段的重定位位置,都調用count_plts函數來确定是否生成一個重定位位置
* count_plts函數正常隻處理JUMP26/CALL26,對于這兩種類型的跳轉,如果目标是在同一個段内的函數則忽略(傳回0),其他基本上都是要生成plt的(傳回1)
這個for循環最終統計.plt和.init.plt段中需要生成的表項的數目,但未做最終的生成.
*/
for (i = 0; i < ehdr->e_shnum; i++) {
/*
根據節區頭部表找到節區首位址
*/
Elf64_Rela *rels = (void *)ehdr + sechdrs[i].sh_offset;
//按照節區大小轉換成重定位表項數
int numrels = sechdrs[i].sh_size / sizeof(Elf64_Rela);
//對于重定位表節區頭部表來說,sh_info指向其要重定位的節區的節區頭部表索引
//這裡直接+就指向待重定位節區的節區頭部表
Elf64_Shdr *dstsec = sechdrs + sechdrs[i].sh_info;
//若目前節區類型非RELA,則pass, ELF64中好像不用SHT_REL,隻是SHT_RELA作為重定向了
if (sechdrs[i].sh_type != SHT_RELA)
continue;
/* ignore relocations that operate on non-exec sections */
/*
若此節區中沒有可執行代碼,則忽略此節區
*/
if (!(dstsec->sh_flags & SHF_EXECINSTR))
continue;
/* sort by type, symbol index and addend */
/*
根據重定位表的 type, 符号索引和addend排序
*/
sort(rels, numrels, sizeof(Elf64_Rela), cmp_rela, NULL);
/*
若重定位表重定位的節區是".init",開頭的,則都記錄到init_plts中
若非.init開頭的,都記錄到core_plts中
*/
if (strncmp(secstrings + dstsec->sh_name, ".init", 5) != 0)
//此函數對重定位中的部分case進行處理,如R_AARCH64_JUMP26,這裡傳回重定位的數量
//這個函數正常隻處理JUMP26/CALL26,對于這兩種類型的跳轉,如果目标是在同一個段内的函數則忽略(傳回0),其他基本上都是要生成plt的(傳回1)
core_plts += count_plts(syms, rels, numrels,
sechdrs[i].sh_info, dstsec);
else
init_plts += count_plts(syms, rels, numrels,
sechdrs[i].sh_info, dstsec);
}
//擷取plt段
pltsec = sechdrs + mod->arch.core.plt_shndx;
pltsec->sh_type = SHT_NOBITS; //這是不占用檔案空間的意思
pltsec->sh_flags = SHF_EXECINSTR | SHF_ALLOC; //代表要配置設定到記憶體
pltsec->sh_addralign = L1_CACHE_BYTES;
//plt段大小為core_plts + 1,這裡多預留了一個,應該是因為plt表中最後一個元素應該為空
pltsec->sh_size = (core_plts + 1) * sizeof(struct plt_entry);
//這個代表目前已經使用了多少個表項
mod->arch.core.plt_num_entries = 0;
//一共要生成多少個表項
mod->arch.core.plt_max_entries = core_plts;
pltsec = sechdrs + mod->arch.init.plt_shndx;
pltsec->sh_type = SHT_NOBITS;
//plt兩個段都設定為要載入記憶體
pltsec->sh_flags = SHF_EXECINSTR | SHF_ALLOC;
pltsec->sh_addralign = L1_CACHE_BYTES;
//這裡大小設定了,配置設定空間時會給其配置設定空間的
pltsec->sh_size = (init_plts + 1) * sizeof(struct plt_entry);
mod->arch.init.plt_num_entries = 0;
mod->arch.init.plt_max_entries = init_plts;
if (tramp) {
//trampoline的
tramp->sh_type = SHT_NOBITS;
tramp->sh_flags = SHF_EXECINSTR | SHF_ALLOC;
tramp->sh_addralign = __alignof__(struct plt_entry);
tramp->sh_size = sizeof(struct plt_entry);
}
return 0;
}
此函數實際上負責為子產品(目标檔案)計算生成plt/init.plt段中需要有多少個元素,結果儲存到 mod->arch.core/init中(每個段多預留了一個位置)
1) 此函數先确定子產品中是否擁有.plt和.init.plt節區,若這兩個節區不存在則直接報錯. 在子產品連結時,若開啟了CONFIG_ARM64_MODULE_PLTS選項,那麼連結腳本會增加module.lds,其内容是生成三個空的節區:
SECTIONS {
.plt (NOLOAD) : { BYTE(0) }
.init.plt (NOLOAD) : { BYTE(0) }
.text.ftrace_trampoline (NOLOAD) : { BYTE(0) }
}
而目前函數也是在開啟CONFIG_ARM64_MODULE_PLTS才存在的,故目前代碼運作時.plt和.init.plt節區應該預設存在(目标檔案本來沒有這兩個節區,需要通過腳本手動建構). 連結時生成的.plt和.init.plt節區是沒有任何内容的兩個空節區,因為其作用隻是在ELF檔案中提供兩個節區頭部表項來記錄動态生成的.plt/.init.plt資訊,是以其節區内容是空,在二進制檔案中隻保留節區頭部表表項.
2) 然後此函數周遊子產品記憶體二進制中所有類型為SHT_RELA的節區(對于目标檔案,此類型節區儲存的是靜态連結的重定位資訊,目标檔案沒有也不需要動态連結重定位資訊,對于arm64 其重定位類型通常都隻用SHT_RELA),對于所有重定位類型為JUMP26/CALL26的表項(是間接跳轉的代碼),若其跳轉的目的位址和目前代碼位址不在同一個段中,則記錄為一個plt表項. 若目前代碼在.init開頭的段,那麼就記錄在init_plts表項中,否則記錄在plts表項中 3) 最終将申請的plts/init_plts表項數目記錄到mod->arch.core/init結構體中,這個結構體是用來計數的,後面用來記錄已經使用了多少個plt表項. 4) 最終設定子產品二進制.plt/.init.plt段的節區頭部表設定為: * pltsec->sh_type = SHT_NOBITS; //代表plt表本身不占用檔案空間的意思 * pltsec->sh_flags = SHF_EXECINSTR | SHF_ALLOC; //plt節區是要配置設定到記憶體的,且是可執行的 * pltsec->sh_size = (core_plts + 1) * sizeof(struct plt_entry);//plt節區需要的子產品記憶體布局空間,在計算記憶體布局(layout_section)時依賴此字段 為plt配置設定空間 這裡使用子產品記憶體節區頭部表來記錄plt資訊的原因是因為後面記憶體配置設定的時候預設的操作就是掃描節區頭部表,并根據是否為SHF_ALLOC來決定是否預留記憶體,SHF_EXECINSTR決定記憶體屬性,這樣可以原樣利用核心此流程. 注: 若未開啟核心位址随機化(CONFIG_RANDOMIZE_BASE),則代表子產品4GB随機化(CONFIG_RANDOMIZE_MODULE_REGION_FULL)也沒開啟,那麼.plt/.init.plt預留表項大小為1(就是附加的一個),因為此時CALL26/JUMP26在跳轉範圍内,不需要plt構造長跳轉. 8.3 修正其他段屬性
//percpu段去掉alloc屬性,後續重新配置設定
info->sechdrs[info->index.pcpu].sh_flags &= ~(unsigned long)SHF_ALLOC;
//".data..ro_after_init"段加上ro_after_init屬性
ndx = find_sec(info, ".data..ro_after_init");
if (ndx)
info->sechdrs[ndx].sh_flags |= SHF_RO_AFTER_INIT;
//__jump_table段也标記為ro_after_init
ndx = find_sec(info, "__jump_table");
if (ndx)
info->sechdrs[ndx].sh_flags |= SHF_RO_AFTER_INIT;
這裡包括三個操作: 1) 去除子產品記憶體二進制percpu段的SHF_ALLOC屬性,percpu段後面會在核心重新配置設定記憶體 2) 添加".data..ro_after_init段屬性SHF_RO_AFTER_INIT,這個屬性應該是核心自己定義的,在ELF标準中應該沒有,在核心中根據節區名來設定此段為ro_after_init 3) 添加"__jump_table"段屬性SHF_RO_AFTER_INIT,同上. 8.4 (僅)計算子產品記憶體ELF節區最終需要的記憶體大小和總大小,并記錄到core_layout/init_layout中
layout_sections(info->mod, info);
此函數的主要作用是掃描子產品二進制的所有節區,并根據節區的屬性(如SHF_ALLOC代表配置設定到記憶體,SHF_EXECINSTR代表可執行,SHF_WRITE代表可寫, SHF_RO_AFTER_INIT代表ro_after_init),将其歸類到core_layout或init_layout的不同段中,這裡最終隻計算了歸類後core_layout和init_layout中各個段的大小,并沒有實際配置設定記憶體.
這裡的記憶體布局并非最終的記憶體布局,代碼/隻讀/ro_after_init段都已經是最終布局了,但後面在layout_symtab函數中(8.5)還會追加符号表相關的資料到core/init_layout的資料段,後者才最終确定core/init_layout的大小, 這裡隻是計算節區的布局,并不是計算能最終的布局 .
static void layout_sections(struct module *mod, struct load_info *info)
{
/*
masks數組中:
[][0]:代表的是節區必須擁有這裡指定的flag
[][1]:代表的是節區必須沒有這裡指定的flag
*/
static unsigned long const masks[][2] = {
/* NOTE: all executable code must be the first section
* in this array; otherwise modify the text_size
* finder in the two loops below */
{ SHF_EXECINSTR | SHF_ALLOC, ARCH_SHF_SMALL }, //RX
{ SHF_ALLOC, SHF_WRITE | ARCH_SHF_SMALL }, //RO
{ SHF_RO_AFTER_INIT | SHF_ALLOC, ARCH_SHF_SMALL }, //ro_after_init
{ SHF_WRITE | SHF_ALLOC, ARCH_SHF_SMALL }, //RW
{ ARCH_SHF_SMALL | SHF_ALLOC, 0 } //ARCH_SHF_SMALL應該是0,不知道幹啥的,這裡應該是其他配置設定到記憶體的段
};
unsigned int m, i;
//周遊所有節區頭部表表項,設定每個段的表項的大小為按位取反為0xffffffff
for (i = 0; i < info->hdr->e_shnum; i++)
info->sechdrs[i].sh_entsize = ~0UL;
pr_debug("Core section allocation order:\n");
/*
周遊所有的masks,是按照屬性在core_layout中增加的節區.
*/
for (m = 0; m < ARRAY_SIZE(masks); ++m) {
/*
為每個mask分别周遊所有節區,對于滿足mask條件的非.init開頭的節區,将其大小加到mod.core.size中,并将此節區的起始偏移記錄到sh_entsize按中
*/
for (i = 0; i < info->hdr->e_shnum; ++i) {
//elf64_hdr 為例
Elf_Shdr *s = &info->sechdrs[i];
//擷取此節區名
const char *sname = info->secstrings + s->sh_name;
/*
如果此節區:
* masks[m][0]中的bit沒有設定,則直接continue,也就是說[0]是必須設定的bit
* 設定了mask[m][1],則直接continue,也就是說[1]是必須不能設定的bit
* 若節區是.init開頭的,則直接continue
*/
if ((s->sh_flags & masks[m][0]) != masks[m][0]
|| (s->sh_flags & masks[m][1])
|| s->sh_entsize != ~0UL
|| strstarts(sname, ".init"))
continue;
//此函數傳回目前section在mod.core中的起始位址偏移,在section之前實際上可以增加一些段前自定義的填充内容
//這裡實際上是修改了mod.core.size,已經加上了目前段
s->sh_entsize = get_offset(mod, &mod->core_layout.size, s, i);
pr_debug("\t%s\n", sname);
}
/*
前面是按照屬性周遊節區,這裡在core_layout中設定各個段的大小
*/
switch (m) {
case 0: /* executable */
//做代碼對齊
mod->core_layout.size = debug_align(mod->core_layout.size);
//這裡設定代碼段大小
mod->core_layout.text_size = mod->core_layout.size;
break;
case 1: /* RO: text and ro-data */
mod->core_layout.size = debug_align(mod->core_layout.size);
//設定隻讀段大小
mod->core_layout.ro_size = mod->core_layout.size;
break;
case 2: /* RO after init */
mod->core_layout.size = debug_align(mod->core_layout.size);
//ro_after_init大小
mod->core_layout.ro_after_init_size = mod->core_layout.size;
break;
case 4: /* whole core */
mod->core_layout.size = debug_align(mod->core_layout.size);
break;
}
}
//為.init做類似的操作,但注意init中應該不包括ro_after_init段,故直接用ro_size即可
//如果出現了.init開頭的段擁有SHF_RO_AFTER_INIT屬性,那麼實際上是歸到最後的rw段了
pr_debug("Init section allocation order:\n");
for (m = 0; m < ARRAY_SIZE(masks); ++m) {
for (i = 0; i < info->hdr->e_shnum; ++i) {
Elf_Shdr *s = &info->sechdrs[i];
const char *sname = info->secstrings + s->sh_name;
if ((s->sh_flags & masks[m][0]) != masks[m][0]
|| (s->sh_flags & masks[m][1])
|| s->sh_entsize != ~0UL
|| !strstarts(sname, ".init"))
continue;
s->sh_entsize = (get_offset(mod, &mod->init_layout.size, s, i)
| INIT_OFFSET_MASK);
pr_debug("\t%s\n", sname);
}
switch (m) {
case 0: /* executable */
mod->init_layout.size = debug_align(mod->init_layout.size);
mod->init_layout.text_size = mod->init_layout.size;
break;
case 1: /* RO: text and ro-data */
mod->init_layout.size = debug_align(mod->init_layout.size);
mod->init_layout.ro_size = mod->init_layout.size;
break;
case 2:
/*
* RO after init doesn't apply to init_layout (only
* core_layout), so it just takes the value of ro_size.
*/
mod->init_layout.ro_after_init_size = mod->init_layout.ro_size;
break;
case 4: /* whole init */
mod->init_layout.size = debug_align(mod->init_layout.size);
break;
}
}
}
正常二進制程式的記憶體布局記錄在其程式頭部表中,而子產品作為目标檔案隻有節區頭部表,并沒有程式頭部表,這裡的做法類似于動态的為目标檔案構造一個程式頭部表(但結構比程式頭部表要簡單的多),核心中使用struct module_layout結構體來代表子產品的記憶體布局,對于Init和非init的代碼/資料,分别有一個對應的struct module_layout core_layout和init_layout.
module_layout以偏移的形式記錄子產品的代碼和資料的布局,實際上裡面除了基位址(base)外,就包括4個偏移,基于此4個偏移,核心子產品中可以有(且隻有)4個屬性的段,此4個偏移和4個段分别為: * .text_size: [0, text_size]都為代碼段 * .ro_size: [text_size, ro_size]都為隻讀段 * .ro_after_init_size: [ro_size, ro_after_init_size]都為ro_after_init段 * .size: [ro_after_init_size, size] 都為資料段 注: 這裡需要注意的是,在計算記憶體布局的過程中,get_offset => arch_mod_section_prepend函數會檢查每個節區前是否需要預留一部分記憶體空間. module_layout中隻按照屬性記錄了記憶體布局, 而每個節區屬于哪個layout,在layout的内部偏移是多少,是記錄在此節區 節區頭部表的sh_entsize字段中的 8.5: 在init_layout為靜态連結符号表,core_layout為核心符号表預留白間
layout_symtab(info->mod, info);
在子產品加載過程中,靜态連結符号表(及對應的靜态連結符号表字元串表)會被放到init_layout資料段的尾部,同時從其靜态連結符号表中提取部分符号資訊儲存到運作時的子產品記憶體中(core_layout的資料段,這一部分被保留的符号稱為核心符号表(core symbol)),而 此函數就負責在init_layout/core_layout中為靜态連結符号表和核心符号表預留白間. 核心符号表的判斷條件是: 1) 符号表編号為0的符号,或livepatch相關所有符号均為核心符号 2) 未定義符号,節區資訊錯誤的符号,init_layout段包含的節區中的的符号都不是核心符号 3) core_layout段包含的節區的符号分兩種情況: - 若開啟了 CONFIG_KALLSYMS_ALL,那麼core_layout段包含的節區中的符号均為核心符号 - 若未開啟 CONFIG_KALLSYMS_ALL, 那麼隻有core_layout段包含的可執行代碼節區中的符号才為核心符号 此函數在init_layout/core_layout中預留的空間包括: * core_layout: - 為核心符号表預留白間(Elf_sym*),起始偏移記錄到 info->symoffs - 為核心符号表字元串表預留白間,起始偏移記錄到 info->stroffs - 為核心符号表類型表預留白間(char數組),起始位址記錄到 info->core_typeoffs * init_layout: - 為靜态連結符号表預留白間,起始偏移記錄到靜态連結符号表節區頭部表表項的symsect->sh_entsize字段 - 為靜态連結符号表字元串表預留白間,起始偏移記錄到靜态連結符号表字元串表節區頭部表表項的symsect->sh_entsize字段 - 為靜态連結符号表預留一個mod_kallsyms結構體(這個實際上跟靜态連結符号表節區頭部表類似),起始偏移記錄到 info->mod_kallsyms_init_off - 為靜态連結符号
static void layout_symtab(struct module *mod, struct load_info *info)
{
//靜态連結符号表節區頭部表位址
Elf_Shdr *symsect = info->sechdrs + info->index.sym;
//靜态連結符号表對應的字元串表的節區頭部表位址
Elf_Shdr *strsect = info->sechdrs + info->index.str;
const Elf_Sym *src;
unsigned int i, nsrc, ndst, strtab_size = 0;
/* Put symbol section at end of init part of module. */
/*
這裡标記靜态連結符号表運作時會被加載到記憶體(最終放到了init_layout布局中的資料段)
*/
symsect->sh_flags |= SHF_ALLOC;
/*
sh_entsize字段本來是記錄節區固定長度表項大小的,但核心中實際上并沒使用此字段,核心都是用自己的
結構體來解析所有表項長度的(如符号表的表項大小就是通過sizeof(Elf_Sym)計算的),故此字段在子產品加載時
實際上沒有用,在核心中複用此字段來記錄一些節區在core/init_layout中的偏移,以及到底是屬于哪個layout.
sh_entsize标記為INIT_OFFSET_MASK,則代表此段應該在init_layout中配置設定空間
這裡在init_layout結尾又追加了資料記憶體,追加的記憶體空間大小是整個靜态連結符号表的大小,init_layout
中最終靜态連結符号表的記憶體偏移傳回到symsect->sh_entsize中.
*/
symsect->sh_entsize = get_offset(mod, &mod->init_layout.size, symsect,
info->index.sym) | INIT_OFFSET_MASK;
//輸出靜态連結符号表的節區名
pr_debug("\t%s\n", info->secstrings + symsect->sh_name);
//擷取靜态連結符号表記憶體首位址
src = (void *)info->hdr + symsect->sh_offset;
//這裡擷取的是靜态連結符号表中共有多少個表項
nsrc = symsect->sh_size / sizeof(*src);
/* Compute total space required for the core symbols' strtab. */
/*
此函數周遊靜态連結符号表中所有的表項,也就是周遊目标檔案中的所有符号,其目的是計算
最終要儲存在核心子產品記憶體中的符号資訊,和對應的字元串資訊.
保留的符号(增加引用計數的符号)包括:
1.符号表中符号0保留;
2.若是livepatch則所有符号都保留;
3.大部分core_layout布局中節區的符号表均保留,包括:
* 正常非配置設定到init_layout中的且會加載到記憶體的代碼段節區中的,在目前子產品定義的符号.
* 若開啟了CONFIG_KALLSYMS_ALL,則同時也包括pcpundx(一般是percpu段)段的符号,以及會加載到記憶體的非init_layout中的資料段符号
4.未定義符号,節區不對的符号,init_layout的符号都不加入
被保留的符号後面稱為子產品的核心符号表
*/
for (ndst = i = 0; i < nsrc; i++) {
if (i == 0 || is_livepatch_module(mod) ||
is_core_symbol(src+i, info->sechdrs, info->hdr->e_shnum,
info->index.pcpu)) {
//這裡strlen計算的是字元串字元個數,後面要加一個0結尾,是以要加一個位元組
strtab_size += strlen(&info->strtab[src[i].st_name])+1;
//計算符号結構體空間
ndst++;
}
}
/*
在core_layout尾部為核心符号和對應的符号名字元串預留白間
實際上這一步類似建構符号表和對應的符号表字元串表,但内容應該和靜态/動态連結符号表都不太一樣????
*/
/* Append room for core symbols at end of core part. */
//位址對齊,info->symoffs是對齊後的核心符号表在core_layout中的起始偏移
info->symoffs = ALIGN(mod->core_layout.size, symsect->sh_addralign ?: 1);
//這裡主要是為core_layout增加了核心符号表的大小,結束位址同時記錄到了info->stroffs,作為核心符号表字元串表的起始偏移
info->stroffs = mod->core_layout.size = info->symoffs + ndst * sizeof(Elf_Sym);
//在core_layout中為核心符号表字元串表預留白間
mod->core_layout.size += strtab_size;
//為核心符号表預留一個類型表,這是一個char數組,符号表的每個表項都對應一個char,其以字元的形式
//顯示此符号的類型,如'U'代表UND,'A'代表ABS,見 add_kallsyms => elf_type
info->core_typeoffs = mod->core_layout.size;
mod->core_layout.size += ndst * sizeof(char);
//core_layout最後再做一個對齊,這是其最終大小了
mod->core_layout.size = debug_align(mod->core_layout.size);
//在init_layout的後面加入靜态連結符号表的字元串表(前面已經把整個靜态連結符号表放到init_layout中了)
//這裡字元串表也是放到init_layout中
strsect->sh_flags |= SHF_ALLOC;
strsect->sh_entsize = get_offset(mod, &mod->init_layout.size, strsect,
info->index.str) | INIT_OFFSET_MASK;
pr_debug("\t%s\n", info->secstrings + strsect->sh_name);
/*
init_layout後面插入了一個mod_kallsyms結構體,這個結構體在後面add_kallsyms函數向核心插入子產品符号時候會用到
mod_kallsyms_init_off 儲存這個結構體在init_layout的偏移,core也有,隻不過core_kallsyms是内嵌的,而init的不是内嵌的
*/
/* We'll tack temporary mod_kallsyms on the end. */
/*
init_layout最後要插入一個 mod_kallsyms 結構體,此結構體記錄init_layout中符号表的資訊,
core_layout實際上也有一個類似的結構體,但此結構體實際上就是 mod->core_kallsyms
而對于init_layout來說,此結構體最後要被釋放掉,是以隻在 mod->kallsyms 中記錄了一個指針
*/
mod->init_layout.size = ALIGN(mod->init_layout.size, __alignof__(struct mod_kallsyms));
info->mod_kallsyms_init_off = mod->init_layout.size;
mod->init_layout.size += sizeof(struct mod_kallsyms);
/*
後面同樣為每個符号加入了一個char的類型資訊,但注意這裡加入的是nsrc,也就是整個符号表的大小
而前面core_layout中隻是加入core symbol的大小
*/
info->init_typeoffs = mod->init_layout.size;
mod->init_layout.size += nsrc * sizeof(char);
mod->init_layout.size = debug_align(mod->init_layout.size);
}
8.6: 配置設定子產品記憶體布局,複制子產品二進制代碼到真正運作時記憶體,修複所有子產品記憶體ELF對應節區頭部表指向記憶體布局
err = move_module(info->mod, info);
if (err)
return ERR_PTR(err);
此函數負責為core_layout和init_layout配置設定RWX記憶體(這裡是vmalloc,arm64中的屬性為RWX),若配置設定成功則: * 先将兩個layout段真正的記憶體首位址記錄到module_layout->base字段中 * 然後再次掃描ko(目标檔案)中所有節區,将所有标記SHF_ALLOC屬性的節區複制到core/init_layout中.而節區複制到哪個layout,以及此節區在layout中的内部偏移是由節區的sh_entsize字段決定的: - 高位是否标記了INIT_OFFSET_MASK決定其要配置設定到init_layout還是core_layout,此flag是在layout_sections函數中設定的,此函數中根據節區名(是否為".init"開頭)判斷其應屬于哪個layout. - 低位記錄了目前節區在對應的layout中的偏移, module_layout結構體中隻記錄了每個layout中4中不同屬性的段的起始結束位址,而每一個段中每個節區應該配置設定到哪裡并沒有記錄,在layout_sections布局時已經将計算好的各個節區的layout内部偏移記錄到節區頭部表的sh_entsize字段了. 此函數最後會修正子產品記憶體二進制中每個節區的節區頭部表中的sh_addr,之前其指向節區的子產品記憶體二進制中的位置之後指向子產品記憶體最終布局中的位置. 注: 子產品記憶體二進制位置指的是: 子產品ELF檔案被加載到記憶體後,在記憶體ELF檔案中此節區的位址 子產品布局位置指的是: 核心為子產品配置設定運作時代碼記憶體,并複制ELF檔案中的節區後,此段代碼在運作時真正的記憶體位址
static int move_module(struct module *mod, struct load_info *info)
{
int i;
void *ptr;
/* Do the allocs. */
/*
此函數通過vmalloc在虛拟位址範圍[module_alloc_base, module_alloc_end]内為子產品配置設定大小為size的RWX記憶體,
在開啟子產品plt的情況下(CONFIG_ARM64_MODULE_PLTS)支援長跳,故若此範圍配置設定失敗(沒虛拟位址空間位址了),
則可以嘗試在向後的2G範圍内配置設定.
ps:子產品頁是4KB粒度的
*/
ptr = module_alloc(mod->core_layout.size);
/*
* The pointer to this block is stored in the module structure
* which is inside the block. Just mark it as not being a
* leak.
*/
kmemleak_not_leak(ptr);
if (!ptr)
return -ENOMEM;
//整片記憶體清空
memset(ptr, 0, mod->core_layout.size);
mod->core_layout.base = ptr;
if (mod->init_layout.size) {
/*
若init_layout非空,則也為init_layout配置設定空間
*/
ptr = module_alloc(mod->init_layout.size);
/*
* The pointer to this block is stored in the module structure
* which is inside the block. This block doesn't need to be
* scanned as it contains data and code that will be freed
* after the module is initialized.
*/
kmemleak_ignore(ptr);
if (!ptr) {
module_memfree(mod->core_layout.base);
return -ENOMEM;
}
//init_layout内容全部清空
memset(ptr, 0, mod->init_layout.size);
mod->init_layout.base = ptr;
} else
//若init_layout中沒有段,則base直接置空
mod->init_layout.base = NULL;
/* Transfer each section which specifies SHF_ALLOC */
pr_debug("final section addresses:\n");
/*
周遊目标檔案的所有節區
這裡實際上是按照前面的計算,将ELF檔案中的節區複制到其記憶體的真正位置,
而實際上記憶體隻有兩個大段,一個是core_layout,一個是init_layout
最終修複info中的節區頭指針,讓其指向新配置設定的最終子產品記憶體.
*/
for (i = 0; i < info->hdr->e_shnum; i++) {
void *dest;
Elf_Shdr *shdr = &info->sechdrs[i];
//忽略非載入記憶體節區
if (!(shdr->sh_flags & SHF_ALLOC))
continue;
//若節區标記了INIT_OFFSET_MASK,則根據其sh_entsize指定的偏移位置
//将節區整個内容複制到init_layout對應位置中
if (shdr->sh_entsize & INIT_OFFSET_MASK)
dest = mod->init_layout.base
+ (shdr->sh_entsize & ~INIT_OFFSET_MASK);
else
//否則複制到core_layout對應位置中
dest = mod->core_layout.base + shdr->sh_entsize;
//對于SHT_NOBITS的(不占用檔案空間的),則直接在記憶體中天靈
if (shdr->sh_type != SHT_NOBITS)
memcpy(dest, (void *)shdr->sh_addr, shdr->sh_size);
/* Update sh_addr to point to copy in image. */
//這裡會修正info中的資訊,并讓其指向新配置設定的位置
shdr->sh_addr = (unsigned long)dest;
pr_debug("\t0x%lx %s\n",
(long)shdr->sh_addr, info->secstrings + shdr->sh_name);
}
return 0;
}
8.7: 切換子產品的struct modules結構體指向最終子產品記憶體布局中的位置
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
kmemleak_load_module(mod, info);
return mod;
子產品的struct modules本來是指向子產品記憶體ELF檔案中的__this_module結構體,在8.6構模組化塊記憶體布局後,核心子產品布局中也複制了一份__this_module結構體,這是最終子產品的運作時位址,故這裡将子產品的mod指針指向這裡并傳回到父函數.
後續分析參考<再談核心子產品加載(三)—子產品加載流程(下)>