@[toc]
Linux内核kallsyms符号压缩的完整构建流程
1. 引言 Linux内核的kallsyms
机制是其自省(Introspection)和调试能力的核心基石。它允许内核在运行时将内存地址解析为可读的符号名称,这对于错误追踪(Oops messages)、性能分析和内核调试至关重要。然而,在一个现代内核中,符号数量可达数十万之多,直接以字符串形式存储将消耗数兆字节的宝贵内存。
为了解决这一问题,内核构建系统采用了一套精巧的多趟链接(Multi-pass Linking)和符号压缩(Symbol Compression)流程。此流程由scripts/link-vmlinux.sh
脚本调度,并使用一个专门的宿主工具scripts/kallsyms
来执行核心的压缩算法。本文将完整地、分步骤地剖析这一流程,从构建脚本的宏观调度到压缩工具的微观算法实现。
2. 宏观调度:scripts/link-vmlinux.sh
的多趟链接 整个kallsyms
数据的生成过程并非一次完成,而是通过一个迭代收敛的过程,以确保最终嵌入内核的符号地址是完全准确的。这个过程巧妙地解决了“测量行为本身影响测量结果”的典型问题——即kallsyms
数据段的大小会影响其他符号的地址,而这些地址的变动又会反过来改变kallsyms
数据本身。
流程图:vmlinux链接与kallsyms生成 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 graph TD subgraph "阶段一:编译宿主工具" A["CONFIG_KALLSYMS=y"] --> B("scripts/Makefile"); B --> C{"make hostprogs"}; C --> D["编译 scripts/kallsyms.c"]; D --> E["生成可执行文件 scripts/kallsyms"]; end subgraph "阶段二:链接 vmlinux (由 scripts/link-vmlinux.sh 驱动)" F["make vmlinux"] --> G("scripts/Makefile.vmlinux"); G --> H["执行 scripts/link-vmlinux.sh"]; subgraph "Pass 1: 生成基础镜像" H --> I["链接 .tmp_vmlinux1 (含空的kallsyms节)"]; I -- "nm -n | sed" --> J["<b>.tmp_vmlinux1.syms</b> (干净的符号列表)"]; end subgraph "Pass 2 & 3: 迭代生成并稳定kallsyms数据" J -- "作为输入" --> L["执行 <b>scripts/kallsyms</b> 程序"]; L -- "压缩符号" --> M["<b>.kallsyms.S</b> (包含压缩数据的汇编文件)"]; M -- "CC (汇编器)" --> N["<b>.kallsyms.o</b> (目标文件)"]; N -- "链接进内核" --> O["链接 .tmp_vmlinux2 (包含真实的kallsyms数据)"]; O --> P{"比较两次 .kallsyms.o 的大小是否一致"}; P -- "否 (地址不稳定)" --> J; end subgraph "阶段三:生成最终产品" P -- "是 (地址已收敛)" --> Q["使用最终的 .kallsyms.o"]; Q --> R["链接最终的 vmlinux"]; end end style J fill:#ccf,stroke:#333,stroke-width:2px style L fill:#f9f,stroke:#333,stroke-width:2px style M fill:#cfc,stroke:#333,stroke-width:2px
脚本代码片段与注释 (scripts/link-vmlinux.sh
) 以下是link-vmlinux.sh
中执行此多趟链接的核心逻辑,并附有详细注释。
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 #!/bin/sh if is_enabled CONFIG_KALLSYMS; then true > .tmp_vmlinux0.syms kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms vmlinux_link .tmp_vmlinux1 sysmap_and_kallsyms .tmp_vmlinux1 size1=$(${CONFIG_SHELL} "${srctree} /scripts/file-size.sh" ${kallsymso} ) vmlinux_link .tmp_vmlinux2 sysmap_and_kallsyms .tmp_vmlinux2 size2=$(${CONFIG_SHELL} "${srctree} /scripts/file-size.sh" ${kallsymso} ) if [ $size1 -ne $size2 ] || [ -n "${KALLSYMS_EXTRA_PASS} " ]; then vmlinux_link .tmp_vmlinux3 sysmap_and_kallsyms .tmp_vmlinux3 fi fi vmlinux_link "${VMLINUX} "
3. 压缩引擎:scripts/kallsyms.c
的算法实现 kallsyms
工具的核心是一种**“查表压缩”(Table Lookup Compression)算法。其基本思想是:在所有符号名称中,找出最常出现的 双字符组合(Token)**,用一个未被使用的单字节值来替换它们,从而达到压缩的目的。这个过程是迭代的,直到所有可用的单字节值都被用于替换最高频的Token。
main()
函数:流程主干main
函数清晰地展示了压缩工具的执行步骤。
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 int main (int argc, char **argv) { read_map(argv[optind]); shrink_table(); sort_symbols(); record_relative_base(); optimize_token_table(); write_src(); return 0 ; }
optimize_token_table()
与 optimize_result()
:压缩核心这是算法的核心部分,通过迭代找到并替换最高频的Token。
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 static void optimize_token_table (void ) { build_initial_token_table(); insert_real_symbols_in_table(); optimize_result(); } static void optimize_result (void ) { int i, best; for (i = 255 ; i >= 0 ; i--) { if (!best_table_len[i]) { best = find_best_token(); if (token_profit[best] == 0 ) break ; best_table_len[i] = 2 ; best_table[i][0 ] = best & 0xFF ; best_table[i][1 ] = (best >> 8 ) & 0xFF ; compress_symbols(best_table[i], i); } } }
write_src()
:生成最终的汇编输出在压缩完成后,此函数负责将所有数据结构以汇编格式写入文件,最终被编译链接进内核。
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 static void write_src (void ) { output_label("kallsyms_num_syms" ); printf ("\t.long\t%u\n" , table_cnt); output_label("kallsyms_names" ); for (i = 0 ; i < table_cnt; i++) { } output_label("kallsyms_markers" ); for (i = 0 ; i < markers_cnt; i++) printf ("\t.long\t%u\n" , markers[i]); output_label("kallsyms_token_table" ); for (i = 0 ; i < 256 ; i++) { } output_label("kallsyms_token_index" ); for (i = 0 ; i < 256 ; i++) printf ("\t.short\t%d\n" , best_idx[i]); output_label("kallsyms_offsets" ); for (i = 0 ; i < table_cnt; i++) { offset = table[i]->addr - relative_base; printf ("\t.long\t%#x\t/* %s */\n" , (int )offset, table[i]->sym); } output_label("kallsyms_relative_base" ); printf ("\tPTR\t_text + %#llx\n" , relative_base - _text); }
4. 结论 Linux内核kallsyms
的生成是一个高度工程化的典范。它通过scripts/link-vmlinux.sh
的多趟链接机制 ,解决了符号数据加入后地址偏移的收敛性问题,确保了地址的最终准确性。同时,其核心压缩工具scripts/kallsyms
利用高效的迭代式查表压缩算法 ,将庞大的符号名称表压缩至原来约50%的大小,显著节省了内核的内存占用。这两个组件的协同工作,为Linux内核提供了一个既节省空间又功能强大的符号解析系统。