Linux内核kallsyms符号压缩与解压机制
文章目录
- **Linux内核kallsyms符号压缩与解压机制**
- **1. 引言:为何需要kallsyms?**
- **2. 压缩数据的“蓝图”:核心数据结构**
- **3. 解压核心:`kallsyms_expand_symbol`**
- **完整代码与Doxygen注释**
- **执行流程图**
- **4. 定位压缩数据:`get_symbol_offset`**
- **完整代码与Doxygen注释**
- **执行流程图**
- **5. 地址到符号的桥梁:`get_symbol_pos`**
- **完整代码与Doxygen注释**
- **6. 全流程回顾**
Linux内核kallsyms符号压缩与解压机制
1. 引言:为何需要kallsyms?
在Linux内核的运行过程中,当发生错误(Oops)、进行性能剖析(Profiling)或使用调试器(Debugger)时,系统需要将内存中的函数地址转换为人类可读的符号名称。例如,将地址0xffffffff810a43c0
转换为printk
。这个地址到符号的映射表就是kallsyms
(Kernel All Symbols)。
然而,内核包含数以万计的符号,如果将所有符号名称作为原始字符串直接存储在内核镜像中,会占用数兆字节的宝贵内存。为了解决这个问题,内核在编译时采用了一种高效的**“查表压缩”**方案,将符号名称字符串压缩成紧凑的字节序列。本文将深入剖析这一压缩数据的结构以及内核在运行时如何对其进行解压,还原出原始的符号名称。
2. 压缩数据的“蓝图”:核心数据结构
要理解解压过程,首先必须了解压缩数据的存储格式。kallsyms
的核心由多个紧密相关的数据表构成,它们在内核编译链接后被静态地嵌入到内核镜像中。
kallsyms_offsets
和kallsyms_relative_base
: 这两者共同构成了符号地址表。kallsyms_offsets
是一个32位无符号整数数组,存储了每个符号相对于基地址kallsyms_relative_base
的偏移。通过kallsyms_sym_address(index)
函数(其实现为kallsyms_relative_base + kallsyms_offsets[index]
),我们可以得到一个按地址排序的符号地址列表,这是实现快速地址查找(二分查找)的基础。kallsyms_names
: 核心的压缩符号数据。这是一个巨大的字节数组,所有符号的名称信息经过压缩后都存储在这里。kallsyms_token_table
: “字典表”。这是一个包含数千个常见符号片段(如"irq"
,"lock"
,"__"
,"init"
等)的巨大字符串,每个片段以\0
结尾。kallsyms_token_index
: “字典索引表”。这是一个整数数组,kallsyms_token_index[i]
存储了第i
个片段在kallsyms_token_table
中的起始偏移量。kallsyms_markers
: “标记表”。用于加速在kallsyms_names
中的查找。kallsyms_markers[i]
存储了第i * 256
个符号在kallsyms_names
中的起始偏移量。
它们之间的关系如下图所示:
3. 解压核心:kallsyms_expand_symbol
此函数是整个机制的核心,负责将kallsyms_names
中的一段压缩数据还原成一个完整的符号字符串。
完整代码与Doxygen注释
/*** @brief kallsyms_expand_symbol - 将一段压缩的符号数据解压成字符串。** 此函数根据“查表压缩”算法,将存储在kallsyms_names中的符号数据展开。* 压缩的数据格式为:[长度][Token 1][Token 2]...* 长度本身是变长的,如果最高位为1,则需要两个字节表示。* 每个Token是一个索引,用于在kallsyms_token_table中查找对应的字符串片段。* 所有片段(除了第一个片段的首字符,即符号类型)拼接起来构成最终的符号名。** @param off 待解压符号在全局kallsyms_names数组中的起始偏移量。* @param result 用于存放解压后字符串的输出缓冲区。* @param maxlen 输出缓冲区的最大长度,防止溢出。* @return 下一个符号在kallsyms_names中的起始偏移量。*/
static unsigned int kallsyms_expand_symbol(unsigned int off,char *result, size_t maxlen)
{int len, skipped_first = 0;const char *tptr;const u8 *data;/* 从第一个字节获取压缩后的长度(即Token的数量) */data = &kallsyms_names[off];len = *data;data++;off++;/* 如果长度的最高位(MSB)为1,说明这是一个“大符号”,* 长度由两个字节编码而成(低7位 + 第二个字节左移7位)。*/if ((len & 0x80) != 0) {len = (len & 0x7F) | (*data << 7);data++;off++;}/* 更新偏移量,使其指向下一个符号的起始位置,作为返回值。*/off += len;/* 循环len次,每次处理一个Token。*/while (len) {/** *data 是一个Token索引。* 1. kallsyms_token_index[*data] 找到该Token在字典表中的偏移。* 2. &kallsyms_token_table[...] 获取该Token字符串的指针。*/tptr = &kallsyms_token_table[kallsyms_token_index[*data]];data++;len--;/* 将获取到的Token字符串追加到result缓冲区。*/while (*tptr) {/** 特殊处理:第一个Token的第一个字符是符号类型(如'T', 't'),* 不属于符号名称,必须跳过。*/if (skipped_first) {if (maxlen <= 1)goto tail;*result = *tptr;result++;maxlen--;} elseskipped_first = 1;tptr++;}}tail:if (maxlen)*result = '\0';/* 返回下一个符号的起始偏移量。*/return off;
}
执行流程图
4. 定位压缩数据:get_symbol_offset
当内核需要查找第pos
个符号时,此函数用于在kallsyms_names
中快速定位其压缩数据的起始偏移。
完整代码与Doxygen注释
/*** @brief get_symbol_offset - 根据符号的全局索引,获取其在压缩数据流中的偏移量。** 为了避免从头线性扫描整个kallsyms_names表,该函数使用kallsyms_markers* 进行加速。kallsyms_markers是一个标记数组,每隔256个符号记录一个偏移量。** 查找过程分两步:* 1. 大步跳转:利用 markers 表直接跳转到离目标位置不远的地方。* 2. 短程扫描:从标记位置开始,线性扫描最多255个符号,找到精确位置。** @param pos 要查找的符号的全局索引 (0 to kallsyms_num_syms-1)。* @return 该符号在kallsyms_names中的起始偏移量。*/
static unsigned int get_symbol_offset(unsigned long pos)
{const u8 *name;int i, len;/** 使用最近的标记。标记每256个位置有一个,这已经足够近了。* pos >> 8 相当于 pos / 256,用于在markers数组中找到正确的起点。*/name = &kallsyms_names[kallsyms_markers[pos >> 8]];/** 从标记位置开始,顺序扫描剩余的符号,直到目标位置。* pos & 0xFF 相当于 pos % 256,即需要扫描的符号数量。* 每个符号的格式是 [<len>][<len> bytes of data],我们只需读取长度* 并跳过相应字节即可,无需解压。*/for (i = 0; i < (pos & 0xFF); i++) {len = *name;/** 如果是“大符号”(MSB为1),长度由两个字节构成,* 所以总跳跃长度要额外加1。*/if ((len & 0x80) != 0)len = ((len & 0x7F) | (name[1] << 7)) + 1;name = name + len + 1;}return name - kallsyms_names;
}
执行流程图
5. 地址到符号的桥梁:get_symbol_pos
此函数负责根据一个给定的内存地址,反向查找出它属于哪个符号。
完整代码与Doxygen注释
/*** @brief get_symbol_pos - 根据内存地址查找对应的符号索引。** 此函数在一个按地址排序的符号列表中,查找包含给定地址`addr`的符号。* 它返回该符号的全局索引`pos`。** 核心操作是二分查找,作用于通过kallsyms_sym_address()动态计算出的* 地址列表上。这非常高效。** @param addr 要查找的内存地址。* @param symbolsize (输出) 用于存储找到的符号的大小。* @param offset (输出) 用于存储`addr`相对于符号起始地址的偏移量。* @return 找到的符号的全局索引`pos`。*/
static unsigned long get_symbol_pos(unsigned long addr,unsigned long *symbolsize,unsigned long *offset)
{unsigned long symbol_start = 0, symbol_end = 0;unsigned long i, low, high, mid;/* 在kallsyms_offsets数组上进行二分查找。*/low = 0;high = kallsyms_num_syms;while (high - low > 1) {mid = low + (high - low) / 2;if (kallsyms_sym_address(mid) <= addr)low = mid;elsehigh = mid;}/** low现在是最后一个地址 <= addr 的符号索引。* 但可能存在多个符号地址相同(别名),我们需要找到第一个。*/while (low && kallsyms_sym_address(low-1) == kallsyms_sym_address(low))--low;symbol_start = kallsyms_sym_address(low);/* 查找下一个不同地址的符号,以确定当前符号的大小。*/for (i = low + 1; i < kallsyms_num_syms; i++) {if (kallsyms_sym_address(i) > symbol_start) {symbol_end = kallsyms_sym_address(i);break;}}/* 如果没找到下一个符号,使用内核代码段的末尾地址。*/if (!symbol_end) {if (is_kernel_inittext(addr))symbol_end = (unsigned long)_einittext;else if (IS_ENABLED(CONFIG_KALLSYMS_ALL))symbol_end = (unsigned long)_end;elsesymbol_end = (unsigned long)_etext;}if (symbolsize)*symbolsize = symbol_end - symbol_start;if (offset)*offset = addr - symbol_start;return low;
}
6. 全流程回顾
当内核需要为一个地址(addr
)查找符号名时,整个过程被完美地串联起来:
- 结论
Linux内核的kallsyms机制是一个精巧的空间换时间设计典范。它通过基于字典的查表压缩算法,极大地减小了符号表在内核镜像中的体积。同时,借助markers等辅助索引结构,它又保证了在需要反向查找符号时,能够以可接受的性能开销(二分查找 + 大步跳转 + 短程扫描)高效地完成解压任务,为内核的调试和可观测性提供了坚实的基础。