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

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)。

然而,内核包含数以万计的符号,如果将所有符号名称作为原始字符串直接存储在内核镜像中,会占用数兆字节的宝贵内存。为了解决这个问题,内核在编译时采用了一种高效的**“查表压缩”**方案,将符号名称字符串压缩成紧凑的字节序列。本文将深入剖析这一压缩数据的结构以及内核在运行时如何对其进行解压,还原出原始的符号名称。

内核运行时
内核编译时
kallsyms子系统
内存地址, e.g., 0xffffffff810a43c0
符号解压
符号名称, e.g., 'printk'
scripts/kallsyms
所有符号名称
符号压缩
生成压缩数据表
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--;} elseskipped_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;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)查找符号名时,整个过程被完美地串联起来:

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

相关文章:

  • 米思米网站订单取消怎么做基金会网站模板
  • 公司网站源码做智能家居网站需要的参考文献
  • 11. Pandas 数据分类与区间分组(cut 与 qcut)
  • 找家里做的工作到什么网站淄博五厘网络技术有限公司
  • 国外哪些网站做产品推广比较好四川建设人员信息查询
  • 第二章:软件需求
  • AI Agent赋能产品经理:从需求分析到用户增长的全流程实践
  • 网站服务公司案例广州网站建设优化公司
  • AI学习日记——神经网络参数的更新
  • Java进阶教程,全面剖析Java多线程编程,多线程和堆内存栈内存的关系,笔记20
  • 建设春风摩托车官方网站百度站长论坛
  • 长春企业网站建设公司建设银行广州招聘网站
  • 网站 开发 周期定制app开发软件
  • 怎么做网站 ppt货代网站制作
  • 2025-10-06 Python不基础12——class原理
  • 龙泉驿建设局网站谷歌seo是什么职业
  • 从东方仙盟筑基期看 JavaScript 动态生成图片技术-东方仙盟
  • 怎么做电脑网站后台谷歌seo推广服务
  • 【笔记】2.1.1.1 电化学定义与组件特征
  • ISO 11452系列子标准介绍 道路车辆窄带辐射电磁能电干扰的部件试验方
  • 南宁网站制作工具山东建设厅执业资格注册中心网站
  • WebStorm对个人免费开放
  • 免费1级做爰片在线观看网站wordpress QQ登录注册
  • Git仓库Python文件Pylint静态分析
  • 青岛网站建设网站设计游乐网站设计
  • 盘锦网站开发推荐几个做网站比较好的公司
  • 10.6 作业
  • Dnsmasq 详细介绍与应用指南
  • MinIO 控制台功能减少使用mc操作
  • 【多线程-进阶】常⻅的锁策略