@[toc]

Linux内核kallsyms符号压缩的完整构建流程

在这里插入图片描述

1. 引言

Linux内核的kallsyms机制是其自省(Introspection)和调试能力的核心基石。它允许内核在运行时将内存地址解析为可读的符号名称,这对于错误追踪(Oops messages)、性能分析和内核调试至关重要。然而,在一个现代内核中,符号数量可达数十万之多,直接以字符串形式存储将消耗数兆字节的宝贵内存。

为了解决这一问题,内核构建系统采用了一套精巧的多趟链接(Multi-pass Linking)和符号压缩(Symbol Compression)流程。此流程由scripts/link-vmlinux.sh脚本调度,并使用一个专门的宿主工具scripts/kallsyms来执行核心的压缩算法。本文将完整地、分步骤地剖析这一流程,从构建脚本的宏观调度到压缩工具的微观算法实现。

整个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

以下是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
# ... (脚本头部)

# ... (vmlinux_link, mksysmap, kallsyms等辅助函数定义) ...

if is_enabled CONFIG_KALLSYMS; then

# kallsyms支持
# 生成一个包含所有符号的节,并将其添加到vmlinux中。
# 这是一个多步骤的过程:
# 0) 首先,生成一个空的kallsyms符号列表,用于第一次链接。
# 这确保了链接器知道__kallsyms节的存在。
true > .tmp_vmlinux0.syms
kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms

# 1) 链接 .tmp_vmlinux1。这个版本包含了所有真实的符号和节,
# 但其__kallsyms节是基于一个空列表生成的,因此几乎为空。
# 对.tmp_vmlinux1运行kallsyms,会得到一个大小基本正确的
# .tmp_vmlinux1.kallsyms.o。
vmlinux_link .tmp_vmlinux1
sysmap_and_kallsyms .tmp_vmlinux1
size1=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" ${kallsymso})

# 2) 链接 .tmp_vmlinux2。这一次,我们将上一步生成的、大小基本正确的
# .kallsyms.o链接进去。由于这个.o文件有实际大小,它的加入
# 会导致其他符号的地址发生偏移。
# 我们再基于.tmp_vmlinux2生成一个新的、更精确的.kallsyms.o。
vmlinux_link .tmp_vmlinux2
sysmap_and_kallsyms .tmp_vmlinux2
size2=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" ${kallsymso})

# 3) 检查收敛性。如果第二次生成的.kallsyms.o大小与第一次不同,
# 说明地址仍在变动(例如,链接器可能因为地址变化而插入了新的跳转桩,
# 引入了新的符号)。此时需要再进行一轮链接以确保地址稳定收敛。
# KALLSYMS_EXTRA_PASS=1环境变量可用于强制执行额外一轮。
if [ $size1 -ne $size2 ] || [ -n "${KALLSYMS_EXTRA_PASS}" ]; then
vmlinux_link .tmp_vmlinux3
sysmap_and_kallsyms .tmp_vmlinux3
fi
fi

# ... (最终链接)
# 使用最后一轮生成的、最准确的${kallsymso}来链接最终的vmlinux。
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
/**
* @brief kallsyms工具的主函数。
* @param argc 参数数量。
* @param argv 参数数组。
* @return 成功返回0,失败返回非0。
*/
int main(int argc, char **argv)
{
// ... (解析命令行参数,如 --all-symbols)

// 1. 读取并解析输入的.map或.syms文件,构建一个包含所有符号的内存表。
read_map(argv[optind]);

// 2. 根据--all-symbols选项,过滤掉不需要的符号(如调试符号、非文本段符号等)。
shrink_table();

// 3. 按地址对符号表进行排序,这是生成kallsyms_offsets的基础。
sort_symbols();

// 4. 记录所有符号中的最低地址,作为计算相对偏移的基准。
record_relative_base();

// 5. 执行核心的查表压缩算法,优化Token表并压缩所有符号名称。
optimize_token_table();

// 6. 将所有处理和压缩后的数据(地址偏移、压缩名称、Token表等)
// 以汇编语言(.S)的格式输出到标准输出。
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
/**
* @brief 优化Token表的主函数,协调整个压缩过程。
*/
static void optimize_token_table(void)
{
// 1. 遍历所有符号,统计所有双字符Token的出现频率。
build_initial_token_table();

// 2. 遍历所有符号,将所有实际出现过的单字符标记为“已使用”,
// 并放入Token表的对应位置。
insert_real_symbols_in_table();

// 3. 执行迭代优化,找出最高频的Token并用未使用的单字节值替换它们。
optimize_result();
}

/**
* @brief 迭代优化过程,是压缩算法的核心循环。
*/
static void optimize_result(void)
{
int i, best;

/* 从255到0循环,尝试填满所有可用的单字节编码空间。*/
for (i = 255; i >= 0; i--) {

/* 如果当前字节值i是空闲的(即没有任何单字符使用它)*/
if (!best_table_len[i]) {

/* 1. 在所有Token中,找到当前“利润”最高(出现频率最高)的一个。*/
best = find_best_token();
if (token_profit[best] == 0)
break; /* 如果利润为0,说明没有可压缩的了。*/

/* 2. 将这个最赚钱的Token(一个双字节值)存入当前空闲的best_table[i]中。*/
best_table_len[i] = 2;
best_table[i][0] = best & 0xFF;
best_table[i][1] = (best >> 8) & 0xFF;

/* 3. 遍历所有符号,将所有出现的该Token替换为单字节i。
* 此函数还会动态更新其他Token的利润值。
*/
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
/**
* @brief 将所有压缩后的kallsyms数据写入汇编源文件。
*/
static void write_src(void)
{
// ... (输出汇编文件头) ...

// 输出符号总数
output_label("kallsyms_num_syms");
printf("\t.long\t%u\n", table_cnt);

// 输出压缩后的符号名称流 (kallsyms_names)
// 格式为:[长度][压缩数据]...
output_label("kallsyms_names");
for (i = 0; i < table_cnt; i++) {
// ... (输出变长编码的长度) ...
// ... (输出压缩后的字节序列) ...
}

// 输出加速查找的标记表 (kallsyms_markers)
output_label("kallsyms_markers");
for (i = 0; i < markers_cnt; i++)
printf("\t.long\t%u\n", markers[i]);

// 输出Token字典表 (kallsyms_token_table)
// 这是解压时用来查找替换片段的表。
output_label("kallsyms_token_table");
for (i = 0; i < 256; i++) {
// ... (解压并输出每个Token代表的原始字符串) ...
}

// 输出Token索引表 (kallsyms_token_index)
// 这是一个short数组,快速定位Token在字典表中的偏移。
output_label("kallsyms_token_index");
for (i = 0; i < 256; i++)
printf("\t.short\t%d\n", best_idx[i]);

// 输出符号地址偏移表 (kallsyms_offsets)
// 存储每个符号相对于relative_base的32位偏移。
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);
}

// 输出相对基地址 (kallsyms_relative_base)
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内核提供了一个既节省空间又功能强大的符号解析系统。