内核模块学习记录01:ELF文件的解析
模块ELF的解析
本系列主要保留本人在学习内核模块(2.6.24)部分的一些文字材料,涉及的内容主要包括:
- ELF格式的解析
- 内核对模块的重定位
本文为该系列的第一篇:模块ELF的解析
如有谬误请批评指正
模块ELF的格式
本文针对ELF的剖析,限定在理解内核如何解析module的范围内,避免宽泛的讨论。
模块ELF的链接视图如下所示
如图所示,对内核加载解析module的过程而言,重点内容为对节头表以及各节的内容的解析。
内核对模块ELF的基本操作
为了更好的了解对module的解析过程,不妨以如下问题作为引导:
- 内核如何访问用户提供模块文件?
- 内核如何确定各节的名称?
- 如何检索各节?
- 有哪些重要的节?作用是什么?
- 内核如何找到模块文件中内建的module对象?
- 内核如何访问用户提供模块文件?
首先在Elf_Ehdr *hdr处分配elf.len长度的临时内核空间,使用copy_from_user()将用户空间指定的elf文件加载到该地址处,接下来通过Elf_Ehdr *hdr完成对elf的解析。
/* Suck in entire file: we'll want most of it. */
/* vmalloc barfs on "unusual" numbers. Check here */
if (len > 64 * 1024 * 1024 || (hdr = vmalloc(len)) == NULL)
return ERR_PTR(-ENOMEM);
if (copy_from_user(hdr, umod, len) != 0) {
err = -EFAULT;
goto free_hdr;
}
-
内核如何确定各节的名称?
ELF头中的e_shstrndx描述了保存各节名称的节在节头表的索引。通过该节,再结合字符串节即可以得到各个节的名称。 -
如何检索各节?
首先根据ELF头确定节头表所在的位置,然后即可遍历节头表中的节头表项。节头表项的个数ELF头中e_shnum确定,注意这里操作的是节头表项,如果需要直接访问具体的节,需要通过节头表项中的成员得到节的具体偏移值。
for (i = 1; i < hdr->e_shnum; i++) {
……
}
- 有哪些主要的节?作用是什么?
-- 代码节:存储代码
-- 数据节:存储数据
-- 重定位节:存储重定位项
-- 符号表节:存储符号和对应的地址(需要引用字符串表)
-- 字符串节:存储elf中所有被引用的字符串,如“各节名称”“符号名称”
-- 名称节:存储了各个节的名称(需要引用字符串表)
-- 信息节:模块的信息,如作者,版本,联系方式等
-- gnu.linkonce.this_module节:内建的module对象
主要的节如下所示:
mit6828@mit:~/driver$ readelf -SW helloDev.ko
There are 20 section headers, starting at offset 0x1218:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-id NOTE 0000000000000000 000040 000024 00 A 0 0 4
[ 2] .text PROGBITS 0000000000000000 000070 000150 00 AX 0 0 16
[ 3] .rela.text RELA 0000000000000000 000cd0 000378 18 I 17 2 8
[ 4] .exit.text PROGBITS 0000000000000000 0001c0 000023 00 AX 0 0 1
[ 5] .rela.exit.text RELA 0000000000000000 001048 000078 18 I 17 4 8
[ 6] .rodata.str1.1 PROGBITS 0000000000000000 0001e3 000051 01 AMS 0 0 1
[ 7] .rodata.str1.8 PROGBITS 0000000000000000 000238 000023 01 AMS 0 0 8
[ 8] .modinfo PROGBITS 0000000000000000 000260 000087 00 A 0 0 8
[ 9] __mcount_loc PROGBITS 0000000000000000 0002e8 000020 00 A 0 0 8
[10] .rela__mcount_loc RELA 0000000000000000 0010c0 000060 18 I 17 9 8
[11] .data PROGBITS 0000000000000000 000308 000008 00 WA 0 0 4
[12] .gnu.linkonce.this_module PROGBITS 0000000000000000 000340 000340 00 WA 0 0 64
[13] .rela.gnu.linkonce.this_module RELA 0000000000000000 001120 000030 18 I 17 12 8
[14] .bss NOBITS 0000000000000000 000680 000028 00 WA 0 0 8
[15] .comment PROGBITS 0000000000000000 000680 000054 01 MS 0 0 1
[16] .note.GNU-stack PROGBITS 0000000000000000 0006d4 000000 00 0 0 1
[17] .symtab SYMTAB 0000000000000000 0006d8 000450 18 18 21 8
[18] .strtab STRTAB 0000000000000000 000b28 0001a7 00 0 0 1
[19] .shstrtab STRTAB 0000000000000000 001150 0000c5 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
- 内核是如何加载节的?
内核为存储elf分配的空间为临时空间,加载模块时会为core和data分配单独的内存空间,并建立起module对象并将module对象挂在到内核moudle相关的链表中。
首先为core何data分配内存,然后将各节的内容拷贝到core或data中。
/* Do the allocs. */
//为module_core分配内存
ptr = module_alloc(mod->core_size);
if (!ptr) {
err = -ENOMEM;
goto free_percpu;
}
//清moudle_core,调整内建module中的module_core指向此
memset(ptr, 0, mod->core_size);
mod->module_core = ptr;
//为module_init分配内存
ptr = module_alloc(mod->init_size);
if (!ptr && mod->init_size) {
err = -ENOMEM;
goto free_core;
}
//清moudle_init,调整内建module中的module_init指向此
memset(ptr, 0, mod->init_size);
mod->module_init = ptr;
然后遍历节,拷贝内容到目标位置
/* Transfer each section which specifies SHF_ALLOC */
DEBUGP("final section addresses:\n");
for (i = 0; i < hdr->e_shnum; i++) {
void *dest;
if (!(sechdrs[i].sh_flags & SHF_ALLOC))
continue;
//对需要加载到内核的节
//如果此节是否属于module:init的一部分,则将此节加载到module_init
//否则则将此节加载到module_core
if (sechdrs[i].sh_entsize & INIT_OFFSET_MASK)
dest = mod->module_init
+ (sechdrs[i].sh_entsize & ~INIT_OFFSET_MASK);
else
dest = mod->module_core + sechdrs[i].sh_entsize;
//拷贝节到dest
if (sechdrs[i].sh_type != SHT_NOBITS)
memcpy(dest, (void *)sechdrs[i].sh_addr,
sechdrs[i].sh_size);
/* Update sh_addr to point to copy in image. */
//调整elf节头表中“当前节映射到虚拟地址空间中的位置”,此时节已经从elf的节拷贝到module:init
//或module:core中了
sechdrs[i].sh_addr = (unsigned long)dest;
DEBUGP("\t0x%lx %s\n", sechdrs[i].sh_addr, secstrings + sechdrs[i].sh_name);
}
- 内核如何确定ELF中内建的moudle对象?
内建的moudle对象,保存在“gnu.linkonce.this_module”节中,因此根据节名检索到对应的节即可。
//找到 ".gnu.linkonce.this_module"节在节头表中的索引
modindex = find_sec(hdr, sechdrs, secstrings,
".gnu.linkonce.this_module");
if (!modindex) {
printk(KERN_WARNING "No module found in object\n");
err = -ENOEXEC;
goto free_hdr;
}
小结
如上就是内核解析模块ELF时的一些最基本的操作。
内核完成对模块ELF进行最重要的操作之一:完成对符号表的重定位,下一篇为录。
参考资料
- 深入Linux内核架构:第七章:模块;附录E:ELF二进制格式
- 2.6.24源码