转自:http://blog.csdn.net/jasonchen_gbd/article/details/44025681
权声明:本文为博主原创文章,转载请附上原博链接。
在内核中维护者一张符号表,记录了内核中所有的符号(函数、全局变量等)的地址以及名字,这个符号表被嵌入到内核镜像中,使得内核可以在运行过程中随时获得一个符号地址对应的符号名。而内核代码中可以通过 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,其中:
- kallsyms_addresses:一个数组,存放所有符号的地址列表,按地址升序排列。
- kallsyms_num_syms:符号的数量。
- kallsyms_names:一个数组,存放所有符号的名称,和kallsyms_addresses一一对应。
其他三个全局变量的含义后续会提到。
这些变量被嵌入在vmlinux中,所以在内核代码中直接extern就可以使用。例如dump_stack()就是通过这些变量来查找一个地址对应的函数名的。
那由scripts/kallsyms生成的汇编文件是如何嵌入到vmlinux中的呢。在编译内核的后期主要进行了一下几步额外的编译和链接过程:
- 链接器ld将内核的绝大部分组件链接成临时内核映像.tmp_vmlinux1。
- 使用nm命令将.tmp_vmlinux1中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms1.S的文件。
- 对.tmp_kallsyms1.S文件进行编译生成.tmp_kallsyms1.o文件。
- 重复1的链接过程,这次将步骤3得到的.tmp_kallsyms1.o文件链接进入内核得到临时内核映像.tmp_vmlinux2文件,其中包含的部分函数和非栈变量的地址发生了变化,但但由于.tmp_kallsyms1.S中的符号表还是旧的,所以.tmp_vmlinux2还不能作为最终的内核映像。
- 再使用nm命令将.tmp_vmlinux2中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms2.S的文件。
- 对.tmp_kallsyms2.S文件进行编译生成.tmp_kallsyms2.o文件。
- .tmp_kallsyms2.o即为最终的kallsyms.o目标,并链接进入内核生成vmlinux文件。
此时,上面的那6个全局变量被写进vmlinux中的“.rodata”段(所以还是叫全局常量吧),内核代码就可以使用了,使用前需extern一下:
extern const unsigned long kallsyms_addresses[] __attribute__((weak));
weak属性表示当我们不确定外部模块是否提供了某个变量或函数时,可以将这个变量或函数定义为弱属性,如果外部有定义则使用,没有定义则相当于自己定义。
在使用这6个全局常量之前,我们先要弄清楚他们都是干什么用的。kallsyms_addresses、kallsyms_num_syms和kallsyms_names在前面已经讲过,实际上他们已经可以提供一个[地址 : 符号]的映射关系了,但是内核中几万个符号这样一条一条的存起来会占用大量的空间,所以内核采用一种压缩算法,将所有符号中出现频率较高的字符串记录成一个个的token,然后将原来的符号中和token匹配的子串进行压缩,这样可以实现使用一个字符来代替n个字符,以减小符号存储长度。
因此符号表维护了一个kallsyms_token_table,他有256个元素,对应一个字节的长度。由于符号名的只能出现下划线、数字和字母,那在kallsyms_token_table[256]数组中,除了这些字符的ASCII码对应的位置,还有很多未被使用的位置就可以用来存储压缩串。kallsyms_token_table表的内容像下面这样:
kallsyms_token_table: .asciz "end" .asciz "Tjffs2" .asciz "map_" .asciz "int" .asciz "to_" .asciz "Tn" .asciz "t__" .asciz "unregist" ... ... .asciz "a" .asciz "b" .asciz "c" .asciz "d" .asciz "e" .asciz "f" .asciz "g" .asciz "h" ... ...
那我们在表示一个函数名时,就可以用0x00来表示“end”,用0x04来表示“to_”等。没有被压缩的如0x61仍然表示“a”。
kallsyms_token_index记录每个token首字符在kallsyms_token_table中的偏移。同token table共256条,在打印token时需要用到。
kallsyms_token_index: .short 0 .short 4 //Tjffs2第一个字符在kallsyms_token_table中的偏移 .short 11 .short 16
至于kallsyms_token_table表是如何生成的,可以阅读scripts/kallsyms.c的实现,大致就是将所有符号出现的相邻的两个字符出现的次数都记录起来,例如对于“nf_nat_nf_init”,就记录下“nf”、“f_”、“_n”、“na”、……,每两个字符组合出现的次数记录在token_profit[0x10000]数组中(两个字符一组,共有2^8 * 2^8 = 0x10000中可能组合),然后挑选出现次数最多的一个组合形成一个token,比如用“g”来表示“nf”,那“nf_nat_nf_init”就被改为“g_nat_g_init”。接下来,再在修改后的所有符号中计算每两个字符的出现次数来挑选出现次数最多的组合,例如用“J”来表示“g_”,那“g_nat_g_init”又被改为“Jnat_Jinit”。直到生成最终的token表。
2.2 内核查找一个符号的过程
这时还没讲到全局常量kallsyms_markers。我们先来看内核如何根据这六个全局常量来查找一个地址对应的函数名的,实现函数为kernel/kallsyms.c中的kallsyms_lookup()。
我不讲函数实现,只是用一个例子来说明内核符号的查找过程:
比如我在内核中想打印出0x80216bf4地址所在的函数。首先不管内核怎么做,我们可以先在System.map文件中看到这个地址位于为nf_register_hook和nf_register_hooks两个符号之间,那可以确定它属于nf_register_hook函数了。
80060000 A _text
... ...
80216b8c T nf_unregister_hooks
80216be4 T nf_register_hook
80216c8c T nf_register_hooks
... ...
注意,System.map和内核启动后的/proc/kallsyms文件中的符号表只是给我们看的,内核不会使用它们。
在由script/kallsyms工具生成的.tmp_kallsyms2.S文件中,kallsyms_addresses数组存放着所有符号的地址,并且是按照地址升序排列的,所以通过二分查找可以定位到0x80216bf4所在函数的起始地址是下面的这个条目:
kallsyms_addresses:
... ...
PTR _text + 0x1b6be4
... ...
而这一项在kallsyms_addresses中的index为8801,所以现在需要找到kallsyms_names中的第8801个符号。
我们这时实际上可以在kallsyms_names进行查找了,怎么找呢?我们先看一下kallsyms_names大致的样子:
kallsyms_names: .byte0x04, 0x54, 0x7e, 0xc3, 0x74 .byte0x08, 0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79 .byte0x09, 0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65 .byte0x09, 0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c .byte0x09, 0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4 .byte0x07, 0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74 ... ...
其中每一行存储一个压缩后的符号,而index和kallsyms_addresses中的index是一一对应的。每一行的内容分为两部分:第一个byte指明符号的长度,后续才是符号自身。虽然我们这里看到的符号是一行一行分开的,但实际上kallsyms_names是一个unsigned char的数组,所以想要找第8801个符号,只能这样来找:
1. 从第一个字节开始,获得第一个符号的长度len;
2. 向后移len+1个字节,就达到第二个符号的长度字节,这时记录下已经走过的总长度;
3. 重复前两步的动作,直到走过的总长度为8801。
这样找的话,要找到kallsyms_names的第8801个符号就要移动8801次,那如果要寻找最后一个符号,就要移动更多次,时间耗费较多,所以内核通过一个kallsyms_markers数组进行查找。
将kallsyms_names每256个符号分为一组,每一组的第一个字符的位置记录在kallsyms_markers中,这样,我们在找kallsyms_names中的某个条目时,可以快速定义到它位于那个组,然后再在组内寻找,组内移动次数最多为255次。
所以我们先通过(8801 >> 8)得到了要找的符号位于第34组,
我们看到kallsyms_markers的第34项为:
PTR 91280
这个值指明了kallsyms_names中第34组的起始字符的偏移,所以我们直接找到kallsyms_names[91280]位置,即是第34组所有符号的第一个字节。同时我们可以通过(8801 && 0xFF)得到要找的符号在第34组组内的序号为97,即第97个符号。
接下来寻找第97个符号就只能通过上面讲到的方法了。
通过上面一系列的查找,我们定位到第34组中第97个符号如下:
.byte 0x08, 0x05, 0x66, 0xdc, 0xb6, 0xc8, 0x68, 0x6f,0x0b
这个是压缩后的符号,第一个字节0x08是符号长度,所以我们接下来的任务就剩下解压了。
每个字节解压后对应的字符串在kallsyms_token_table中可以找到。于是在kallsyms_token_table表中寻找第5(0x05)项、第5(0x05)项、第102(0x66)项、……、第11(0x0b)项,得到的结果分别为:
"Tn", "f", "_re","gist", "er_", "h", "o", "ok"
由于在压缩的时候将符号类型“T”也压进去了,所以要去掉第一个字符,至此就获得了0x80216bf4地址所在的函数为nf_register_hook。
3. 内核模块的符号
内核模块是在内核启动过程中动态加载到内核中的,所以,不能试图将模块中的符号嵌入到vmlinux中。加载模块时,模块的符号表被存放在该模块的struct module结构中。所有已加载的模块的structmodule结构都放在一个全局链表中。
在查找一个内核模块的符号时,调用的函数依然是kallsyms_lookup(),模块符号的实际查找工作在get_ksymbol()函数中完成。
附录:一个.tmp_kallsyms2.S文件
#include <asm/types.h> #if BITS_PER_LONG == 64 #define PTR .quad #define ALGN .align 8 #else #define PTR .long #define ALGN .align 4 #endif .section.rodata, "a" .globl kallsyms_addresses ALGN kallsyms_addresses: PTR _text + 0x400 PTR _text + 0x400 PTR _text + 0x410 PTR _text + 0x810 PTR _text + 0x9e0 PTR _text + 0xa14 PTR _text + 0xea0 PTR _text + 0xec4 PTR _text + 0xf00 PTR _text + 0xf10 ... ... .globl kallsyms_num_syms ALGN kallsyms_num_syms: PTR 11132 .globl kallsyms_names ALGN kallsyms_names: .byte 0x04,0x54, 0x7e, 0xc3, 0x74 .byte 0x08,0xa0, 0x6b, 0xfa, 0xda, 0xbc, 0xe4, 0xe2, 0x79 .byte 0x09,0xa0, 0x69, 0xd6, 0x93, 0x63, 0x6d, 0x64, 0xa5, 0x65 .byte 0x09,0x54, 0xaa, 0x5f, 0xec, 0xfe, 0xc2, 0x63, 0xe7, 0x6c .byte 0x09,0x1a, 0x0d, 0x5f, 0xe3, 0xb2, 0xd3, 0x75, 0x75, 0xa4 .byte 0x07,0x05, 0x61, 0x6d, 0xfe, 0x04, 0x95, 0x74 .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0x5f, 0x68, 0xe7, 0x74 .byte 0x09,0x74, 0xf6, 0x68, 0x37, 0x39, 0xdc, 0xf1, 0xee, 0x74 .byte 0x0b,0x54, 0xc4, 0x73, 0x79, 0xf1, 0x65, 0xcc, 0x74, 0x79, 0x70, 0x65 ... ... .globl kallsyms_markers ALGN kallsyms_markers: PTR 0 PTR 2831 PTR 5578 PTR 8289 PTR 10855 PTR 13684 PTR 16544 PTR 19519 PTR 22294 PTR 25225 PTR 27761 PTR 30097 ... ... .globl kallsyms_token_table ALGN kallsyms_token_table: .asciz "end" .asciz "Tjffs2" .asciz "map_" .asciz "int" .asciz "to_" .asciz "Tn" .asciz "t__" .asciz "unregist" .asciz "tn" .asciz "yn" .asciz "Tf" ... ... .globl kallsyms_token_index ALGN kallsyms_token_index: .short 0 .short 4 .short 11 .short 16 .short 20 .short 24 .short 27 .short 31 .short 40 ... ...