内核符号表的生成和查找过程【转】

转自: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_tablekallsyms_token_index,其中:

 

  • kallsyms_addresses:一个数组,存放所有符号的地址列表,按地址升序排列。
  • kallsyms_num_syms:符号的数量。
  • kallsyms_names:一个数组,存放所有符号的名称,和kallsyms_addresses一一对应。

 

其他三个全局变量的含义后续会提到。

这些变量被嵌入在vmlinux中,所以在内核代码中直接extern就可以使用。例如dump_stack()就是通过这些变量来查找一个地址对应的函数名的。

那由scripts/kallsyms生成的汇编文件是如何嵌入到vmlinux中的呢。在编译内核的后期主要进行了一下几步额外的编译和链接过程:

 

  1.   链接器ld将内核的绝大部分组件链接成临时内核映像.tmp_vmlinux1。
  2.   使用nm命令将.tmp_vmlinux1中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms1.S的文件。
  3.   对.tmp_kallsyms1.S文件进行编译生成.tmp_kallsyms1.o文件。
  4.   重复1的链接过程,这次将步骤3得到的.tmp_kallsyms1.o文件链接进入内核得到临时内核映像.tmp_vmlinux2文件,其中包含的部分函数和非栈变量的地址发生了变化,但但由于.tmp_kallsyms1.S中的符号表还是旧的,所以.tmp_vmlinux2还不能作为最终的内核映像。
  5.   再使用nm命令将.tmp_vmlinux2中符号和相对的地址导出来,并使用kallsyms工具生成tmp_kallsyms2.S的文件。
  6.   对.tmp_kallsyms2.S文件进行编译生成.tmp_kallsyms2.o文件。
  7.   .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
   ... ...

 

 

 

时间: 2024-10-28 02:01:36

内核符号表的生成和查找过程【转】的相关文章

linux内核符号表kallsyms简介

在使用perf排查问题时,我们经常会发现[kernel.kallsyms]这个模块.这到底是个什么东西呢? 简介: 在2.6版的内核中,为了更方便的调试内核代码,开发者考虑将内核代码中所有函数以及所有非栈变量的地址抽取出来,形成是一个简单的数据块(data blob:符号和地址对应),并将此链接进 vmlinux 中去. 在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试.完成这一地址抽取+数据快组织封装功能的相关子系统就称之为 kallsyms. 反之,如

浅谈算法和数据结构 六 符号表及其基本实现

前面几篇文章介绍了基本的排序算法,排序通常是查找的前奏操作.从本文开始介绍基本的查找算法. 在介绍查找算法,首先需要了解符号表这一抽象数据结构,本文首先介绍了什么是符号表,以及这一抽象数据结构的的API,然后介绍了两种简单的符号表的实现方式. 一符号表 在开始介绍查找算法之前,我们需要定义一个名为符号表(Symbol Table)的抽象数据结构,该数据结构类似我们再C#中使用的Dictionary,他是对具有键值对元素的一种抽象,每一个元素都有一个key和value,我们可以往里面添加key,v

自己动手构造编译系统:编译、汇编与链接2.4.2 表信息生成

2.4.2  表信息生成    汇编器的符号表除了记录符号的信息之外,还需要记录段相关的信息以及重定位符号的信息,这些信息都是生成可重定位目标文件所必需的. 对于段表的信息,可以在汇编器识别section语法模块时进行处理.比如声明代码段的汇编代码及段表信息生成(见图2-14). section .text   图2-14  段表信息生成 汇编器的语法分析器只要计算两次section声明之间的地址差,便能获得段的长度,从而将段的名称.偏移.大小记录到段表项内.如果规定段按照4字节对齐,则需要对段

VxWorks 符号表

符号表初始化          符号表用于建立符号名称.类型和值之间的关系.其中,名称为null结尾的任意字符串:类型为标识各种符号的整数:值为一个字符指针.符号表主要用来作为目标模块加载的基础,但在需要名称和值关联的任何时候都看使用.           运行系统中一般存在两个符号表结构sysSymTbl和statSymTbl.sysSymTbl为目标机的系统符号表,通过程序或tShell动态加载的目标模块的符号模块的符号都添加到该符号表中,sysSymTbl和statSymTbl两个标识本身

手把手教你写脚本引擎(五)——简单的高级语言(3,符号表)

符号表的结构的复杂度跟语言的语义规则的复杂度有关.对于C#来说,每一个符号都附带了一大堆信息,譬如位置啦,所在的namespace啦,类型啦什么的.对于JavaScript来说,符号表几乎是不需要的,因为东西都动态了,编译时几乎不检查内容.语义分析的输出是符号表,代码生成的输入是符号表和语法树.因此语法树除了放语法相关的内容,语义相关的内容最好放到符号表里面(譬如说表达式的类型啦,语句的scope结果啦).关于一个现实中的符号表组织可以看CMinus的语义分析结果. 首先我们要解决类型的表达问题

php内核探索之zend_execute的具体执行过程

解释器引擎最终执行op的函数是zend_execute,实际上zend_execute是一个函数指针,在引擎初始化的时候zend_execute默认指向了execute,这个execute定义在{PHPSRC}/Zend/zend_vm_execute.h: ZEND_API void execute(zend_op_array *op_array TSRMLS_DC) { zend_execute_data *execute_data; zend_bool nested = 0; zend_b

直接从SQL语句问题贴子数据建表并生成建表语句的存储过程

存储过程|数据|问题|语句 下面的存储过程,可帮你在回答SQL语句问题时,直接从贴子的样本数据建表并生成建表语句,省去大量的手工输入数据的工作. /*Create Table from your web page data* 2004-JAN-1, OpenVMS,V0.1* 2004-JAN-2, V0.5, add tab & blank values logical * 2004-JAN-3, V1.0, add SQL Statement generation * 2004-JAN-4,

可配置语法分析器开发纪事(二) 构造符号表

上一篇博客讲到了构造语法树的问题.有朋友在留言问我,为什么一定要让语法分析器产生语法树,而不是让用户自己决定要怎么办呢?在这里我先解答这个问题. 1.大部分情况下都是真的需要有语法树 2.如果要直接返回计算结果之类的事情的话,只需要写一个visitor运行一下语法树就好了,除去自动生成的代码以外(反正这不用人写,不计入代价),代码量基本上没什么区别 3.加入语法树可以让文法本身描述起来更简单,如果要让程序员把文法单独放在一边,然后自己写完整的语义函数来让他生成语法树的话,会让大部分情况(需要语法

iOS符号表恢复&amp;逆向支付宝

推荐序 本文介绍了恢复符号表的技巧,并且利用该技巧实现了在 Xcode 中对目标程序下符号断点调试,该技巧可以显著地减少逆向分析时间.在文章的最后,作者以支付宝为例,展示出通过在 UIAlertView 的 show 方法处下断点,从而获得支付宝的调用栈的过程. 本文涉及的代码也开源在:https://github.com/tobefuturer/restore-symbol,欢迎 Star 和提 Issue.感谢作者授权发表. 作者介绍:杨君,中山大学计算机系研究生,iOS 开发者,擅长领域