@[toc]
Linux内核kallsyms符号压缩与解压机制
1. 引言:为何需要kallsyms? 在Linux内核的运行过程中,当发生错误(Oops)、进行性能剖析(Profiling)或使用调试器(Debugger)时,系统需要将内存中的函数地址转换为人类可读的符号名称。例如,将地址0xffffffff810a43c0
转换为printk
。这个地址到符号的映射表就是kallsyms
(Kernel All Symbols)。
然而,内核包含数以万计的符号,如果将所有符号名称作为原始字符串直接存储在内核镜像中,会占用数兆字节的宝贵内存。为了解决这个问题,内核在编译时采用了一种高效的**“查表压缩”**方案,将符号名称字符串压缩成紧凑的字节序列。本文将深入剖析这一压缩数据的结构以及内核在运行时如何对其进行解压,还原出原始的符号名称。
1 2 3 4 5 6 7 8 9 10 11 12 graph TD subgraph "内核编译时" A["所有符号名称"] --> B("scripts/kallsyms"); B --> C{"符号压缩"}; C --> D["生成压缩数据表"]; end subgraph "内核运行时" E["内存地址, e.g., 0xffffffff810a43c0"] --> F("kallsyms子系统"); F --> G{"符号解压"}; G --> H["符号名称, e.g., 'printk'"]; end D --> F;
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
中的起始偏移量。
它们之间的关系如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 graph TD subgraph "压缩符号数据 (kallsyms_names)" A("符号A: [len, T1, T2, T3, ...]"); end subgraph "字典索引 (kallsyms_token_index)" B["..."]; C["index[T1] = offset1"]; D["index[T2] = offset2"]; E["..."]; end subgraph "字典表 (kallsyms_token_table)" F["... \0 token_x \0 ..."]; G["... \0 token_1 \0 ..."]; H["... \0 token_2 \0 ..."]; I["..."]; end A -- "使用索引T1" --> C; A -- "使用索引T2" --> D; C -- "得到偏移offset1" --> G; D -- "得到偏移offset2" --> H; style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#ccf,stroke:#333,stroke-width:2px style G fill:#cfc,stroke:#333,stroke-width:2px
3. 解压核心:kallsyms_expand_symbol
此函数是整个机制的核心,负责将kallsyms_names
中的一段压缩数据还原成一个完整的符号字符串。
完整代码与Doxygen注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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; data = &kallsyms_names[off]; len = *data; data++; off++; if ((len & 0x80 ) != 0 ) { len = (len & 0x7F ) | (*data << 7 ); data++; off++; } off += len; while (len) { tptr = &kallsyms_token_table[kallsyms_token_index[*data]]; data++; len--; while (*tptr) { if (skipped_first) { if (maxlen <= 1 ) goto tail; *result = *tptr; result++; maxlen--; } else skipped_first = 1 ; tptr++; } } tail: if (maxlen) *result = '\0' ; return off; }
执行流程图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 graph TD S["开始: kallsyms_expand_symbol(off, ...)"] --> A{"读取第一个字节: 压缩长度(len)"}; A --> B{"len的最高位(MSB)是否为1?"}; B -- "是 (大符号)" --> C["读取第二个字节, 计算真实长度: len = (len & 0x7F) | (byte2 << 7)"]; B -- "否 (普通符号)" --> D["len即为压缩长度"]; C --> E["进入解压循环"]; D --> E; E --> F{"循环 len 次"}; F -- "循环中" --> G["从数据流中读取一个字节(token_index)"]; G --> H["在 kallsyms_token_index 中查找: offset = kallsyms_token_index[token_index]"]; H --> I["在 kallsyms_token_table 中定位: token_ptr = &kallsyms_token_table[offset]"]; I --> J{"是否为第一个Token的第一个字符?"}; J -- "是" --> K["跳过 (该字符为符号类型)"]; J -- "否" --> L["将 token_ptr 指向的字符串追加到 result 缓冲区"]; K --> M{"循环结束?"}; L --> M; M -- "否" --> F; M -- "是" --> N["在 result 缓冲区末尾添加 '\0'"]; N --> O["返回下一个符号的偏移量"]; O --> E_END["结束"];
4. 定位压缩数据:get_symbol_offset
当内核需要查找第pos
个符号时,此函数用于在kallsyms_names
中快速定位其压缩数据的起始偏移。
完整代码与Doxygen注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static unsigned int get_symbol_offset (unsigned long pos) { const u8 *name; int i, len; name = &kallsyms_names[kallsyms_markers[pos >> 8 ]]; for (i = 0 ; i < (pos & 0xFF ); i++) { len = *name; if ((len & 0x80 ) != 0 ) len = ((len & 0x7F ) | (name[1 ] << 7 )) + 1 ; name = name + len + 1 ; } return name - kallsyms_names; }
执行流程图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 graph TD subgraph "kallsyms_names (巨大的字节数组)" direction LR S(Start) -- "符号0..255" --> M1("Marker[0]指向的位置"); M1 -- "符号256..511" --> M2("Marker[1]指向的位置"); M2 -- "..." --> END(...); end subgraph "查找第600个符号 (pos=600)" A["开始: get_symbol_offset(600)"] --> B{"计算marker索引: 600 / 256 = 2"}; B --> C["跳转到 kallsyms_names[kallsyms_markers[2]]"]; C --> D{"计算剩余扫描次数: 600 % 256 = 88"}; D --> E["从Marker[2]位置开始, 向后线性扫描88个符号"]; F["扫描时仅读取长度并跳跃, 不解压"]; E --> F; F --> G["最终指针位置即为第600个符号的偏移"]; end style M1 fill:#add,stroke:#333,stroke-width:2px style M2 fill:#add,stroke:#333,stroke-width:2px
5. 地址到符号的桥梁:get_symbol_pos
此函数负责根据一个给定的内存地址,反向查找出它属于哪个符号。
完整代码与Doxygen注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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; 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; } 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
)查找符号名时,整个过程被完美地串联起来:
1 2 3 4 5 6 7 graph TD A["输入: 内存地址 addr"] --> B("get_symbol_pos"); B -- "在符号地址列表上进行二分查找" --> C["输出: 符号索引 pos"]; C --> D("get_symbol_offset"); D -- "使用 markers 跳转 + 短程扫描" --> E["输出: 压缩数据偏移 off"]; E --> F("kallsyms_expand_symbol"); F -- "查字典表并拼接" --> G["输出: 原始符号字符串"];
结论 Linux内核的kallsyms机制是一个精巧的空间换时间设计典范。它通过基于字典的查表压缩算法,极大地减小了符号表在内核镜像中的体积。同时,借助markers等辅助索引结构,它又保证了在需要反向查找符号时,能够以可接受的性能开销(二分查找 + 大步跳转 + 短程扫描)高效地完成解压任务,为内核的调试和可观测性提供了坚实的基础。