linux内核模块分两种形态,一是静态编译进内核的模块,二是用insmod命令动态加载的模块,也就是后缀名为KO的文件。这里主要讨论linux内核动态加载模块的过程,也就是KO文件被动态加载进内核,并运行的过程。
后缀为KO的文件其实是一种ELF格式文件,很类似于ELF目标文件(.o文件),但是又与ELF目标文件有一点小区别。使用readelf工具可以看到,KO文件里有一个叫.gnu.linkonce.this_module的段,而普通目标文件是没有这个段的。这个段的内容其实是一个struct module结构体(段的地址就等于module结构体的首地址),记录了KO模块的一些信息,这个结构体在linux kernel源代码里也有定义(include/linux/module.h),因为内核在加载模块时要用到这个结构体。
当linux顺利启动,进入shell的时候,就可以输入insmod命令,加载我们自己的内核模块拉。insmod的实现在busybox中,该实现比较简单,把ko文件映射到用户空间,然后进行 sys_init_module 的系统调用,所以ko模块加载主要的实现就是在sys_init_module系统调用中。
下面具体看一下这个系统调用时如何实现的。
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
copy_module_from_user其实就是把映射到用户空间的ko文件,拷贝到内核空间,放在info结构体的hdr 字段中,核心部分的实现在load_module中,这个函数很长,我们只看我们关心的部分:
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
long err;
。。。。。。。。。。。。。。。。。
/* Figure out module layout, and allocate all the memory. */
-----------------------------------(1)
mod = layout_and_allocate(info, flags);
if (IS_ERR(mod)) {
err = PTR_ERR(mod);
goto free_copy;
}
/* Reserve our place in the list. */
-------------------------------------(2)
err = add_unformed_module(mod);
if (err)
goto free_module;
。。。。。。。。。。。。。。。。。。。。。。。。。。。
/* Now we've got everything in the final locations, we can
* find optional sections. */
---------------------------------------(3)
find_module_sections(mod, info);
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
/* Fix up syms, so that st_value is a pointer to location. */
---------------------------------------(4)
err = simplify_symbols(mod, info);
if (err < 0)
goto free_modinfo;
---------------------------------------(5)
err = apply_relocations(mod, info);
if (err < 0)
goto free_modinfo;
。。。。。。。。。。。。。。
-----------------------------------------(6)
return do_init_module(mod);
。。。。。。。。。。。。。。。。。。。。
}
详解load_module之前,先看一下两个变量:
struct load_info info = { NULL, };
struct module *mod;
其中info是一个struct load_info结构体,这个结构体主要保存了ELF文件的一些基本信息:
struct load_info {
Elf_Ehdr *hdr;
unsigned long len;
Elf_Shdr *sechdrs;
char *secstrings, *strtab;
unsigned long symoffs, stroffs;
struct _ddebug *debug;
unsigned int num_debug;
struct {
unsigned int sym, str, mod, vers, info, pcpu;
} index;
};
hdr是ELF文件头的指针,len是文件长度,sechdrs是段表指针,secstrings 和 strtab 分别是段表字符串表和字符串表的首地址。index结构体里保存的是一些段在段表里的索引号,看到有个mod段了吧,这个mod其实就是上面提到过的.gnu.linkonce.this_module段在段表中的下标。
struct module这个结构体的内容和.gnu.linkonce.this_module段的内容是一一对应的,定义有点复杂,用到的时候再看。
(1)先来看layout_and_allocate函数:
这个函数主要任务是决定ko文件中哪些段需要为其分配地址空间,并为ko文件中的每个需要加载的段计算并分配虚拟地址,也就是运行时地址。前面说过,ko文件是类似于.o目标文件的,所以它的每个段的虚拟地址就像目标文件一样,都为0,因此需要链接后才能运行。分配每个段的运行时地址就是链接的第一步。
static struct module *layout_and_allocate(struct load_info *info, int flags)
{
/* Module within temporary copy. */
struct module *mod;
Elf_Shdr *pcpusec;
int err;
mod = setup_load_info(info, flags);
if (IS_ERR(mod))
return mod;
err = check_modinfo(mod, info, flags);
if (err)
return ERR_PTR(err);
/* Allow arches to frob section contents and sizes. */
err = module_frob_arch_sections(info->hdr, info->sechdrs,
info->secstrings, mod);
if (err < 0)
goto out;
pcpusec = &info->sechdrs[info->index.pcpu];
if (pcpusec->sh_size) {
/* We have a special allocation for this section. */
err = percpu_modalloc(mod,
pcpusec->sh_size, pcpusec->sh_addralign);
if (err)
goto out;
pcpusec->sh_flags &= ~(unsigned long)SHF_ALLOC;
}
/* Determine total sizes, and put offsets in sh_entsize. For now
this is done generically; there doesn't appear to be any
special cases for the architectures. */
layout_sections(mod, info);
layout_symtab(mod, info);
/* Allocate and move to the final place */
err = move_module(mod, info);
if (err)
goto free_percpu;
/* Module has been copied to its final place now: return it. */
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
kmemleak_load_module(mod, info);
return mod;
free_percpu:
percpu_modfree(mod);
out:
return ERR_PTR(err);
}
layout_and_allocate()函数先调用setup_load_info()对info进一步初始化,接着调用layout_sections()分配各个段在最终虚拟地址上的偏移,.init段会被单独分配偏移,因为.init段的虚拟地址是单独分配的,后面将详述。然后调用layout_symtab()分配符号表和字符串表在虚拟地址上的偏移。最后调用move_module()将段移动到最终的虚拟地址上去。
static struct module *setup_load_info(struct load_info *info, int flags)
{
unsigned int i;
int err;
struct module *mod;
/* Set up the convenience variables */
info->sechdrs = (void *)info->hdr + info->hdr->e_shoff;
info->secstrings = (void *)info->hdr
+ info->sechdrs[info->hdr->e_shstrndx].sh_offset;
err = rewrite_section_headers(info, flags);
if (err)
return ERR_PTR(err);
/* Find internal symbols and strings. */
for (i = 1; i < info->hdr->e_shnum; i++) {
if (info->sechdrs[i].sh_type == SHT_SYMTAB) {
info->index.sym = i;
info->index.str = info->sechdrs[i].sh_link;
info->strtab = (char *)info->hdr
+ info->sechdrs[info->index.str].sh_offset;
break;
}
}
info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
if (!info->index.mod) {
printk(KERN_WARNING "No module found in object\n");
return ERR_PTR(-ENOEXEC);
}
/* This is temporary: point mod into copy of data. */
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
if (info->index.sym == 0) {
printk(KERN_WARNING "%s: module has no symbols (stripped?)\n",
mod->name);
return ERR_PTR(-ENOEXEC);
}
info->index.pcpu = find_pcpusec(info);
/* Check module struct version now, before we try to use module. */
if (!check_modstruct_version(info->sechdrs, info->index.vers, mod))
return ERR_PTR(-ENOEXEC);
return mod;
}
rewrite_section_headers()这个函数将每个段的虚拟地址暂时设置为其在临时空间中的地址。并将info段和vers段的SHF_ALLOC符号清零,表示不为这两个段分配空间。
static int rewrite_section_headers(struct load_info *info, int flags)
{
unsigned int i;
/* This should always be true, but let's be sure. */
info->sechdrs[0].sh_addr = 0;
for (i = 1; i < info->hdr->e_shnum; i++) {
Elf_Shdr *shdr = &info->sechdrs[i];
if (shdr->sh_type != SHT_NOBITS
&& info->len < shdr->sh_offset + shdr->sh_size) {
printk(KERN_ERR "Module len %lu truncated\n",
info->len);
return -ENOEXEC;
}
/* Mark all sections sh_addr with their address in the
temporary image. */
shdr->sh_addr = (size_t)info->hdr + shdr->sh_offset;
#ifndef CONFIG_MODULE_UNLOAD
/* Don't load .exit sections */
if (strstarts(info->secstrings+shdr->sh_name, ".exit"))
shdr->sh_flags &= ~(unsigned long)SHF_ALLOC;
#endif
}
/* Track but don't keep modinfo and version sections. */
if (flags & MODULE_INIT_IGNORE_MODVERSIONS)
info->index.vers = 0; /* Pretend no __versions section! */
else
info->index.vers = find_sec(info, "__versions");
info->index.info = find_sec(info, ".modinfo");
info->sechdrs[info->index.info].sh_flags &= ~(unsigned long)SHF_ALLOC;
info->sechdrs[info->index.vers].sh_flags &= ~(unsigned long)SHF_ALLOC;
return 0;
}
rewrite_section_headers()返回后,将符号表和字符串表的信息记录在info中。
info->index.mod = find_sec(info, ".gnu.linkonce.this_module");
这里将.gnu.linkonce.this_module段在段表中的下标记录在mod中。
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
现在,mod指针就指向临时空间中的.gnu.linkonce.this_module段的地址了,而.gnu.linkonce.this_module段的内容是编译器生成的并初始化的,因此struct module这个结构体的初始值相当于编译时就设置好了.
现在程序执行完setup_load_info()返回到layout_and_allocate(),接着layout_and_allocate()调用layout_sections()。
static void layout_sections(struct module *mod, struct load_info *info)
{
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 },
{ SHF_ALLOC, SHF_WRITE | ARCH_SHF_SMALL },
{ SHF_WRITE | SHF_ALLOC, ARCH_SHF_SMALL },
{ ARCH_SHF_SMALL | SHF_ALLOC, 0 }
};
unsigned int m, i;
for (i = 0; i < info->hdr->e_shnum; i++)
info->sechdrs[i].sh_entsize = ~0UL;
pr_debug("Core 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->core_size, s, i);
pr_debug("\t%s\n", sname);
}
switch (m) {
case 0: /* executable */
mod->core_size = debug_align(mod->core_size);
mod->core_text_size = mod->core_size;
break;
case 1: /* RO: text and ro-data */
mod->core_size = debug_align(mod->core_size);
mod->core_ro_size = mod->core_size;
break;
case 3: /* whole core */
mod->core_size = debug_align(mod->core_size);
break;
}
}
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_size, s, i)
| INIT_OFFSET_MASK);
pr_debug("\t%s\n", sname);
}
switch (m) {
case 0: /* executable */
mod->init_size = debug_align(mod->init_size);
mod->init_text_size = mod->init_size;
break;
case 1: /* RO: text and ro-data */
mod->init_size = debug_align(mod->init_size);
mod->init_ro_size = mod->init_size;
break;
case 3: /* whole init */
mod->init_size = debug_align(mod->init_size);
break;
}
}
}
layout_sections()利用了struct module 里的两个成员变量:core_size 和 init_size,后面会看到,kernel为ko文件分配最终虚拟地址的时候,实际上分配了两块地址,一块叫core,另一块叫init, 这两个变量分别记录了这两块地址的size。一个内核模块为什么要分配两块地址呢?这是考虑到内核模块的__init函数只运行一次,所以将它单独放在一块内存中可以方便运行结束后,回收这块内存。。__init函数就是用 __init 宏定义的函数, #define __init __section(.init.text) ,编译器会将它放入ko文件的.init.text段中。
第一个for循环将所有段的sh_entsize设置为一个特殊值——0xffffffff。这是个标记,凡是sh_entsize等于这个值的段,就是还未被分配虚拟空间偏移的段。
前面说了,为ko文件分配的最终虚拟地址有两块,core空间和init空间,core_size和init_size记录了这两个空间的size,初始值为0。
第二个for循环为所有具有SHF_ALLOC标志,并且非.init的段分配其在core虚拟空间的偏移,这些段后面将会被复制到core虚拟空间,这是不会被自动释放,常驻内核的空间。
每个段在core空间的偏移记录在sh_entsize中,偏移是通过get_offset得到的.
当第二个for循环完毕,第三个for循环就为.init段分配其在init虚拟空间的偏移,分配方法和前面一样,然后返回layout_and_allocate()函数。layout_and_allocate()函数接着调用layout_symtab()为符号表和字符串表分配虚拟空间。symsect和strsect分别是表示符号表和字符串表的段描述符。符号表和字符串表会在core空间与init空间同时分配。
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;
先为符号表在init空间分配偏移:
/* Put symbol section at end of init part of module. */
symsect->sh_flags |= SHF_ALLOC;
symsect->sh_entsize = get_offset(mod, &mod->init_size, symsect,
info->index.sym) | INIT_OFFSET_MASK;
pr_debug("\t%s\n", info->secstrings + symsect->sh_name);
接着为“core符号”及其对应的字符串在core空间分配偏移,其实就是只将部分符号表在core空间分配偏移,遍历符号表,对每个符号表项调用is_core_symbol()函数判断是否为“core符号”,如果是,为core符号对应的字符串分配空间,字符串空间记录在strtab_size中。
src = (void *)info->hdr + symsect->sh_offset;
nsrc = symsect->sh_size / sizeof(*src);
/* Compute total space required for the core symbols' strtab. */
for (ndst = i = 0; i < nsrc; i++) {
if (i == 0 ||
is_core_symbol(src+i, info->sechdrs, info->hdr->e_shnum)) {
strtab_size += strlen(&info->strtab[src[i].st_name])+1;
ndst++;
}
}
core空间的符号表与字符串表分配好了偏移。(注意:分配的偏移没有记录在sh_entsize中,只是记录在info结构体中,也就是说只会为core空间的符号表与字符串表预留好位置,不会真的将符号表、字符串表复制到core空间来)
/* Append room for core symbols at end of core part. */
info->symoffs = ALIGN(mod->core_size, symsect->sh_addralign ?: 1);
info->stroffs = mod->core_size = info->symoffs + ndst * sizeof(Elf_Sym);
mod->core_size += strtab_size;
最后为字符串表分配init空间的偏移。
/* Put string table section at end of init part of module. */
strsect->sh_flags |= SHF_ALLOC;
strsect->sh_entsize = get_offset(mod, &mod->init_size, strsect,
info->index.str) | INIT_OFFSET_MASK;
pr_debug("\t%s\n", info->secstrings + strsect->sh_name);
}
返回layout_and_allocate()函数。调用move_module函数进行虚拟空间的实际申请,和段的加载操作。这时候分配的空间,就是ko模块的运行空间
static int move_module(struct module *mod, struct load_info *info)
{
int i;
void *ptr;
/* Do the allocs. */
先为core空间申请一块大小为core_size的内存,将其首地址赋值给struct module结构体的module_core成员:
ptr = module_alloc_update_bounds(mod->core_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;
再为init空间申请一块大小为init_size的内存,将其首地址赋值给struct module结构体的module_init成员:
memset(ptr, 0, mod->core_size);
mod->module_core = ptr;
if (mod->init_size) {
ptr = module_alloc_update_bounds(mod->init_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_free(mod, mod->module_core);
return -ENOMEM;
}
memset(ptr, 0, mod->init_size);
mod->module_init = ptr;
} else
mod->module_init = NULL;
/* Transfer each section which specifies SHF_ALLOC */
pr_debug("final section addresses:\n");
for (i = 0; i < info->hdr->e_shnum; i++) {
void *dest;
Elf_Shdr *shdr = &info->sechdrs[i];
if (!(shdr->sh_flags & SHF_ALLOC))
continue;
下面的for循环对每个有SHF_ALLOC标记的段分配绝对虚拟地址(前面分配的只是各个段相对于未来要分配的虚拟地址的偏移,也就是相对于module_core和module_init的偏移)。分配绝对虚拟地址很简单,将申请的虚拟空间的地址(分别保存在module_core和module_init中)直接加上之前分配好的偏移量就行了。如下:(符号表和字符串表的绝对虚拟地址都被分配到了init空间内,所以后面搬移的时候是把这两个表搬移到了init空间而非core空间)
if (shdr->sh_entsize & INIT_OFFSET_MASK)
dest = mod->module_init
+ (shdr->sh_entsize & ~INIT_OFFSET_MASK);
else
dest = mod->module_core + shdr->sh_entsize;
开始段的搬移,将段从临时内核空间,搬移到运行时的虚拟地址上去:
if (shdr->sh_type != SHT_NOBITS)
memcpy(dest, (void *)shdr->sh_addr, shdr->sh_size);
/* Update sh_addr to point to copy in image. */
最后把绝对虚拟地址赋值给相应段表项的sh_addr成员。返回。
shdr->sh_addr = (unsigned long)dest;
pr_debug("\t0x%lx %s\n",
(long)shdr->sh_addr, info->secstrings + shdr->sh_name);
}
return 0;
}
move_module()返回后,返回到layout_and_allocate()函数中。layout_and_allocate()函数最后将mod指针变量重新指向搬移后的.gnu.linkonce.this_module段的虚拟地址值。
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
(2)add_unformed_module 函数把mod加载到内核链表中,在其他ko模块链接的时候,有可能会在内核链表中查找已经安装的ko模块的引出符号地址,进行链接
(3)find_module_sections 是把ko的符号表的加载地址放入mod中,方便其他模块链接的时候,可以查找到该模块导出的符号表。
(4)simplify_symbols
接着调用simplify_symbols()函数,这个函数根据ko实际的加载地址修正符号表里的符号的绝对地址,写入到st_value域中(符号表、字符串表现在都在init空间了)。
for循环遍历init空间的符号表,分析每个符号表项的st_shndx域,st_shndx通常表示符号所在的段,但它有三个特殊值:SHN_ABS,SHN_COMMON,SHN_UNDEF。所以函数中分了4种case来进行处理。其实需要地址修正的符号一般包括两种,一种是引用了外部符号,这些符号多为SHN_UNDEF类型的,需要查找内核或者其他ko模块的符号表来修正该地址。还有一种是模块内部的全局符号,包括内部函数以及全局变量,对于这些符号的引用一般不是相对寻址,所以其地址会随着加载地址不同而变化,所以也需要修正。
对于模块内的符号,程序进入default进行处理,处理很简单,st_value = st_value + 符号所在段的绝对虚拟地址(st_value中原本保存着符号在其所在段的offset)。这样一来,st_value中现在保存的就是符号的绝对虚拟地址了。
对于内核导出的符号,由于它在模块中没有定义,所以它的st_shndx为SHN_UNDEF。对于SHN_UNDEF 这种case的处理过程如下:
1:调用resolve_symbol_wait()函数解析内核符号,这个函数返回一个struct kernel_symbol结构体。
2:将这个结构体的value成员直接赋值给st_value。
结构体定义在include/linux/export.h中:
struct kernel_symbol
{
unsigned long value;
const char *name;
};
static int verify_export_symbols(struct module *mod)
{
unsigned int i;
struct module *owner;
const struct kernel_symbol *s;
struct {
const struct kernel_symbol *sym;
unsigned int num;
} arr[] = {
{ mod->syms, mod->num_syms },
{ mod->gpl_syms, mod->num_gpl_syms },
{ mod->gpl_future_syms, mod->num_gpl_future_syms },
#ifdef CONFIG_UNUSED_SYMBOLS
{ mod->unused_syms, mod->num_unused_syms },
{ mod->unused_gpl_syms, mod->num_unused_gpl_syms },
#endif
};
for (i = 0; i < ARRAY_SIZE(arr); i++) {
for (s = arr[i].sym; s < arr[i].sym + arr[i].num; s++) {
if (find_symbol(s->name, &owner, NULL, true, false)) {
printk(KERN_ERR
"%s: exports duplicate symbol %s"
" (owned by %s)\n",
mod->name, s->name, module_name(owner));
return -ENOEXEC;
}
}
}
return 0;
}
resolve_symbol_wait()函数用来解析内核导出的符号以及其他控模块导出的符号。不是所有的内核符号都默认导出的,默认内核中的符号在运行时是对外“不可见的”,而内核本身对那些符号地址的引用,都是静态编译链接内核时,链接器写进去的。所以外部模块无法得到内核符号的地址。如果外部模块想要引用内核符号,除非内核将符号地址导出来!内核中的符号可以通过EXPORT_SYMBOL()宏来导出,这个宏就是将符号信息保存在一个struct kernel_symbol结构体中,再将这个结构体编译进内核的一个特殊段,以后如果外部想引用这个符号,只需要在这个段中寻找对应的符号的struct kernel_symbol结构体就行了。
resolve_symbol_wait
-------------->resolve_symbol
--------------->find_symbol
---------------->each_symbol_section
bool each_symbol_section(bool (*fn)(const struct symsearch *arr,
struct module *owner,
void *data),
void *data)
{
struct module *mod;
static const struct symsearch arr[] = {
{ __start___ksymtab, __stop___ksymtab, __start___kcrctab,
NOT_GPL_ONLY, false },
{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
__start___kcrctab_gpl,
GPL_ONLY, false },
{ __start___ksymtab_gpl_future, __stop___ksymtab_gpl_future,
__start___kcrctab_gpl_future,
WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
{ __start___ksymtab_unused, __stop___ksymtab_unused,
__start___kcrctab_unused,
NOT_GPL_ONLY, true },
{ __start___ksymtab_unused_gpl, __stop___ksymtab_unused_gpl,
__start___kcrctab_unused_gpl,
GPL_ONLY, true },
#endif
};
if (each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data))
return true;
list_for_each_entry_rcu(mod, &modules, list) {
struct symsearch arr[] = {
{ mod->syms, mod->syms + mod->num_syms, mod->crcs,
NOT_GPL_ONLY, false },
{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
mod->gpl_crcs,
GPL_ONLY, false },
{ mod->gpl_future_syms,
mod->gpl_future_syms + mod->num_gpl_future_syms,
mod->gpl_future_crcs,
WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
{ mod->unused_syms,
mod->unused_syms + mod->num_unused_syms,
mod->unused_crcs,
NOT_GPL_ONLY, true },
{ mod->unused_gpl_syms,
mod->unused_gpl_syms + mod->num_unused_gpl_syms,
mod->unused_gpl_crcs,
GPL_ONLY, true },
#endif
};
if (mod->state == MODULE_STATE_UNFORMED)
continue;
if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data))
return true;
}
return false;
}
可以看到先去解析内核的符号段,__start___ksymtab,__stop___ksymtab等符号是内核特殊的符号段,编译的时候用特定的宏可以把符号编译到这些段中方便引出,查看内核的system.map符号表就可以看到好些符号位于这些段中。比如内核符号通过EXPORT_SYMBOL()宏导出到一个特殊段,在链接内核的时候,链接脚本就将这些段合并为几个内核符号表段,并定义了几个标志开始和结束地址的符号,如__start___ksymtab,__stop___ksymtab就标识了___ksymtab符号表段的开始地址和结束地址。如果没找到,则会去查找其他ko模块引出方符号。
(5)apply_relocations
上面只是修复了符号表,但是真正的代码段并未发生变化,程序运行的时候并不依赖符号表,所以需要通过重定位表和符号表,来进行真正的重定位工作。这边如何理解先修复符号表,再根据重定位表进行重定位,这边举一个小例子分析一下基本的原理。
如果有如下一个程序:
#include<stdio.h>
int a = 9;
extern int b;
int inc_test(int m,int n) {
return m - n;
}
extern int add_test(int m, int n);
void test() {
int c;
int d;
int e;
c=a+b;
d=inc_test(a,b);
e=add_test(a,b);
a=inc_test(a,b);
printf("c=%d\n",c);
}
我们把其编译成一个.o文件
在这个文件中定义了全局变量a, 外部变量b,内部函数inc_test,和外部函数add_test,
可以看一下text段的反汇编,主要包含test和inc_test函数:
Disassembly of section .text:
00000000 <inc_test>:
0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
4: e28db000 add fp, sp, #0 ; 0x0
8: e24dd00c sub sp, sp, #12 ; 0xc
c: e50b0008 str r0, [fp, #-8]
10: e50b100c str r1, [fp, #-12]
14: e51b2008 ldr r2, [fp, #-8]
18: e51b300c ldr r3, [fp, #-12]
1c: e0633002 rsb r3, r3, r2
20: e1a00003 mov r0, r3
24: e28bd000 add sp, fp, #0 ; 0x0
28: e8bd0800 pop {fp}
2c: e12fff1e bx lr
00000030 <test>:
30: e92d4800 push {fp, lr}
34: e28db004 add fp, sp, #4 ; 0x4
38: e24dd010 sub sp, sp, #16 ; 0x10
3c: e59f3098 ldr r3, [pc, #152] ; dc <test+0xac>
40: e5932000 ldr r2, [r3]
44: e59f3094 ldr r3, [pc, #148] ; e0 <test+0xb0>
48: e5933000 ldr r3, [r3]
4c: e0823003 add r3, r2, r3
50: e50b3010 str r3, [fp, #-16]
54: e59f3080 ldr r3, [pc, #128] ; dc <test+0xac>
58: e5932000 ldr r2, [r3]
5c: e59f307c ldr r3, [pc, #124] ; e0 <test+0xb0>
60: e5933000 ldr r3, [r3]
64: e1a00002 mov r0, r2
68: e1a01003 mov r1, r3
6c: ebfffffe bl 0 <inc_test>
70: e1a03000 mov r3, r0
74: e50b300c str r3, [fp, #-12]
78: e59f305c ldr r3, [pc, #92] ; dc <test+0xac>
7c: e5932000 ldr r2, [r3]
80: e59f3058 ldr r3, [pc, #88] ; e0 <test+0xb0>
84: e5933000 ldr r3, [r3]
88: e1a00002 mov r0, r2
8c: e1a01003 mov r1, r3
90: ebfffffe bl 0 <add_test>
94: e1a03000 mov r3, r0
98: e50b3008 str r3, [fp, #-8]
9c: e59f3038 ldr r3, [pc, #56] ; dc <test+0xac>
a0: e5932000 ldr r2, [r3]
a4: e59f3034 ldr r3, [pc, #52] ; e0 <test+0xb0>
a8: e5933000 ldr r3, [r3]
ac: e1a00002 mov r0, r2
b0: e1a01003 mov r1, r3
b4: ebfffffe bl 0 <inc_test>
b8: e1a02000 mov r2, r0
bc: e59f3018 ldr r3, [pc, #24] ; dc <test+0xac>
c0: e5832000 str r2, [r3]
c4: e59f0018 ldr r0, [pc, #24] ; e4 <test+0xb4>
c8: e51b1010 ldr r1, [fp, #-16]
cc: ebfffffe bl 0 <printf>
d0: e24bd004 sub sp, fp, #4 ; 0x4
d4: e8bd4800 pop {fp, lr}
d8: e12fff1e bx lr
...
主要分析test函数,test函数的3c和44这两行其实分别是对内部全局变量a的寻址和外部变量b的寻址,这边先看3c,这个寻址地址是dc,可以看到test函数的结尾是d8,其实对于全局变量,有的编译器会在test段的末尾,放一些空间,如这边的dc位置,会放置a变量在data段中的地址,因为编译的时候无法得知数据段会被加载到什么地方,所以利用这种方法在冲低位的时候修复该变量地址就可以实现正确寻址。再来看一下内部符号inc_test和外部符号add_test,可以看到编译的时候,编译器都不为其指定跳转地址,这些地址都需要在链接的时候对其修正,
下面看一下其符号表和重定位表
Relocation section '.rel.text' at offset 0x65c contains 9 entries:
Offset Info Type Sym.Value Sym. Name
0000002c 00000028 R_ARM_V4BX
0000006c 0000121c R_ARM_CALL 00000000 inc_test
00000090 0000151c R_ARM_CALL 00000000 add_test
000000b4 0000121c R_ARM_CALL 00000000 inc_test
000000cc 0000161c R_ARM_CALL 00000000 printf
000000d8 00000028 R_ARM_V4BX
000000dc 00001102 R_ARM_ABS32 00000000 a
000000e0 00001702 R_ARM_ABS32 00000000 b
000000e4 00000a02 R_ARM_ABS32 00000000 .rodata
There are no unwind sections in this file.
Symbol table '.symtab' contains 25 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS test.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 NOTYPE LOCAL DEFAULT 3 $d
6: 00000000 0 NOTYPE LOCAL DEFAULT 1 $a
7: 00000000 0 SECTION LOCAL DEFAULT 5
8: 00000000 0 SECTION LOCAL DEFAULT 6
9: 00000000 0 NOTYPE LOCAL DEFAULT 6 $d
10: 00000000 0 SECTION LOCAL DEFAULT 8
11: 00000000 0 NOTYPE LOCAL DEFAULT 8 $d
12: 000000dc 0 NOTYPE LOCAL DEFAULT 1 $d
13: 00000000 0 NOTYPE LOCAL DEFAULT 5 $d
14: 00000000 0 SECTION LOCAL DEFAULT 10
15: 00000000 0 SECTION LOCAL DEFAULT 9
16: 00000000 0 SECTION LOCAL DEFAULT 11
17: 00000000 4 OBJECT GLOBAL DEFAULT 3 a
18: 00000000 48 FUNC GLOBAL DEFAULT 1 inc_test
19: 00000000 0 NOTYPE GLOBAL DEFAULT UND __aeabi_unwind_cpp_pr0
20: 00000030 184 FUNC GLOBAL DEFAULT 1 test
21: 00000000 0 NOTYPE GLOBAL DEFAULT UND add_test
22: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
23: 00000000 0 NOTYPE GLOBAL DEFAULT UND b
24: 00000000 0 NOTYPE GLOBAL DEFAULT UND __aeabi_unwind_cpp_pr1
先看符号表,可以看到符号表中的符号的值大多为0,这些值就是这些符号在段中的地址,这些地址根据链接地址,大多是需要修正的,然后在看一下重定位表,
重定位项的结构一般如下:
typedef struct {
ELF32_ADDR r_offset;
ELF32_Word r_info;
} ELF32_Word r_info;
r_offset 该重定位符号在重定位段中的偏移,等会可以在符号表中看到。
r_info的低8位表示重定位的类型,高24位表示重定位符号在符号表中的下表。
有了这些信息,我们就知道如何去重定位了,回到上面的重定位表,rel.text。指的就是text段的重定位,表中有两个inc_test选项,因为在test函数中,引用了两次inc_test,所以对于这两次引用的寻址代码都需要重定位。
0000006c 0000121c R_ARM_CALL 00000000 inc_test
第一次引用的地址在代码段0000006c 中,查看反汇编刚好就是6c的位置,说明这句代码在符号表修正后,需要进行重定位,
000000b4 0000121c R_ARM_CALL 00000000 inc_test
第二次引用在000000b4 中,刚好页对应反汇编中的b4的位置,而a,b等便令虽然有多次引用,但是这些变量在test段中为其开辟了独立的空间,代码段中的引用总是指向该空间,所以只要修正该空间中记录的真正的data的地址就行了,所以在重定位表中只需要修复一次。
再来看inc_test符号的r_info 的值为0000121c ,所以该符号在符号表中的索引为,18,刚好符号表的第18项就是inc_test,所以符号重定位的原理大概如上所述,下面接着分析ko模块的重定位。
static int apply_relocations(struct module *mod, const struct load_info *info)
{
unsigned int i;
int err = 0;
/* Now do relocations. */
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)
err = apply_relocate(info->sechdrs, info->strtab,
info->index.sym, i, mod);
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循环遍历临时内核空间的各个段,筛选出其中有效的重定位表段,对重定位表所作用的段进行重定位。重定位段的类型主要有SHT_REL和SHT_RELA,以SHT_REL为例,当重定位表段的类型是SHT_REL时,调用apply_relocate()进行重定位。
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;
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;
#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:
*(u32 *)loc += sym->st_value;
break;
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) {
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;
*(u32 *)loc &= 0xff000000;
*(u32 *)loc |= offset & 0x00ffffff;
break;
case R_ARM_V4BX:
/* Preserve Rm and the condition code. Alter
* other bits to re-code instruction as
* MOV PC,Rm.
*/
*(u32 *)loc &= 0xf000000f;
*(u32 *)loc |= 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 = *(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;
*(u32 *)loc &= 0xfff0f000;
*(u32 *)loc |= ((offset & 0xf000) << 4) |
(offset & 0x0fff);
break;
#ifdef CONFIG_THUMB2_KERNEL
case R_ARM_THM_CALL:
case R_ARM_THM_JUMP24:
upper = *(u16 *)loc;
lower = *(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;
/*
* 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 &&
!(offset & 1)) ||
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);
*(u16 *)loc = (u16)((upper & 0xf800) | (sign << 10) |
((offset >> 12) & 0x03ff));
*(u16 *)(loc + 2) = (u16)((lower & 0xd000) |
(j1 << 13) | (j2 << 11) |
((offset >> 1) & 0x07ff));
break;
case R_ARM_THM_MOVW_ABS_NC:
case R_ARM_THM_MOVT_ABS:
upper = *(u16 *)loc;
lower = *(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;
*(u16 *)loc = (u16)((upper & 0xfbf0) |
((offset & 0xf000) >> 12) |
((offset & 0x0800) >> 1));
*(u16 *)(loc + 2) = (u16)((lower & 0x8f00) |
((offset & 0x0700) << 4) |
(offset & 0x00ff));
break;
#endif
default:
printk(KERN_ERR "%s: unknown relocation: %u\n",
module->name, ELF32_R_TYPE(rel->r_info));
return -ENOEXEC;
}
}
return 0;
}
这个函数所做的大概工作就是遍历重定位表项,对每个重定位项,找到重定位入口地址,再根据符号表得到符号的绝对虚拟地址,再根据重定位入口的类型,进行对应的地址修正。总之,重定位完成后,代码中对符号的引用,都将会被修正为符号在内核的正确地址。
这里有一点要注意下,前面不是提到.gnu.linkonce.this_module段吗?这个段也有一个自己的重定位表,叫.rel.gnu.linkonce.this_module,这个重定位表里只有两个重定位表项,还记得前面提到的struct module结构体吗?现在给出struct module结构体的定义。。module结构内有两个成员,init和exit。这两个成员存放着模块的__init函数和__exit函数的指针,.rel.gnu.linkonce.this_module重定位表中的两个重定位项就分别对应着.gnu.linkonce.this_module段中的这两个指针!
也就是说,这两个指针的值也会被apply_relocate()函数重定位,重定位这两个指针有什么用呢?因为后面将会用这两个指针,调用模块的__init函数和__exit函数。
struct module
{
enum module_state state;
/* Member of list of modules */
struct list_head list;
/* Unique handle for this module */
char name[MODULE_NAME_LEN];
/* Sysfs stuff. */
struct module_kobject mkobj;
struct module_attribute *modinfo_attrs;
const char *version;
const char *srcversion;
struct kobject *holders_dir;
/* Exported symbols */
const struct kernel_symbol *syms;
const unsigned long *crcs;
unsigned int num_syms;
/* Kernel parameters. */
struct kernel_param *kp;
unsigned int num_kp;
/* GPL-only exported symbols. */
unsigned int num_gpl_syms;
const struct kernel_symbol *gpl_syms;
const unsigned long *gpl_crcs;
#ifdef CONFIG_UNUSED_SYMBOLS
/* unused exported symbols. */
const struct kernel_symbol *unused_syms;
const unsigned long *unused_crcs;
unsigned int num_unused_syms;
/* GPL-only, unused exported symbols. */
unsigned int num_unused_gpl_syms;
const struct kernel_symbol *unused_gpl_syms;
const unsigned long *unused_gpl_crcs;
#endif
/* symbols that will be GPL-only in the near future. */
const struct kernel_symbol *gpl_future_syms;
const unsigned long *gpl_future_crcs;
unsigned int num_gpl_future_syms;
/* Exception table */
unsigned int num_exentries;
struct exception_table_entry *extable;
/* Startup function. */
int (*init)(void);
/* If this is non-NULL, vfree after init() returns */
void *module_init;
/* Here is the actual code + data, vfree'd on unload. */
void *module_core;
/* Here are the sizes of the init and core sections */
unsigned int init_size, core_size;
/* The size of the executable code in each section. */
unsigned int init_text_size, core_text_size;
/* Size of RO sections of the module (text+rodata) */
unsigned int init_ro_size, core_ro_size;
/* Arch-specific module values */
struct mod_arch_specific arch;
unsigned int taints; /* same bits as kernel:tainted */
#ifdef CONFIG_GENERIC_BUG
/* Support for BUG */
unsigned num_bugs;
struct list_head bug_list;
struct bug_entry *bug_table;
#endif
#ifdef CONFIG_KALLSYMS
/*
* We keep the symbol and string tables for kallsyms.
* The core_* fields below are temporary, loader-only (they
* could really be discarded after module init).
*/
Elf_Sym *symtab, *core_symtab;
unsigned int num_symtab, core_num_syms;
char *strtab, *core_strtab;
/* Section attributes */
struct module_sect_attrs *sect_attrs;
/* Notes attributes */
struct module_notes_attrs *notes_attrs;
#endif
/* The command line arguments (may be mangled). People like
keeping pointers to this stuff */
char *args;
#ifdef CONFIG_SMP
/* Per-cpu data. */
void __percpu *percpu;
unsigned int percpu_size;
#endif
#ifdef CONFIG_TRACEPOINTS
unsigned int num_tracepoints;
struct tracepoint * const *tracepoints_ptrs;
#endif
#ifdef HAVE_JUMP_LABEL
struct jump_entry *jump_entries;
unsigned int num_jump_entries;
#endif
#ifdef CONFIG_TRACING
unsigned int num_trace_bprintk_fmt;
const char **trace_bprintk_fmt_start;
#endif
#ifdef CONFIG_EVENT_TRACING
struct ftrace_event_call **trace_events;
unsigned int num_trace_events;
#endif
#ifdef CONFIG_FTRACE_MCOUNT_RECORD
unsigned int num_ftrace_callsites;
unsigned long *ftrace_callsites;
#endif
#ifdef CONFIG_MODULE_UNLOAD
/* What modules depend on me? */
struct list_head source_list;
/* What modules do I depend on? */
struct list_head target_list;
/* Who is waiting for us to be unloaded */
struct task_struct *waiter;
/* Destruction function. */
void (*exit)(void);
struct module_ref __percpu *refptr;
#endif
#ifdef CONFIG_CONSTRUCTORS
/* Constructor functions. */
ctor_fn_t *ctors;
unsigned int num_ctors;
#endif
};
继续一路返回到load_module()函数,load_module()后面的代码不看,一路返回到sys_init_module(),在sys_init_module()中,后面会调用do_one_initcall(),参数就是被重定位过的init指针。
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
这个do_one_initcall()函数会调用这个init指针所指向的函数,至此,我们模块的__init函数就被调用了。
(__init函数是指用__init前缀定义的函数,__exit函数是指用__exit前缀定义的函数,前面提过。)
文中好多内容参考自这篇文章: