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 主要刪除了 Flags 為 I 的 Sections,而 Flags 帶 A 的 Sections 是不能被刪除的。關于 SectionsFlags 的定義在 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?文件中?Flags?為?I?的?Sections?在模塊 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 屬性(即 Flags 帶 A)的 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: ");
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(" %s ", sname);
}
}
/* (3) 遍歷 ko 文件的 section,根據上述標志來統計
把 ALLOC 類型的并且名字以 '.init' 開頭的 section 統計進 mod->init_layout.size
*/
pr_debug("Init section allocation order: ");
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(" %s ", sname);
}
}
}
Flags 帶 I 的 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.ko 的 text/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 模塊符號表的內存占用,精簡收益還是不錯的。
編輯:黃飛
?
評論
查看更多