内核模块学习记录02:内核对模块的重定位
模块的重定位
本系列主要保留本人在学习内核模块(2.6.24)部分的一些文字材料,涉及的内容主要包括:
- ELF格式的解析
- 内核对模块的重定位
本文为该系列的第二篇:内核对模块的重定位
如有谬误请批评指正
首先提出如下几个问题:
- 在编译模块时是如何表示对内核函数的引用的呢?
- 内核是如何确定内核函数和地址之间的对应关系的呢?
- 内核是如何确定模块函数和地址之间的对应关系的呢?
- 通过对模块EFL的解析知道,编译模块时使用符号表和重定位项来描述模块对内核或其它模块的引用。
- 在编译内核时,会生成描述内核符号和地址对应关系的符号表,并链接到内核镜像中。因此在加载模块时内核能使用内核符号表完成对模块的重定位。
- 同上,模块可以选择主动导出符号供其他模块引用。在加载该模块时对应的符号表也被加载到内核空间中,因此实现了模块间的相互引用。
对模块的重定位,可分为如下两个阶段
- 完成对模块符号表的重定位
- 完成对代码或数据引用的重定位
模块符号表的重定位
simplify_symbols()负责完成对当前模块符号表的重定位操作,具体来说就是对当前模块符号表中的每一个未解析符号项X,在内核符号表或内核其它模块符号表中搜索与X名称相同的符号表项,确定X符号对应的内核地址。
解析模块符号表的函数调用链如下所示:
simplify_symbols()
/* Change all symbols so that st_value encodes the pointer directly. */
/*
*@sechdrs:节头表
*@symindex:符号表所在节的索引
*@strtab:字符串节的虚拟地址
*@versindex:“version”节的索引
*@pcpuindex:默认为0?
*@mod内建的module
*/
static int simplify_symbols(Elf_Shdr *sechdrs,
unsigned int symindex,
const char *strtab,
unsigned int versindex,
unsigned int pcpuindex,
struct module *mod)
{
//找到符号表的虚拟地址
Elf_Sym *sym = (void *)sechdrs[symindex].sh_addr;
unsigned long secbase;
//计算符号表中符号项的个数
unsigned int i, n = sechdrs[symindex].sh_size / sizeof(Elf_Sym);
int ret = 0;
//忽略第一个符号表项
for (i = 1; i < n; i++) {
//对每个符号项进行解析,根据st_shndx判断是否需要对符号进行重定位
//st_shndx保存了一个节在节头表中的索引
switch (sym[i].st_shndx) {
case SHN_COMMON:
/* We compiled with -fno-common. These are not
supposed to happen. */
DEBUGP("Common symbol: %s\n", strtab + sym[i].st_name);
printk("%s: please compile with -fno-common\n",
mod->name);
ret = -ENOEXEC;
break;
//绝对符号,不需要进行重定位
case SHN_ABS:
/* Don't need to do anything */
DEBUGP("Absolute symbol: 0x%08lx\n",
(long)sym[i].st_value);
break;
//未定义符号,需要通过外部符号表来解决
case SHN_UNDEF:
//resolve_symbol根据符号名称(strtab+sym[i].st_name)
//查找该符号最终在内核中的虚拟地址
sym[i].st_value
= resolve_symbol(sechdrs, versindex,
strtab + sym[i].st_name, mod);
/* Ok if resolved. */
if (sym[i].st_value != 0)
break;
//没有检索到符号,则根据是否置STB_WAEAK报错或跳过
/* Ok if weak. */
if (ELF_ST_BIND(sym[i].st_info) == STB_WEAK)
break;
printk(KERN_WARNING "%s: Unknown symbol %s\n",
mod->name, strtab + sym[i].st_name);
ret = -ENOENT;
break;
default:
/* Divert to percpu allocation if a percpu var. */
if (sym[i].st_shndx == pcpuindex)
secbase = (unsigned long)mod->percpu;
else
secbase = sechdrs[sym[i].st_shndx].sh_addr;
sym[i].st_value += secbase;
break;
}
}
return ret;
}
resolve_symbos() 完成具体的“查找"工作,完成在内核符号表和内核模块符号表中的查找匹配。
resolve_symbols()
/* Resolve a symbol for this module. I.e. if we find one, record usage.
Must be holding module_mutex. */
/*
*@sechdrs:节头表
*@versindex:“version”节的索引
*@name:具体的符号名称,指向的是字符串表中的相关位置
*mod:内建的moudle
*/
static unsigned long resolve_symbol(Elf_Shdr *sechdrs,
unsigned int versindex,
const char *name,
struct module *mod)
{
struct module *owner;//标识符号属于内核(null)还是模块(module*)
unsigned long ret;//符号在内核中的虚拟地址
const unsigned long *crc;
//查找name对应的符号表的位置
//这里ret返回的是符号在内核中的虚拟地址
ret = __find_symbol(name, &owner, &crc,
!(mod->taints & TAINT_PROPRIETARY_MODULE));
if (ret) {
/* use_module can fail due to OOM,
or module initialization or unloading */
if (!check_version(sechdrs, versindex, name, mod, crc) ||
!use_module(mod, owner))
ret = 0;
}
return ret;
}
__find_symbol负责在 内核符号表和模块符号表中查找指定名称的符号,查找顺序ksymtab>ksymtab_gpl>ksymtab_gpl_future>ksymtab_unused>ksymtab_unused_gpl>其它模块。
/* Find a symbol, return value, crc and module which owns it */
//针对moudle中类型为UND(为定义)的符号,在内核自生的符号表中或已载入的moudle(s)中查找
static unsigned long __find_symbol(const char *name,
struct module **owner,
const unsigned long **crc,
int gplok)
{
struct module *mod;
const struct kernel_symbol *ks;
/* Core kernel first. */
//owner标识该符号所属的对象 null:内核 non_null:匹配该符号的module
*owner = NULL;
//首先在内核的ksymtab中查找该符号
//假设当前在ksymtab符号表中能找到该符号,基于该假设来考察一下lookup_symbol()的上下文变化
/*
*const struct kernel_symbol *ks = start;
*
*
*/
ks = lookup_symbol(name, __start___ksymtab, __stop___ksymtab);
if (ks) {
/*
#ifndef CONFIG_MODVERSIONS
#define symversion(base, idx) NULL
#else
#define symversion(base, idx) ((base != NULL) ? ((base) + (idx)) : NULL)
#endif
*/
*crc = symversion(__start___kcrctab, (ks - __start___ksymtab));
//ks->value即为该符号在内核中的最终虚拟地址值
return ks->value;
}
//根据flag,在内核的ksymtab_gpl符号表中查找
if (gplok) {
ks = lookup_symbol(name, __start___ksymtab_gpl,
__stop___ksymtab_gpl);
if (ks) {
*crc = symversion(__start___kcrctab_gpl,
(ks - __start___ksymtab_gpl));
return ks->value;
}
}
//然后在内核的ksymtal_gpl_future符号表中查找
ks = lookup_symbol(name, __start___ksymtab_gpl_future,
__stop___ksymtab_gpl_future);
if (ks) {
if (!gplok) {
printk(KERN_WARNING "Symbol %s is being used "
"by a non-GPL module, which will not "
"be allowed in the future\n", name);
printk(KERN_WARNING "Please see the file "
"Documentation/feature-removal-schedule.txt "
"in the kernel source tree for more "
"details.\n");
}
*crc = symversion(__start___kcrctab_gpl_future,
(ks - __start___ksymtab_gpl_future));
return ks->value;
}
//接下来在ksymtab_unused符号表中查找
ks = lookup_symbol(name, __start___ksymtab_unused,
__stop___ksymtab_unused);
if (ks) {
printk_unused_warning(name);
*crc = symversion(__start___kcrctab_unused,
(ks - __start___ksymtab_unused));
return ks->value;
}
//接下来在ksymtab_unused_gpl符号表中查找
if (gplok)
ks = lookup_symbol(name, __start___ksymtab_unused_gpl,
__stop___ksymtab_unused_gpl);
if (ks) {
printk_unused_warning(name);
*crc = symversion(__start___kcrctab_unused_gpl,
(ks - __start___ksymtab_unused_gpl));
return ks->value;
}
/* Now try modules. */
//接下来在其他module的符号表中查找,查找的顺序为syms>gpl_syms>unused_syms>unused_gpl_syms
//>gpl_future_syms
list_for_each_entry(mod, &modules, list) {
//owner标识匹配到该符号的的moudle
*owner = mod;
ks = lookup_symbol(name, mod->syms, mod->syms + mod->num_syms);
if (ks) {
*crc = symversion(mod->crcs, (ks - mod->syms));
return ks->value;
}
if (gplok) {
ks = lookup_symbol(name, mod->gpl_syms,
mod->gpl_syms + mod->num_gpl_syms);
if (ks) {
*crc = symversion(mod->gpl_crcs,
(ks - mod->gpl_syms));
return ks->value;
}
}
ks = lookup_symbol(name, mod->unused_syms, mod->unused_syms + mod->num_unused_syms);
if (ks) {
printk_unused_warning(name);
*crc = symversion(mod->unused_crcs, (ks - mod->unused_syms));
return ks->value;
}
if (gplok) {
ks = lookup_symbol(name, mod->unused_gpl_syms,
mod->unused_gpl_syms + mod->num_unused_gpl_syms);
if (ks) {
printk_unused_warning(name);
*crc = symversion(mod->unused_gpl_crcs,
(ks - mod->unused_gpl_syms));
return ks->value;
}
}
ks = lookup_symbol(name, mod->gpl_future_syms,
(mod->gpl_future_syms +
mod->num_gpl_future_syms));
if (ks) {
if (!gplok) {
printk(KERN_WARNING "Symbol %s is being used "
"by a non-GPL module, which will not "
"be allowed in the future\n", name);
printk(KERN_WARNING "Please see the file "
"Documentation/feature-removal-schedule.txt "
"in the kernel source tree for more "
"details.\n");
}
*crc = symversion(mod->gpl_future_crcs,
(ks - mod->gpl_future_syms));
return ks->value;
}
}
DEBUGP("Failed to find symbol %s\n", name);
return 0;
}
上述内核符号表对应的开始地址以及结束地址由链接器确定,在module.c中声明。
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 struct kernel_symbol __start___ksymtab_unused[];
extern const struct kernel_symbol __stop___ksymtab_unused[];
extern const struct kernel_symbol __start___ksymtab_unused_gpl[];
extern const struct kernel_symbol __stop___ksymtab_unused_gpl[];
extern const struct kernel_symbol __start___ksymtab_gpl_future[];
extern const struct kernel_symbol __stop___ksymtab_gpl_future[];
extern const unsigned long __start___kcrctab[];
extern const unsigned long __start___kcrctab_gpl[];
extern const unsigned long __start___kcrctab_gpl_future[];
extern const unsigned long __start___kcrctab_unused[];
extern const unsigned long __start___kcrctab_unused_gpl[];
模块的重定位
在完成对模块符号表的重定向操作之后,接下来就是根据模块符号表和可重定位项完成对模块代码或数据的重定位操作。
函数调用链如图2所示
可重定位节中的每个重定位项,描述了符号名称与“要重定位的项所在的地址”之间的关系:<符号名称,待重定位项所在地址>,模块符号表描述了<符号名称,内核空间地址>的关系。因此根据符模块符号表中的符号项,按照转换规则将符号项中的“内核空间地址”填入由“待重定位项所在地址”指定的位置,完成重定位操作。具体的重定向地址转换规则与体系接头相关。
//真正的重定位操作
//Q:模块中对内核函数的引用,是如何被解析的呢?
//Q:具体来说,是如何找到内核的符号表的呢?
/* Now do relocations. */
//遍历节头表
for (i = 1; i < hdr->e_shnum; i++) {
//重定位每一个节之前,首先找到字符串表
const char *strtab = (char *)sechdrs[strindex].sh_addr;
unsigned int info = sechdrs[i].sh_info;
/* Not a valid relocation section? */
if (info >= hdr->e_shnum)
continue;
/* Don't bother with non-allocated sections */
//对不需要加载到内存中的节,放过
if (!(sechdrs[info].sh_flags & SHF_ALLOC))
continue;
//遍历到重定位对应的节,SHT_REL:普通重定位 SHT_RELA:需要添加常数的重定位项
//如果当前节类型为SHT_REL或SHT_RELA则说明当前节需要被重定位
//Q:这里的symindex指向的是当前elf文件节头吗?
//Q:如果是这样,那么对内核的引用是如何重定位的呢?不该指向内核的符号表吗?-20201015
//A:在前面的simplify_symbols(sechdrs, symindex, strtab, versindex, pcpuindex,mod)
//中完成对当前elf中符号表的全部解析过程,对符号表中未解析的符号关联到匹配的内核符号或模块符号在内核
//中的最终虚拟地址,因此接下来只需要对当前elf中的待重定位项,用当前elf中的符号表完成重定向即可
//-20201015
if (sechdrs[i].sh_type == SHT_REL)
err = apply_relocate(sechdrs, strtab, symindex, i,mod);
else if (sechdrs[i].sh_type == SHT_RELA)
err = apply_relocate_add(sechdrs, strtab, symindex, i,
mod);
if (err < 0)
goto cleanup;
}
apply_relocate()/apply_relocate_add()负责重定位项,完成重定位操作。
以arm体系结构为例,对应的apply_relocate()函数如下所示。路径arch/arm/kernel/module.c
/**
* *
* @sechdrs:节头表首地址
* @strtab:符号表首地址
* @symindex:符号表对应的节在节头表项中的索引
* @relindex:当前遍历到的节头表中的项的索引
* @module:内建的module
*
**/
int
apply_relocate(Elf32_Shdr *sechdrs, const char *strtab, unsigned int symindex,
unsigned int relindex, struct module *module)
{
Elf32_Shdr *symsec = sechdrs + symindex;//符号表对应的节在节头表中的索引
Elf32_Shdr *relsec = sechdrs + relindex;//指向当前遍历到的【可重定位】节在节头表中的索引
Elf32_Shdr *dstsec = sechdrs + relsec->sh_info;//待重定位的节在节头表中的位置?20201015
Elf32_Rel *rel = (void *)relsec->sh_addr;//当前遍历到的【可重定位】节在虚拟地址空间中的位置
unsigned int i;
//对当前的【可重定位】节(类型:SHT_REL),遍历其中的重定位项
for (i = 0; i < relsec->sh_size / sizeof(Elf32_Rel); i++, rel++) {
unsigned long loc;
Elf32_Sym *sym;
s32 offset;
//重定位项中f_info字段的高24位标识了对应的符号表中的索引
offset = ELF32_R_SYM(rel->r_info);
if (offset < 0 || offset > (symsec->sh_size / sizeof(Elf32_Sym))) {
printk(KERN_ERR "%s: bad relocation, section %d reloc %d\n",
module->name, relindex, i);
return -ENOEXEC;
}
//对当前的节(类型:SHT_REL),对其中的每一个重定位项,
//找到其在符号表中对应的Elf32_Sym结构
//最终根据符号表中来填充重定位项中r_offset的位置完成重定向
sym = ((Elf32_Sym *)symsec->sh_addr) + offset;
if (rel->r_offset < 0 || rel->r_offset > dstsec->sh_size - sizeof(u32)) {
printk(KERN_ERR "%s: out of bounds relocation, "
"section %d reloc %d offset %d size %d\n",
module->name, relindex, i, rel->r_offset,
dstsec->sh_size);
return -ENOEXEC;
}
//得到需要重定位的项的虚拟地址,rel->r_offset标识了哪里需要被重定位(相对于目的节的偏移)
loc = dstsec->sh_addr + rel->r_offset;
//根据重定位项的具体信息来进行针对性的重定向处理
//重定位项中的rel->r_info字段的低8位描述了重定位类型
switch (ELF32_R_TYPE(rel->r_info)) {
//绝对重定位
case R_ARM_ABS32:
*(u32 *)loc += sym->st_value;
break;
//相对重定位以及call和jump
case R_ARM_PC24:
case R_ARM_CALL:
case R_ARM_JUMP24:
offset = (*(u32 *)loc & 0x00ffffff) << 2;
if (offset & 0x02000000)
offset -= 0x04000000;
offset += sym->st_value - loc;
if (offset & 3 ||
offset <= (s32)0xfe000000 ||
offset >= (s32)0x02000000) {
printk(KERN_ERR
"%s: relocation out of range, section "
"%d reloc %d sym '%s'\n", module->name,
relindex, i, strtab + sym->st_name);
return -ENOEXEC;
}
offset >>= 2;
//完成具体的重定位操作,更改loc地址处的值,完成重定位操作
*(u32 *)loc &= 0xff000000;
*(u32 *)loc |= offset & 0x00ffffff;
break;
default:
printk(KERN_ERR "%s: unknown relocation: %u\n",
module->name, ELF32_R_TYPE(rel->r_info));
return -ENOEXEC;
}
}
return 0;
}
小结
对内核模块进行重定向大致分为两个阶段
- 重定向模块符号表 :按照kernel--->module的顺序查找符号表
- 按模块符号表重定向指令和数据:与具体的体系架构相关联
参考资料
- 深入Linux内核架构:第七章:模块;附录E:ELF二进制格式
- 2.6.24源码