Linux 驱动模块内存精简

文章目录

  • 1. Strip 文件
  • 2. 运行时内存占用
  • 参考文档

Linux 驱动模块可以独立的编译成 .ko 文件,虽然大小一般只有几 MB,但对总内存只有几十 MB 的小型 Linux 系统来说,常常也是一个非常值得优化的点。本文以一个实际例子,详细描述一下 .ko 内存精简优化的具体过程。

1. Strip 文件

因为 .ko 文件是一个标准的 ELF 文件,通常我们首先会想到使用 strip 命令来精简文件大小。strip .ko 有以下几种选项:

strip --strip-all test.ko               // strip 掉所有的调试段,ko 文件体积减少很多,ko 不能正常 insmod
strip --strip-debug test.ko             // strip 掉 debug 段,ko 文件体积减少不多,ko 可以正常 insmod
strip --strip-unneeded test.ko          // strip 掉 和动态重定位无关的段,ko 文件体积减少不多,ko 可以正常 insmod

.ko 文件具体的体积变化:

 6978208 origin-test.ko*                // no strip
 1984856 strip-all-test.ko*             // strip --strip-all
 6884544 strip-debug-test.ko*           // strip --strip-debug
 6830704 strip-unneeded-test.ko*        // strip --strip-unneeded

可以看到在保存 .ko 能正常使用的前提下,strip 命令对 .ko 文件并不能减少多大的体积。而且一通操作下来,.ko 文件中的关键数据 text/data/bss 段的体积没有任何变化:

$ size *.ko
   text	   data	    bss	    dec	    hex	filename
1697671	 275791	  28367	2001829	 1e8ba5	origin-test.ko
1697671	 275791	  28367	2001829	 1e8ba5	strip-all-test.ko
1697671	 275791	  28367	2001829	 1e8ba5	strip-debug-test.ko
1697671	 275791	  28367	2001829	 1e8ba5	strip-unneeded-test.ko
  • Question 1: strip 命令是否还有命令能实现更多的精简?strip 的本质是什么,具体 strip 掉了哪些东西?

我们通过读取 ELF 文件的 section 信息来比较 strip 前后的差异:

$ readelf -S origin-test.ko
There are 48 section headers, starting at offset 0x6a6ea0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.build-i NOTE             0000000000000000  00000040
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .note.Linux       NOTE             0000000000000000  00000064
       0000000000000018  0000000000000000   A       0     0     4
  [ 3] .text             PROGBITS         0000000000000000  0000007c
       00000000001393d6  0000000000000000  AX       0     0     2
  [ 4] .rela.text        RELA             0000000000000000  003b9b90
       00000000002b7550  0000000000000018   I      45     3     8
  [ 5] .text.unlikely    PROGBITS         0000000000000000  00139452
       0000000000000d74  0000000000000000  AX       0     0     2
  [ 6] .rela.text.unlike RELA             0000000000000000  006710e0
       0000000000001950  0000000000000018   I      45     5     8
  [ 7] .init.text        PROGBITS         0000000000000000  0013a1c6
       000000000000016e  0000000000000000  AX       0     0     2
  [ 8] .rela.init.text   RELA             0000000000000000  00672a30
  ...

$ readelf -S strip-all-test.ko
There are 27 section headers, starting at offset 0x1e4298:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.build-i NOTE             0000000000000000  00000040
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .note.Linux       NOTE             0000000000000000  00000064
       0000000000000018  0000000000000000   A       0     0     4
  [ 3] .text             PROGBITS         0000000000000000  0000007c
       00000000001393d6  0000000000000000  AX       0     0     2
  [ 4] .text.unlikely    PROGBITS         0000000000000000  00139452
       0000000000000d74  0000000000000000  AX       0     0     2
  [ 5] .init.text        PROGBITS         0000000000000000  0013a1c6
       000000000000016e  0000000000000000  AX       0     0     2
  ...

从信息上看 strip 主要删除了 FlagsISections,而 FlagsASections 是不能被删除的。关于 Sections Flags 的定义在 Readelf 命令的最后面有详细描述:

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),
  p (processor specific)

另外还发现,对 .ko 文件来说 .rela. 开头的 Sections 是不能被删除的,insmod 时需要这些信息。例如 .rela.text 占用了很大的体积,但是不能直接粗暴的直接 strip 掉。

  • Question 2: 对于 .ko 文件中 FlagsISections 在模块 insmod 以后是否需要占据内存?

内核代码中对 .ko 文件 insmod 动态加载时的主流程:

SYSCALL_DEFINE3(finit_module) / SYSCALL_DEFINE3(init_module)
|→ load_module()
    |→ layout_and_allocate()
    |   |→ setup_load_info()     // info->index.mod = section ".gnu.linkonce.this_module"
    |   |
    |   |→ layout_sections()     // 解析 ko ELF 文件,统计需要加载到内存中的 section
    |   |                         // 累计长度到 mod->core_layout.size 和 mod->init_layout.size
    |   |
    |   |→ layout_symtab()       // 解析 ko ELF 文件,统计需要加载到内存中的符号表
    |   |                         // 累计长度到 mod->core_layout.size
    |   |
    |   |→ move_module()         // 根据 mod->core_layout.size 和 mod->init_layout.size 的长度
    |                             // 使用 vmalloc 分配空间,并且拷贝对应的 section 到内存
    |
    |→ apply_relocations()       // 对加载到内存的 section 做重定位处理
    |
    |→ do_init_module()          // 执行驱动模块的 module_init() 函数,完成后释放 mod->init_layout.size 内存

