当前位置: 首页 > news >正文

实用指南:Linux内核kallsyms符号压缩与解压机制

实用指南: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_offsetskallsyms_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中的起始偏移量。

它们之间的关系如下图所示:

字典表 (kallsyms_token_table)
字典索引 (kallsyms_token_index)
压缩符号数据 (kallsyms_names)
使用索引T1
使用索引T2
得到偏移offset1
得到偏移offset2
... \0 token_x \0 ...
... \0 token_1 \0 ...
... \0 token_2 \0 ...
...
...
index[T1] = offset1
index[T2] = offset2
...
符号A: [len, T1, T2, T3, ...]
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--;
} else
skipped_first = 1;
tptr++;
}
}
tail:
if (maxlen)
*result = '\0';
/* 返回下一个符号的起始偏移量。*/
return off;
}
执行流程图
是 (大符号)
否 (普通符号)
循环中
开始: kallsyms_expand_symbol(off, ...)
读取第一个字节: 压缩长度(len)
len的最高位(MSB)是否为1?
读取第二个字节, 计算真实长度: len = (len & 0x7F) | (byte2 << 7)
len即为压缩长度
进入解压循环
循环 len 次
从数据流中读取一个字节(token_index)
在 kallsyms_token_index 中查找: offset = kallsyms_token_index[token_index]
在 kallsyms_token_table 中定位: token_ptr = &kallsyms_token_table[offset]
是否为第一个Token的第一个字符?
跳过 (该字符为符号类型)
将 token_ptr 指向的字符串追加到 result 缓冲区
循环结束?
在 result 缓冲区末尾添加 '\0'
返回下一个符号的偏移量
结束
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;}
执行流程图
查找第600个符号 (pos=600)
kallsyms_names (巨大的字节数组)
符号0..255
符号256..511
...
计算marker索引: 600 / 256 = 2
开始: get_symbol_offset(600)
跳转到 kallsyms_names[kallsyms_markers[2]]
计算剩余扫描次数: 600 % 256 = 88
从Marker[2]位置开始, 向后线性扫描88个符号
扫描时仅读取长度并跳跃, 不解压
最终指针位置即为第600个符号的偏移
Marker[0]指向的位置
Start
Marker[1]指向的位置
...
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;
else
high = 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;
else
symbol_end = (unsigned long)_etext;
}
if (symbolsize)
*symbolsize = symbol_end - symbol_start;
if (offset)
*offset = addr - symbol_start;
return low;
}
6. 全流程回顾

当内核需要为一个地址(addr)查找符号名时,整个过程被完美地串联起来:

在符号地址列表上进行二分查找
使用 markers 跳转 + 短程扫描
查字典表并拼接
输入: 内存地址 addr
get_symbol_pos
输出: 符号索引 pos
get_symbol_offset
输出: 压缩数据偏移 off
kallsyms_expand_symbol
输出: 原始符号字符串
  1. 结论
    Linux内核的kallsyms机制是一个精巧的空间换时间设计典范。它通过基于字典的查表压缩算法,极大地减小了符号表在内核镜像中的体积。同时,借助markers等辅助索引结构,它又保证了在需要反向查找符号时,能够以可接受的性能开销(二分查找 + 大步跳转 + 短程扫描)高效地完成解压任务,为内核的调试和可观测性提供了坚实的基础。
http://www.hskmm.com/?act=detail&tid=36594

相关文章:

  • 5G企业应用的七大场景与商业机遇
  • 2025 水泥墩源头厂家最新推荐排行榜:光伏 / 围挡 / 交通 / 防撞水泥墩多品类优选,实力品牌权威榜单
  • 高效数据结构 - 循环队列
  • 2025 年国内活塞杆厂家最新推荐排行榜:聚焦精密 / 不锈钢 / 油缸 / 气缸 / 45# 镀铬类产品,助力企业精准挑选可靠合作方
  • Day16
  • 数据类型,二元运算符,自动类型提升规则,关系运算,取余模运算
  • 股票技术面分析平台QuantMatrix深度解析 - 实践
  • 迷宫问题
  • WPF使用MediaCapture开发相机应用(四、相机录视频)
  • 链队
  • Gitee本土化战略深度解析:中国开发者生态的合规与效率革命
  • 2025年10月上海装修公司口碑榜:十强对比评测
  • 02-GPIO-铁头山羊STM32标准库新版笔记
  • 【多校支持、EI检索】第六届大数据与社会科学国际学术会议(ICBDSS 2025)
  • IDC iPaaS市场报告解读:独立厂商与云巨头的“双轨竞速”
  • 2025年10月仓储管理系统推荐:鸿链云仓领衔五大方案对比评测榜
  • 2025年10月电动叉车销售公司排行榜:五家主流服务商对比评测
  • 2025年口罩机厂家权威推荐榜:全自动口罩机器,全自动KN95口罩机源头企业综合评测与采购指南
  • 2025年包装机厂家权威推荐榜单:全自动包装机/包装生产线/非标定制机器与生产线专业选购指南
  • Timing Signoff 技术精要
  • Oracle故障处理:10G RAC srvctl注册实例正常,但是crs切不能管理实例
  • 杂题选做-2
  • 读书笔记:白话解读Oracle范围分区
  • 2025年10月人形机器人场景落地商评测榜:赛飞特工程技术集团数据透视
  • 科林电气与利驰软件续签合作,共启数字化协同新篇章!
  • 详细介绍:资产信息收集与指纹识别:HTTPX联动工具实战指南
  • 易基因:剑桥大学团队利用微量WGBS等揭示DNMT3L在胎盘发育中的DNA甲基化调控机制:CSC(IF20.5)
  • 10.22
  • 和橘子学AI创作【500集120实战】
  • iOS 26 性能调试工具全景指南 多工具组合 + 实战流程