在内核中维护者一张符号表,记录了内核中所有的符号(函数、全局变量等)的地址以及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。而内核代码中可以通过 printk("%pS\n", addr) 打印符号名。
本文介绍内核符号表的生成和查找过程。
1. System.map和/proc/kallsyms
System.map文件是编译内核时生成的,它记录了内核中的符号列表,以及符号在内存中的虚拟地址。这个文件通过nm命令生成,具体可参考内核目录下的scripts/mksysmap脚本。System.map中每个条目由三部分组成,例如:
f0081e80 T alloc_vfsmnt
即“地址 符号类型 符号名”
其中符号类型有如下几种:
- A =Absolute
- B =Uninitialised data (.bss)
- C = Comonsymbol
- D =Initialised data
- G =Initialised data for small objects
- I = Indirectreference to another symbol
- N =Debugging symbol
- R = Readonly
- S =Uninitialised data for small objects
- T = Textcode symbol
- U =Undefined symbol
- V = Weaksymbol
- W = Weaksymbol
- Corresponding small letters are local symbols
/proc/kallsyms文件是在内核启动后生成的,位于文件系统的/proc目录下,实现代码见kernel/kallsyms.c。前提是内核必须打开CONFIG_KALLSYMS编译选项。它和System.map的区别是它同时包含了内核模块的符号列表。
通常情况下我们只需要_stext~_etext和_sinittext~_einittext之间的符号,如果需要将nm命令获得的所有符号都记录下来,则需要开启内核的CONFIG_KALLSYMS_ALL编译选项,不过一般是不需要打开的。
2. 内核符号表
内核在执行过程中,可能需要获得一个地址所在的函数,比如在输出某些调试信息的时候。一个典型的例子就是使用dump_stack()函数打印栈回溯信息。
但是内核在查找一个地址对应的函数名时,没有求助于上述两个文件,而是在编译内核时,向vmlinux嵌入了一个符号表,这样做可能是为了方便快速的查找并避免文件操作带来的不良影响。
2.1 内核符号表的结构
内嵌的符号表是通过内核目录下的scripts/kallsyms工具生成的,工具的源码为相同目录下的kallsyms.c。这个工具的用法如下:
nm -n vmlinux | scripts/kallsyms [--all-symbols] > symbols.S
可见同样是通过nm命令得到vmlinux的符号表,并将这些符号表信息进行调整,最终生成一个汇编文件。这个汇编文件中包含了6个全局变量:kallsyms_addresses,kallsyms_num_syms,kallsyms_names,kallsyms_markers,kallsyms_token_table和kallsyms_token_index,其中: