对“未解决的引用”符号(unresolved symbol)的处理
前文中已多次提到内核模块ELF文件中的“未解决的引用”符号,所谓的“未解决的引用”符号,就是模块的编译工具链在对模块进行链接生成最终的,ko文件时,对于模块中调用的一些函数,最简单的比如printk函数,链接工具无法在该模块的所有目标文件中找到这个函数的具体指令码(因为这个函数是在Linux的内核源代码中实现的,其指令码存在于编译内核生成的目标文件中,模块的链接工具显然不会也不应该去查找内核的目标文件),所以就会将这个符号标记为“未解决的引用”,对它的处理将一直延续到内核模块被加载时(处理的核心是在内核或者是其他内核模块导出的符号中找到这个“未解决的引用”符号,继而找到该符号所在的内存地址,从而最终形成正确的函数调用)。
Linux内核中,一个名为simplify_symbols的函数用来实现这一功能,
/*模块进行符号解析 *经过simplify_symbols函数的调用之后,内核模块符号表中的所有符号就都有了正确的st_value值, *也即都有了正确的内存地址。 */ /* Change all symbols so that st_value encodes the pointer directly. */ static int simplify_symbols(struct module *mod, const struct load_info *info) { Elf_Shdr *symsec = &info->sechdrs[info->index.sym]; Elf_Sym *sym = (void *)symsec->sh_addr; unsigned long secbase; unsigned int i; int ret = 0; const struct kernel_symbol *ksym; /* *通过一个循环遍历符号表中的所有符号,对于每一个符号都会根据 *该符号的st_shndx值分情况进行处理 */ for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) { const char *name = info->strtab + sym[i].st_name; switch (sym[i].st_shndx) { case SHN_COMMON: /* Ignore common symbols */ if (!strncmp(name, "__gnu_lto", 9)) break; /* We compiled with -fno-common. These are not supposed to happen. */ pr_debug("Common symbol: %s\n", name); pr_warn("%s: please compile with -fno-common\n", mod->name); ret = -ENOEXEC; break; case SHN_ABS: /* Don't need to do anything */ pr_debug("Absolute symbol: 0x%08lx\n", (long)sym[i].st_value); break; case SHN_UNDEF: ksym = resolve_symbol_wait(mod, info, name); /* Ok if resolved. */ if (ksym && !IS_ERR(ksym)) { sym[i].st_value = ksym->value; break; } /* Ok if weak. */ if (!ksym && ELF_ST_BIND(sym[i].st_info) == STB_WEAK) break; pr_warn("%s: Unknown symbol %s (err %li)\n", mod->name, name, PTR_ERR(ksym)); ret = PTR_ERR(ksym) ?: -ENOENT; break; default: /* Divert to percpu allocation if a percpu var. */ /*先得到符号所在section的最终内存地址,然后加上它在section中的偏移量, 这样就得到了符号的最终内存地址。*/ if (sym[i].st_shndx == info->index.pcpu) secbase = (unsigned long)mod_percpu(mod); else secbase = info->sechdrs[sym[i].st_shndx].sh_addr; sym[i].st_value += secbase; break; } } return ret; }
简言之,在加载模块的过程中,simplify_symbols函数用来为当前正在加载的模块中所有“未解决的引用”符号产生正确的目标地址。对这段代码的透彻理解需要读者熟悉ELF文件格式规范的相关概念,我们不可能在本书中全面介绍ELF文件格式,但是为了让读者能理解上面的代码,还是从代码的角度出发,将其中所涉及的一些有关ELF文件的概念予以简单介绍。
typedef struct elf64_sym { Elf64_Word st_name; //st_name是符号名在符号名称字符串表中的索引值 /* Symbol name, index in string tbl */ unsigned char st_info; //st_value是符号所在的内存地址。 /* Type and binding attributes */ unsigned char st_other; /* No defined meaning, 0 */ Elf64_Half st_shndx; //st_shndx是该符号所在的section在Section header table中的索引值。 /* Associated section index */ Elf64_Addr st_value; /* Value of the symbol */ Elf64_Xword st_size; /* Associated symbol size */ } Elf64_Sym;
其中,st_name是符号名在符号名称字符串表中的索引值,详见本章前面的“字符串表(string Table)”部分。st_value是符号所在的内存地址。simplify_symbols函数的唯一功能就是在加载模块时重新生成正确的st_value值。st_shndx是该符号所在的section在Section header table中的索引值。但是该值还有一些特殊的定义。对于符号表,它是ELF文件中的一个section,这个section就是由一系列struct Elf_Sym型元素所构成的一个数组,每个元素代码一个符号。
在对符号表的概念有了基本了解之后,回过头来看看simplify_symbols函数的代码实现。函数首先通过一个循环遍历符号表中的所有符号,对于每一个符号都会根据该符号的st_shndx值分情况进行处理。前面刚刚提到st_shndx,通常情况下,该值表示符号所在section的索引值,为方便叙述,我们称这种符号为一般符号。对于一般符号来说,它的st_value在ELF文件中的值是从其所在section起始处算起的一个偏移量,代码中在switch的default分支下进行处理:先得到符号所在section的最终内存地址,然后加上它在section中的偏移量,这样就得到了符号的最终内存地址。
除了一般符号,还有些符号的st_shndx具有特殊的含义,典型的如SHN_ABS和SHN_UNDEF,前者表明该符号具有绝对地址,因此simplify_symbols函数无须对这种情况予以任何处理,后者表明该符号是一“undefined symbol”,其实就是我们一直说的“未解决的引用”符号。这种情况下simplify_symbols函数会调用resolve_symbol函数来处理该未定义符号,后者会调用find_symbol函数去查找该符号(详细的查找过程见本章前面的"find_symbol函数”部分),如果找到了,就把它在内存中的实际地址赋值给st_valueo如此,经过simplify_symbols函数的调用之后,内核模块符号表中的所有符号就都有了正确的st_value值,也即都有了正确的内存地址。
回头看看图1巧“内核模块导出的符号”,每个“__ksymtab”,“
__ksymtab_gpl”和__ksymtab_gpl_future”section都是由struct kemel_symbol类型的元素所构成的数组。到目前为止,如果仔细考察每个元素的话,会发现其中的value成员依然是内核模块在静态编译时产生的地址。换句话说,根本不是这些符号在模块被加载进系统之后在内存中的实际地址。这显然不是我们想要的效果:想想本节前半部分提到的对模块中“未解决的引用”符号的处理,如果在别的模块中找到的符号其内存地址只是当初该模块在静态链接时填入的地址,那么对该符号的引用必然导致错误的内存访问。这是个很严重的问题。而Linux
内核对这一问题的处理便引出了下一部分的内容—重定位。
重定位
重定位主要用来解决静态链接时的符号引用与动态加载时实际符号地址不一致的问题,上节结束部分提到的模块导出的符号地址,就是一个典型的需要重定位的例子。仔细讨论重定位的内容不是件简单的事情,因为重定位的任务包含很多方面的内容,尤其是跟体系架构相关的一些微妙晦涩的技术细节。
如果模块有用EXPORT_SYMBOL导出的符号,那么模块的编译工具链会为这个模块的ELF文件生成一个独立的特殊section:“.rel_ksymtab",它专门用于对"_ksymtab"section的重定位,称为relocation section。这个section是由下面的数据结构元素形成的一个数组。
typedef struct elf32_rel { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
Linux源码中用于内核模块加载时重定位的代码:
static int apply_relocations(struct module *mod, const struct load_info *info) { unsigned int i; int err = 0; /* Now do relocations. */ //遍历HDR视图中Section header table中所有的entry。 //对于一个重定位的section,其entry中的sh_type的值为SHT_REL或者 //SHT_RELA,分别对应两种不同的重定位方式,我们拿第一种类型SHT_REL //来说事。对于sh_type=SHT_REL的section而言,其Section header中的 //sh_info成员指明了被重定位的section在Section header table中的索 //引值,代码中用info变量来表示。 for (i = 1; i < info->hdr->e_shnum; i++) { unsigned int infosec = info->sechdrs[i].sh_info; /* Not a valid relocation section? */ if (infosec >= info->hdr->e_shnum) continue; /* Don't bother with non-allocated sections */ if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC)) continue; if (info->sechdrs[i].sh_type == SHT_REL) //如果发现了一个sh_type=SHT_REL的section, //系统就调用apply_relocate函数来执行重定位 err = apply_relocate(info->sechdrs, info->strtab, info->index.sym, i, mod);//系统就调用apply_relocate函数来执行重定位 else if (info->sechdrs[i].sh_type == SHT_RELA) err = apply_relocate_add(info->sechdrs, info->strtab, info->index.sym, i, mod); if (err < 0) break; } return err; }
代码用一个for循环来遍历HDR视图中Section header table中所有的entry。对于一个重定位的section,其entry中的sh_type的值为SHT_REL或者SHT_RELA,分别对应两种不同的重定位方式,我们拿第一种类型SHT_REL来说事。对于sh_type=SHT_REL的section而言,其Section header中的sh_info成员指明了被重定位的section在Section header table中的索引值,代码中用info变量来表示。
在遍历的过程中,如果发现了一个sh_type=SHT_REL的section,系统就调用apply_relocate函数来执行重定位,后者是个体系结构相关的函数。总体上,该函数对模块导出符号的重定位原理是,根据重定位元素中的r_offset以及relocation section header entry中的sh_info得到需要修改的导出符号struct kernel_symbol中value所在的内存地址:
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; Elf32_Rel *rel = (void *)relsec->sh_addr;//entry[i]对应当前正在处理的relocation section unsigned int i; for (i = 0; i < relsec->sh_size / sizeof(Elf32_Rel); i++, rel++) { unsigned long loc; Elf32_Sym *sym; const char *symname; s32 offset; u32 tmp; #ifdef CONFIG_THUMB2_KERNEL u32 upper, lower, sign, j1, j2; #endif offset = ELF32_R_SYM(rel->r_info); if (offset < 0 || offset > (symsec->sh_size / sizeof(Elf32_Sym))) { pr_err("%s: section %u reloc %u: bad relocation sym offset\n", module->name, relindex, i); return -ENOEXEC; } sym = ((Elf32_Sym *)symsec->sh_addr) + offset; symname = strtab + sym->st_name; if (rel->r_offset < 0 || rel->r_offset > dstsec->sh_size - sizeof(u32)) { pr_err("%s: section %u reloc %u sym '%s': out of bounds relocation, offset %d size %u\n", module->name, relindex, i, symname, rel->r_offset, dstsec->sh_size); return -ENOEXEC; } loc = dstsec->sh_addr + rel->r_offset; switch (ELF32_R_TYPE(rel->r_info)) { case R_ARM_NONE: /* ignore */ break; case R_ARM_ABS32: case R_ARM_TARGET1: *(u32 *)loc += sym->st_value; break; case R_ARM_PC24: case R_ARM_CALL: case R_ARM_JUMP24: if (sym->st_value & 3) { pr_err("%s: section %u reloc %u sym '%s': unsupported interworking call (ARM -> Thumb)\n", module->name, relindex, i, symname); return -ENOEXEC; } offset = __mem_to_opcode_arm(*(u32 *)loc); offset = (offset & 0x00ffffff) << 2; if (offset & 0x02000000) offset -= 0x04000000; offset += sym->st_value - loc; if (offset <= (s32)0xfe000000 || offset >= (s32)0x02000000) { pr_err("%s: section %u reloc %u sym '%s': relocation %u out of range (%#lx -> %#x)\n", module->name, relindex, i, symname, ELF32_R_TYPE(rel->r_info), loc, sym->st_value); return -ENOEXEC; } offset >>= 2; offset &= 0x00ffffff; *(u32 *)loc &= __opcode_to_mem_arm(0xff000000); *(u32 *)loc |= __opcode_to_mem_arm(offset); break; case R_ARM_V4BX: /* Preserve Rm and the condition code. Alter * other bits to re-code instruction as * MOV PC,Rm. */ *(u32 *)loc &= __opcode_to_mem_arm(0xf000000f); *(u32 *)loc |= __opcode_to_mem_arm(0x01a0f000); break; case R_ARM_PREL31: offset = *(u32 *)loc + sym->st_value - loc; *(u32 *)loc = offset & 0x7fffffff; break; case R_ARM_MOVW_ABS_NC: case R_ARM_MOVT_ABS: offset = tmp = __mem_to_opcode_arm(*(u32 *)loc); offset = ((offset & 0xf0000) >> 4) | (offset & 0xfff); offset = (offset ^ 0x8000) - 0x8000; offset += sym->st_value; if (ELF32_R_TYPE(rel->r_info) == R_ARM_MOVT_ABS) offset >>= 16; tmp &= 0xfff0f000; tmp |= ((offset & 0xf000) << 4) | (offset & 0x0fff); *(u32 *)loc = __opcode_to_mem_arm(tmp); break; #ifdef CONFIG_THUMB2_KERNEL case R_ARM_THM_CALL: case R_ARM_THM_JUMP24: /* * For function symbols, only Thumb addresses are * allowed (no interworking). * * For non-function symbols, the destination * has no specific ARM/Thumb disposition, so * the branch is resolved under the assumption * that interworking is not required. */ if (ELF32_ST_TYPE(sym->st_info) == STT_FUNC && !(sym->st_value & 1)) { pr_err("%s: section %u reloc %u sym '%s': unsupported interworking call (Thumb -> ARM)\n", module->name, relindex, i, symname); return -ENOEXEC; } upper = __mem_to_opcode_thumb16(*(u16 *)loc); lower = __mem_to_opcode_thumb16(*(u16 *)(loc + 2)); /* * 25 bit signed address range (Thumb-2 BL and B.W * instructions): * S:I1:I2:imm10:imm11:0 * where: * S = upper[10] = offset[24] * I1 = ~(J1 ^ S) = offset[23] * I2 = ~(J2 ^ S) = offset[22] * imm10 = upper[9:0] = offset[21:12] * imm11 = lower[10:0] = offset[11:1] * J1 = lower[13] * J2 = lower[11] */ sign = (upper >> 10) & 1; j1 = (lower >> 13) & 1; j2 = (lower >> 11) & 1; offset = (sign << 24) | ((~(j1 ^ sign) & 1) << 23) | ((~(j2 ^ sign) & 1) << 22) | ((upper & 0x03ff) << 12) | ((lower & 0x07ff) << 1); if (offset & 0x01000000) offset -= 0x02000000; offset += sym->st_value - loc; if (offset <= (s32)0xff000000 || offset >= (s32)0x01000000) { pr_err("%s: section %u reloc %u sym '%s': relocation %u out of range (%#lx -> %#x)\n", module->name, relindex, i, symname, ELF32_R_TYPE(rel->r_info), loc, sym->st_value); return -ENOEXEC; } sign = (offset >> 24) & 1; j1 = sign ^ (~(offset >> 23) & 1); j2 = sign ^ (~(offset >> 22) & 1); upper = (u16)((upper & 0xf800) | (sign << 10) | ((offset >> 12) & 0x03ff)); lower = (u16)((lower & 0xd000) | (j1 << 13) | (j2 << 11) | ((offset >> 1) & 0x07ff)); *(u16 *)loc = __opcode_to_mem_thumb16(upper); *(u16 *)(loc + 2) = __opcode_to_mem_thumb16(lower); break; case R_ARM_THM_MOVW_ABS_NC: case R_ARM_THM_MOVT_ABS: upper = __mem_to_opcode_thumb16(*(u16 *)loc); lower = __mem_to_opcode_thumb16(*(u16 *)(loc + 2)); /* * MOVT/MOVW instructions encoding in Thumb-2: * * i = upper[10] * imm4 = upper[3:0] * imm3 = lower[14:12] * imm8 = lower[7:0] * * imm16 = imm4:i:imm3:imm8 */ offset = ((upper & 0x000f) << 12) | ((upper & 0x0400) << 1) | ((lower & 0x7000) >> 4) | (lower & 0x00ff); offset = (offset ^ 0x8000) - 0x8000; offset += sym->st_value; if (ELF32_R_TYPE(rel->r_info) == R_ARM_THM_MOVT_ABS) offset >>= 16; upper = (u16)((upper & 0xfbf0) | ((offset & 0xf000) >> 12) | ((offset & 0x0800) >> 1)); lower = (u16)((lower & 0x8f00) | ((offset & 0x0700) << 4) | (offset & 0x00ff)); *(u16 *)loc = __opcode_to_mem_thumb16(upper); *(u16 *)(loc + 2) = __opcode_to_mem_thumb16(lower); break; #endif default: pr_err("%s: unknown relocation: %u\n", module->name, ELF32_R_TYPE(rel->r_info)); return -ENOEXEC; } } return 0; }
然后根据重定位元素中的r_info获得需要定位的符号在符号表中的偏移量:
loc = dstsec->sh_addr + rel->r_offset;
因为符号表section的基地址很容易获得,于是就可以获得需要重定位的符号在符号表中对应的Elf32_Sym型元素:
sym = ((Elf32_Sym *)symsec->sh_addr) + offset;
所以,最终导出符号的地址被修改。
这一过程简单地说,就是根据导出符号所在section的relocation section,结合导出符号表section,修改导出符号的地址为在内存中最终的地址值。如此,内核模块导出符号的地址在系统执行完重定位之后被更新为正确的值。