在前两篇文章中,我们讨论了编译Linux内核和vmlinux,本文就围绕Linux内核符号表展开,简单记录其核心概念。内核符号表在内核编译后对应文件System.map,它伴随编译过程产生,并在vmlinux压缩时嵌入。本文将聚焦三个核心问题:符号表是什么、怎么来的,以及用来做什么。
什么是符号表?
符号表本质上是一个列表,记录了Linux内核中的符号(即变量和函数名称)及其在内存中的虚拟地址。你可能马上会联想到 /proc/kallsyms 下显示的映射关系,它与System.map有何区别?
简单来说,System.map是在编译阶段生成的静态内核符号表,而 /proc/kallsyms 则是内核启动后动态生成的运行时符号表。动态符号表会随内核模块的加载而更新,因此开发者插入模块后,其export的函数也能在 /proc/kallsyms 中查到映射。
符号表System.map是怎样生成的?
符号表的生成源于编译过程。具体来说,它由kernel目录下的 scripts/kallsyms 工具产生。我们不深究嵌入细节,先看看编译期间的生成流程。
根Makefile会执行 scripts/Makefile.vmlinux,进而调用脚本 scripts/link-vmlinux.sh。在这个脚本中,mksysmap 函数定义如下:
mksysmap()
{
info NM ${2}
${CONFIG_SHELL} "${srctree}/scripts/mksysmap" ${1} ${2}
}
编译时通过命令 mksysmap vmlinux System.map 触发。继续查看 scripts/mksysmap 脚本,其内容实为调用NM工具:
$NM -n $1 | grep -v \
-e ' [aNUw] ' \
-e ' \$' \
-e ' \.L' \
-e ' __crc_' \
-e ' __kstrtab_' \
-e ' __kstrtabns_' \
-e ' L0$' \
> $2
由此可见,System.map的实际生成工具是NM。其具体实现在同目录的 scripts/kallsyms.c 中,涉及三个关键变量:
kallsyms_addresses:按地址升序排列的符号地址数组。
kallsyms_num_syms:符号数量。
kallsyms_names:与地址一一对应的符号名称数组。
符号表用来做什么?
a. 内核模块开发
在内核中,常看到通过 EXPORT_SYMBOL 导出的函数。编写内核模块时,我们可用 extern 声明这些外部符号。模块加载时,内核会查找符号表中对应地址,并填充到模块符号表,使模块能调用这些函数。反之,模块自身导出的函数也可供其他模块使用。
示例代码:
//kernel:
void function_xxx(void)
{
...
}
EXPORT_SYMBOL(function_xxx);
//编写ko:
extern void function_xxx(void);
b. 内核崩溃调试
内核崩溃(panic)时,调用栈会打印一系列地址。借助符号表,这些地址可转换为函数名称,极大便利问题定位。类似地,dump_stack 也依赖符号表查询来打印函数名。
c. 函数Hook与高级调试
利用符号表的地址映射,我们可以根据函数名查询地址,实现Hook操作。例如,kprobe 在钩子函数时正是通过查询符号表来获取目标地址,从而支持调试或新功能开发。
更多操作系统与底层技术讨论,欢迎访问云栈社区。
|