老僧非是爱花红

导航

内核模块学习记录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源码

posted on 2020-10-22 20:41  老僧非是爱花红  阅读(493)  评论(0编辑  收藏  举报