分析具体的代码细节,发现只有带 ALLOC 属性(即 FlagsA)的 section 才会在模块加载时统计并拷贝进内存:

static void layout_sections(struct module *mod, struct load_info *info)
{
    /* (1) 只识别带 SHF_ALLOC 的 section */
	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_RO_AFTER_INIT | SHF_ALLOC, 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;

    /* (2) 遍历 ko 文件的 section,根据上述标志来统计
            把 ALLOC 类型的 section 统计进 mod->core_layout.size
     */
	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
			    || module_init_section(sname))
				continue;
			s->sh_entsize = get_offset(mod, &mod->core_layout.size, s, i);
			pr_debug("\t%s\n", sname);
		}
	}

    /* (3) 遍历 ko 文件的 section,根据上述标志来统计
            把 ALLOC 类型的并且名字以 '.init' 开头的 section 统计进 mod->init_layout.size
     */
	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
			    || !module_init_section(sname))
				continue;
			s->sh_entsize = (get_offset(mod, &mod->init_layout.size, s, i)
					 | INIT_OFFSET_MASK);
			pr_debug("\t%s\n", sname);
		}
	}
}

FlagsI 的 section 只会在 apply_relocations() 重定位时提供信息,这部分 section 不会在内存中常驻。

结论:strip 操作 .ko 文件只会精简掉少量 I 的 section,.ko 文件少量减小,但是对动态加载后的内存占用毫无影响。

2. 运行时内存占用

但是生活还得继续,优化还得想办法。我们仔细分析关键数据 text/data/bss 段在模块加载过程中的内存占用。

加载前:

$ size test.ko
   text	   data	    bss	    dec	    hex	filename
1697671	 275791	  28367	2001829	 1e8ba5	test.ko

模块 insmod 后的内存占用,因为是通过 vmalloc() 分配的,我们可以通过 vmallocinfo 查看内存占用情况:

# cat /sys/module/test/coresize
4203425
# cat /sys/module/test/initsize
0
# cat /proc/vmallocinfo
// core_layout.size 占用 4.2 M 内存
0x00000000fd4ec521-0x000000007ff17966 4210688 load_module+0x1b86/0x1c8e pages=1027 vmalloc vpages
0x000000007ff17966-0x000000004e29ad2e   16384 load_module+0x1b86/0x1c8e pages=3 vmalloc

可以看到,加载前 test.kotext/data/bss 段的总长为 2 M 左右,但是模块加载后总共占用了 4.2 M 内存。

  • Question 3:为什么模块加载后会有多出的内存占用?

我们在内核代码中加上调试信息,跟踪 mod->core_layout.size 的变化情况,终于找到了关键所在:

SYSCALL_DEFINE3(finit_module) / SYSCALL_DEFINE3(init_module)
|→ load_module()
    |→ layout_and_allocate()
    |   |→ setup_load_info()     // mod->core_layout.size = 0x0.
    |   |
    |   |→ layout_sections()     // mod->core_layout.size = 0x1f8390
    |   |
    |   |→ layout_symtab()       // mod->core_layout.size = 0x4023a1.
    |   |
    |   |→ move_module()         // 根据 mod->core_layout.size 和 mod->init_layout.size 的长度

可以看到是在 layout_symtab() 函数中增大了多余的长度,layout_symtab() 函数在 CONFIG_KALLSYMS 使能的情况下才有效,存储的驱动模块的符号表。

一般情况下我们并不需要模块符号表,可以关闭内核的 CONFIG_KALLSYMS 选项来查看内存的占用情况:

# cat /sys/module/test/coresize
2092876
# cat /sys/module/test/initsize
0
# cat /proc/vmallocinfo
// core_layout.size 占用 2.0 M 内存
0x000000009e1c62e8-0x000000001024ef17 2097152 0xffffffff8006f3de pages=511 vmalloc
0x000000004070c817-0x00000000cc1b6736   28672 0xffffffff41534922 pages=6 vmalloc

多余的 2.2 M 内存被完美的精简下来。

但是这种方法也只能减少 .ko 的静态内存占用,驱动动态分配的内存只能分析代码逻辑去优化。

结论:关闭 CONFIG_KALLSYMS 选项可以精简 .ko 模块符号表的内存占用,精简收益还是不错的。

参考文档

1.strip (从编译的目标文件中丢弃符号)
2.Linux 命令(60)—— strip 命令
3.Meaning of Flags in ELF Section header?
4.linux 内存 - KO内存占用
5.再谈内核模块加载(一)—背景知识
6.再谈内核模块加载(二)—模块加载流程(上)
7.再谈内核模块加载(三)—模块加载流程(下)
8.ko module加载flow
9.Linux中THIS_MODULE宏定义详解 section(“.gnu.linkonce.this_module”)

你可能感兴趣的:(Linux,Kernel解析,linux,ko,内存优化)