Linux内核几个重要文件之System.map

What

什么是System.map文件?

System.map是编译内核时生成,它记录了文件内核中的符号列表,以及符号在内存中的虚拟地址,这里的符号可以理解成函数名和变量。System.map文件不是一成不变的,每次编译内核都会重新生成System.map文件。

下面我们简单看看System.map文件里面的内容

System.map文件内容

下面列出了我最近编译的6.8内核的System.map(前20行):

00000000009f5a00 A __pecoff_data_raw_size
0000000000a78000 A __pecoff_data_virt_size
ffffffff80000000 T _start
ffffffff80000040 t pe_head_start
ffffffff80000044 t coff_header
ffffffff80000058 t optional_header
ffffffff80000070 t extra_header_fields
ffffffff800000f8 t section_table
ffffffff80001000 t efi_header_end
ffffffff80001000 T relocate_enable_mmu
ffffffff80001066 T secondary_start_sbi
ffffffff800010d8 T _start_kernel
ffffffff80002000 T __traceiter_initcall_level
ffffffff80002000 T _stext
ffffffff80002000 T _text
ffffffff8000203c T __probestub_initcall_level
ffffffff80002050 T __traceiter_initcall_start
ffffffff8000208c T __probestub_initcall_start
ffffffff800020a0 T __traceiter_initcall_finish
ffffffff800020e4 T __probestub_initcall_finish
... ...

System.map文件格式:地址 + 符号类型 + 符号名

符号类型说明: 大写为全局符号,小写为局部符号

  • A: 该符号的值是不能改变的,等于const
  • B: 该符号来自于未初始化代码段bss段
  • C: 该符号是通用的,通用的符号指未初始化的数据。当链接时,多个通用符号可能对应一个名称,如果该符号在某一个位置定义,则这个通用符号被当做未定义的引用
  • D: 该符号位于初始化的数据段
  • G: 位于初始化数据段,专门对应小的数据对象,比如global int x,对应的大数据对象为 数组类型等
  • I: 到其他符号的间接引用,是对于a.out文件的GNU扩展,使用非常少
  • N: 调试符号
  • R: 位于只读代码段的符号
  • S: BSS段(未初始化数据段)的小对象符号
  • T: 代码段符号,全局函数,t为局部函数
  • U: 未定义的符号
  • V: 该符号是一个 weak object(弱符号),当其连接到为定义的对象上,该符号的值变为0
  • W: 类似于V
  • -: 该符号是a.out文件中的一个stabs symbol,获取调试信息
  • ?: 未知类型的符号

Why

为什么会有System.map文件? 它有啥作用?

对计算机而言是没有符号这个概念,只有0和1,只有内存地址;但是我们比较容易理解的是函数名这样的符号,System.map文件就是计算机和人类在理解程序中的桥梁,相当于是程序地址与变量名或者函数名的映射,就像翻译词典一样。

当程序报错的时候,计算机会在堆栈信息里保存出错的内存地址,但是我们光看内存地址是没法理解程序到底是哪里出错。于是可以把出错的内存地址通过System.map文件转换成函数名,这样我们就知道是哪个函数出错了。

所以当我们用gdb调试程序的时候,可以通过函数名设置断点,也是因为在程序中有一份符号表,如果用strip后的程序做gdb调试,再用函数名设置断点的时候会提示找不到函数名,因为程序里的符号信息都被删除了。

How

Linux内核是怎么生成System.map文件的呢?

System.map文件是通过调用scripts/mksysmap脚本生成:

mksysmap vmlinux System.map [exclude]

手动执行如上命令生成的System.map和编译内核时生成的是一样的

mksysmap脚本里主要内容就是通过调用nm命令生成:

${NM} -n ${1} | sed >${2} -e "
$(if [ $# -ge 3 ]; then ${NM} ${3} | sed -n '/ U /!s:.* \([^ ]*\)$:/ \1$/d:p'; fi)
"

nm命令: list symbols from object files

扩展

对于应用程序的启发

对于应用程序,其实也可以参考内核的操作,先通过链接生成带符号的可执行文件,生成符号文件(类似System.map),再通过strip去除可执行文件中的调试符号。这样既减小了可执行程序的大小,又可以回溯到代码、函数的运行地址。

/proc/kallsyms文件

/proc/kallsyms文件是在内核启动后生成的,位于文件系统的/proc目录下,实现代码见kernel/kallsyms.c。前提是内核必须打开CONFIG_KALLSYMS编译选项。
注意:它和System.map的区别在于它同时包含了内核模块的符号列表。此外内核启动后的/proc/kallsyms文件中的符号表只是给用户态开放了一个可以操作的接口,非人为特意操作,内核一般不会使用它们。

内核符号表

内核在执行过程中,可能需要获得一个地址所在的函数名。比如发生Oops的时,使用dump_stack()函数打印栈回溯信息等等。

但是内核在查找一个地址对应的函数名时,并没有使用System.map/proc/kallsyms,而是在编译内核时,向vmlinux嵌入了一个符号表,这样做可能是为了方便快速的查找且避免文件操作带来的不良影响。

内核符号表详细的介绍可看参考后面的文章

参考