版权声明:本文为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指针指向这里并返回到父函数.
后续分析参考<再谈内核模块加载(三)—模块加载流程(下)>