[toc]
在这里插入图片描述

scripts/Makefile.vmlinux

输出文件

  • .tmp_vmlinux2.kallsyms.S
  • .tmp_vmlinux2.kallsyms.o
  • .tmp_vmlinux2.syms
1
2
3
-rw-rw-r--   1 embedsky embedsky 285K Oct  5 10:38 .tmp_vmlinux2.kallsyms.o
-rw-rw-r-- 1 embedsky embedsky 2.9M Oct 5 10:38 .tmp_vmlinux2.kallsyms.S
-rw-rw-r-- 1 embedsky embedsky 625K Oct 5 10:38 .tmp_vmlinux2.syms
  • .tmp_vmlinux2.syms截取一点的内容如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00000000 A cpu_v7m_suspend_size
c0008000 T _text
c0008000 T stext
c0008028 t __vet_atags
c0008060 T __entry_text_start
c0008060 T __idmap_text_end
c0008060 T __idmap_text_start
c0008060 t __ret_fast_syscall
c0008060 T _stext
c0008060 t ret_fast_syscall
c00080ac t fast_work_pending
c00080b8 t slow_work_pending
c00080d4 t ret_slow_syscall
c00080d4 T ret_to_user
c00080d8 T ret_to_user_from_irq
c00080e0 t no_work_pending
c0008120 T ret_from_fork
c0008140 T vector_swi
c000818e t local_restart
c00081c4 t __sys_trace
c00081ea t __sys_trace_return_nosave
c00081f4 t __sys_trace_return
c0008200 T sys_call_table
c0008960 t sys_syscall

这是一个非常好的问题,它触及了内核构建过程的核心。Makefile 中的定义是抽象的,实际执行的命令会根据你的架构、配置和工具链被具体化。

实际上,Makefile 规则会扩展成一个类似下面这样的命令行,并传递给你的 shell 来执行。这个命令行是scripts/link-vmlinux.sh 脚本的调用

传递给脚本的命令模板

其基本形式如下:

1
scripts/link-vmlinux.sh <链接器> <链接器标志> <输出文件名>

针对 STM32H750 (ARMv7-M) 的一个具体示例

如果我们假设你正在为 STM32H750 平台进行交叉编译,那么实际在你的主机终端上执行的命令会非常接近下面这样:

1
2
3
4
5
# 为了可读性,用反斜杠分行
scripts/link-vmlinux.sh \
arm-none-eabi-ld \
"-p --build-id=none -X -T arch/arm/kernel/vmlinux.lds" \
vmlinux.unstripped

我们来逐个解析这个命令的参数,它们都是 scripts/link-vmlinux.sh 脚本的输入:

  1. scripts/link-vmlinux.sh: 要执行的脚本本身。

  2. arm-none-eabi-ld: 这是传递给脚本的第一个参数 ($1)。它代表了将要使用的链接器程序。对于 ARM Cortex-M 裸机目标,通常是 arm-none-eabi-ld。这个值来自 Makefile 变量 $(LD)

  3. "-p --build-id=none -X -T arch/arm/kernel/vmlinux.lds": 这是传递给脚本的第二个参数 ($2),一个包含所有链接器标志的长字符串。

    • -p: 是一个链接器选项。
    • --build-id=none: 指示链接器不要生成 build-id note section。
    • -X: 是另一个链接器选项。
    • -T arch/arm/kernel/vmlinux.lds: 这是其中最关键的标志。它告诉链接器使用 arch/arm/kernel/vmlinux.lds 这个链接器脚本来指导链接过程。对于无 MMU 的 STM32H750,这个脚本会定义内核代码和数据应该被放置在哪个物理内存地址,以及各个段(section)的布局。
  4. vmlinux.unstripped: 这是传递给脚本的第三个参数 ($3),指定了最终输出文件的名称。

脚本内部做了什么?

理解这一点至关重要:上面这条命令并不是最终的链接命令scripts/link-vmlinux.sh 脚本是一个封装器 (wrapper)。它接收到上述参数后,会在其内部构造一个更长、更复杂的最终链接命令,然后才去执行它。

脚本内部构造的最终命令可能看起来像这样(这是一个极大简化的示意):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 这是由 link-vmlinux.sh 脚本在内部生成并执行的命令
arm-none-eabi-ld \
-p --build-id=none -X -T arch/arm/kernel/vmlinux.lds \
-o vmlinux.unstripped \
--start-group \
init/built-in.a \
usr/built-in.a \
block/built-in.a \
crypto/built-in.a \
drivers/built-in.a \
fs/built-in.a \
ipc/built-in.a \
kernel/built-in.a \
lib/built-in.a \
net/built-in.a \
security/built-in.a \
sound/built-in.a \
arch/arm/lib/lib.a \
--end-group

脚本的核心作用

  1. 解析参数:接收来自 Makefile 的链接器、标志和输出文件名。
  2. 构造输入文件列表:最关键的一步是,它会负责将 vmlinux.o 这个“伪”对象文件(实际上是一个描述文件)展开成一个包含内核所有部分的 built-in.a 归档文件的完整列表。
  3. 执行链接:使用接收到的链接器和标志,以及它自己构造的完整输入文件列表,执行真正的链接操作。
  4. 执行额外任务:在链接之后,脚本还会负责生成 System.map 文件,这个文件是内核符号地址的文本映射表,对于调试至关重要。

总结来说,Makefile 调用 link-vmlinux.sh 并给它一些高级指令,然后 link-vmlinux.sh 脚本负责处理所有繁琐的细节,构造出一条完整的、包含数百个输入文件的最终链接命令来生成 vmlinux.unstripped

scripts/link-vmlinux.sh 内核映像最终链接的主控脚本

本代码是一个 shell 脚本,是 Linux 内核构建系统(Kbuild)的核心组件。它扮演着最终链接阶段的“主控程序”角色,负责将内核的所有组成部分(vmlinux.a 等)链接成一个单一的、完整的 vmlinux ELF 可执行文件。这个脚本远不止是简单地调用链接器 ld;它是一个复杂的多阶段过程的 orchestrator,负责处理条件编译、生成并嵌入内核符号表(Kallsyms)、生成 BTF 调试信息、创建 System.map 文件,并处理架构特定的需求。

实现原理分析

此脚本的实现原理体现了内核构建过程的复杂性和对精确性的要求,其核心是基于配置的条件化执行解决自引用数据依赖的迭代链接

  1. 封装与参数化: 脚本首先从命令行接收由 Makefile 传递来的参数($1$4),如链接器程序、链接器标志和输出文件名。这是一种典型的封装,将复杂的链接逻辑从 Makefile 语法中解耦出来,提高了可读性和可维护性。

  2. 条件化特征集成: 脚本的主体由大量的 if is_enabled CONFIG_... 判断构成。is_enabled 函数通过检查 include/config/auto.conf 文件来确定某个内核配置项是否被启用。这使得脚本的行为是高度动态的:

    • 如果 CONFIG_KALLSYMS 被启用,脚本会执行一个复杂的多遍链接过程来生成和嵌入符号表。
    • 如果 CONFIG_DEBUG_INFO_BTF 被启用,它会调用 pahole 工具来生成 BTF 类型信息,并将其链接到内核中。
    • 如果 CONFIG_LTO_CLANG (链接时优化) 被启用,它会直接使用 vmlinux.o 而不是 vmlinux.a 作为输入,以避免重复执行耗时的 LTO 链接。
      这种设计使得最终的链接过程可以根据内核配置,精确地包含所需的功能,而不会引入不必要的开销或依赖。
  3. 迭代链接解决“鸡生蛋”问题 (Kallsyms): 脚本中最复杂的部分是 CONFIG_KALLSYMS 的处理。这里存在一个经典的自引用依赖(“鸡生蛋”)问题:

    • 问题: 内核需要将所有符号的地址嵌入到一个名为 __kallsyms 的数据段中。但是,__kallsyms 数据段本身的大小会影响其后所有符号的最终地址。这意味着,在不知道 __kallsyms 确切大小之前,无法计算出正确的符号地址;而在没有计算出所有符号地址之前,又无法确定 __kallsyms 的内容和大小。
    • 解决方案 (多遍链接): 脚本通过一个迭代过程来解决这个问题:
      a. 第一遍: 首先链接一个不包含或只包含一个虚拟 kallsyms 段的临时内核 (.tmp_vmlinux1)。
      b. 生成符号表: 基于 .tmp_vmlinux1 生成一个近似的符号表 (.tmp_vmlinux1.kallsyms.o)。
      c. 第二遍: 将这个大小近似正确的符号表对象文件链接进去,生成一个新的临时内核 (.tmp_vmlinux2)。由于增加了 kallsyms 段,很多符号的地址发生了偏移。
      d. 再次生成符号表: 基于 .tmp_vmlinux2 生成一个更精确的符号表。
      e. 可选的第三遍: 脚本会比较两次生成的符号表大小。如果大小不同(意味着地址偏移导致了链接器需要增加新的 branch stubs,从而引入了新的符号),它会再进行一次链接,以确保符号表收敛并达到稳定状态。
      f. 最终链接: 最后,使用这个稳定、正确的符号表对象文件,链接生成最终的 vmlinux 文件。
  4. 生成辅助文件: 脚本不仅生成 vmlinux,还负责创建两个至关重要的调试文件:

    • System.map: 通过 mksysmap 函数(内部调用 nm 工具)生成。这是一个文本文件,列出了所有内核全局符号的名称及其对应的地址,是内核调试的“地图”。
    • vmlinux.map: 如果配置了 CONFIG_VMLINUX_MAP,脚本会传递 -Map=vmlinux.map 标志给链接器,生成一个更详细的链接映射文件,包含所有符号(包括局部的)和内存段的详细布局信息。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
#
# 链接 vmlinux
#
# vmlinux 由 vmlinux.a 和 $(KBUILD_VMLINUX_LIBS) 中的对象链接而成。
# vmlinux.a 包含无条件链接的对象。
# $(KBUILD_VMLINUX_LIBS) 是有条件链接的归档文件(不在 --whole-archive 内)。
# ... (注释描述了文件依赖关系)

# 发生错误时立即退出
set -e

# === 参数解析 ===
# 从命令行接收由 make 传递的参数
LD="$1" # 链接器程序 (如 arm-none-eabi-ld)
KBUILD_LDFLAGS="$2" # Kbuild 定义的链接器标志
LDFLAGS_vmlinux="$3" # 专门用于 vmlinux 链接的标志
VMLINUX="$4" # 最终输出文件名 (如 vmlinux.unstripped)

# === 辅助函数 ===
# 检查一个内核配置项是否被启用
is_enabled() {
grep -q "^$1=y" include/config/auto.conf
}

# 以 Kbuild 格式打印信息性输出
info()
{
printf " %-7s %s\n" "${1}" "${2}"
}

# === 核心函数:vmlinux 链接 ===
# ${1} - 输出文件名
vmlinux_link()
{
local output=${1}
# ... (局部变量定义)

info LD ${output} # 打印 "LD vmlinux" 等信息
shift # 移出第一个参数 (输出文件名)

# 根据配置选择输入文件
if is_enabled CONFIG_LTO_CLANG || is_enabled CONFIG_X86_KERNEL_IBT; then
# 如果启用了LTO, 直接使用 vmlinux.o 以避免重复漫长的LTO链接
objs=vmlinux.o
libs=
else
# 否则,使用包含所有内核对象的归档文件 vmlinux.a
objs=vmlinux.a
libs="${KBUILD_VMLINUX_LIBS}"
fi

# 如果配置了内置DTB,则添加DTB对象文件
if is_enabled CONFIG_GENERIC_BUILTIN_DTB; then
objs="${objs} .builtin-dtbs.o"
fi

# 添加必要的内核对象文件
objs="${objs} .vmlinux.export.o"
objs="${objs} init/version-timestamp.o"

# 针对不同架构设置链接器和标志
if [ "${SRCARCH}" = "um" ]; then # User-mode Linux 特殊处理
# ...
else
wl= # 用于传递给链接器的选项前缀, e.g., -Wl,
ld="${LD}"
ldflags="${KBUILD_LDFLAGS} ${LDFLAGS_vmlinux}"
ldlibs=
fi

# 指定链接器脚本,这是定义内存布局的关键!
ldflags="${ldflags} ${wl}--script=${objtree}/${KBUILD_LDS}"

# 如果需要,添加剥离调试信息的标志
if [ -n "${strip_debug}" ] ; then
ldflags="${ldflags} ${wl}--strip-debug"
fi

# 如果需要,生成详细的链接映射文件
if [ -n "${generate_map}" ]; then
ldflags="${ldflags} ${wl}-Map=vmlinux.map"
fi

# === 执行最终的链接命令 ===
${ld} ${ldflags} -o ${output} \
${wl}--whole-archive ${objs} ${wl}--no-whole-archive \
${wl}--start-group ${libs} ${wl}--end-group \
${kallsymso} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
}

# === 核心函数:生成 BTF (BPF Type Format) 信息 ===
gen_btf()
{
# ... (调用 pahole 和 objcopy 生成 BTF 数据)
}
# === 脚本主执行流程 ===

# 确保 init/version-timestamp.o 是最新的
${MAKE} -f "${srctree}/scripts/Makefile.build" obj=init init/version-timestamp.o

# ... (初始化变量)

# --- Kallsyms/BTF 多阶段链接 ---

# 如果启用了 KALLSYMS,先生成一个空的 kallsyms 对象文件
if is_enabled CONFIG_KALLSYMS; then
true > .tmp_vmlinux0.syms
kallsyms .tmp_vmlinux0.syms .tmp_vmlinux0.kallsyms
fi

# 如果启用了 KALLSYMS 或 BTF,进行第一次链接
if is_enabled CONFIG_KALLSYMS || is_enabled CONFIG_DEBUG_INFO_BTF; then
# 如果不需要BTF,则在链接时剥离调试信息以加快速度
if ! is_enabled CONFIG_DEBUG_INFO_BTF; then
strip_debug=1
fi
# [第一遍链接] 生成临时内核 .tmp_vmlinux1
vmlinux_link .tmp_vmlinux1
fi

# 如果启用了 BTF,基于 .tmp_vmlinux1 生成 BTF 数据
if is_enabled CONFIG_DEBUG_INFO_BTF; then
# ...
fi

# 如果启用了 KALLSYMS,开始迭代链接过程
if is_enabled CONFIG_KALLSYMS; then
# ... (注释解释了多遍链接的原因)

strip_debug=1 # kallsyms 链接不需要调试信息

# 基于 .tmp_vmlinux1 生成符号表和 kallsyms 对象
sysmap_and_kallsyms .tmp_vmlinux1
size1=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" ${kallsymso})

# [第二遍链接] 将第一次生成的 kallsyms 对象链接进去,生成 .tmp_vmlinux2
vmlinux_link .tmp_vmlinux2
# 基于 .tmp_vmlinux2 再次生成符号表
sysmap_and_kallsyms .tmp_vmlinux2
size2=$(${CONFIG_SHELL} "${srctree}/scripts/file-size.sh" ${kallsymso})

# 检查 kallsyms 大小是否稳定,如果不稳定或强制要求,则进行第三遍链接
if [ $size1 -ne $size2 ] || [ -n "${KALLSYMS_EXTRA_PASS}" ]; then
vmlinux_link .tmp_vmlinux3
sysmap_and_kallsyms .tmp_vmlinux3
fi
fi

strip_debug= # 清除标志,为最终链接做准备

# 如果配置了,则在最终链接时生成 vmlinux.map
if is_enabled CONFIG_VMLINUX_MAP; then
generate_map=1
fi

# === 最终链接 ===
# 使用经过迭代后得到的、大小正确的 kallsymso,链接生成最终的 vmlinux 文件
vmlinux_link "${VMLINUX}"

# ... (最终的 BTF ID 解析、System.map 生成、表排序和一致性检查)

# === 生成 System.map ===
mksysmap "${VMLINUX}" System.map

# ...

# 检查最终的 System.map 和 kallsyms 过程中的符号表是否一致
if is_enabled CONFIG_KALLSYMS; then
if ! cmp -s System.map "${kallsyms_sysmap}"; then
echo >&2 Inconsistent kallsyms data
exit 1
fi
fi

mksysmap 与 kallsyms: 内核符号表的提取与二进制化

本代码片段是 link-vmlinux.sh 脚本中的一组核心辅助函数,它们共同负责将一个链接好的内核 ELF 文件(如 .tmp_vmlinux1)中的符号信息,提取出来并转化为一个可被再次链接回内核的二进制对象文件(.o 文件)。mksysmap 负责第一步:从 ELF 文件中提取符号并生成一个人类可读的文本符号映射表(System.map 格式)。kallsyms 则负责第二步:读取这个文本映射表,并调用一个专门的工具将其转化为包含高度压缩的二进制符号数据的汇编代码,最后再将该汇编代码编译成一个标准的 .o 对象文件。

实现原理分析

此机制是一个经典的多阶段数据转换流水线,其核心是将链接器产生的符号信息,通过一系列转换,变成一种紧凑的、可被内核自身在运行时查询的二进制数据格式,并最终将其作为数据段链接回内核

  1. 第一阶段:符号提取与格式化 (mksysmap)

    • 提取: 该函数的核心是 ${NM} -n "${1}" 命令。nm 是一个标准的二进制工具,用于列出对象文件中的符号。-n 选项告诉 nm 按地址对符号进行数字排序。
    • 格式化: nm 的原始输出包含了许多不必要的信息。因此,其输出被通过管道(|)传递给 sed -f "${srctree}/scripts/mksysmap"sed 在这里使用一个名为 mksysmap 的脚本文件,该脚本包含了一系列复杂的文本替换和过滤规则,用于清除局部符号、调试符号,并最终将输出格式化为标准的 地址 类型 符号名 格式。这个最终的文本文件就是 System.map 的内容。
  2. 第二阶段:文本到二进制的转换 (kallsyms)

    • 转换: 该函数的核心是调用 scripts/kallsyms ${kallsymopt} "${1}" > "${2}.S"。这里的 scripts/kallsyms 不是 shell 函数,而是一个预先编译好的 C 语言可执行程序。这个程序是整个 kallsyms 机制的大脑。它读取 mksysmap 生成的纯文本符号列表,然后在内存中执行复杂的压缩算法(如前文分析的构建令牌表、压缩名称、计算相对偏移等),最后将这些压缩好的二进制数据,以汇编语言的数据定义指令(如 .byte, .long, .asciz)的形式,打印到标准输出。
    • 汇编: 该函数的下一步是 ${CC} ... -c -o "${2}.o" "${2}.S"。它使用 C 编译器(在这里实际上是调用其集成的汇编器 as)来将上一步生成的 .S 汇编文件,汇编成一个标准的 ELF 对象文件。这个对象文件现在包含了一个或多个数据段(如 .rodata.__kallsyms_names),其中就存放着完整的、压缩后的二进制内核符号表。
  3. 封装器 (sysmap_and_kallsyms): 这个函数简单地将上述两个步骤串联起来,形成一个完整的“从 ELF 到可链接的符号表对象”的处理流程。这是在 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
45
46
47
48
49
50
51
52
53
54
55
/**
* @brief 将文本符号列表转换为包含二进制符号数据的汇编代码,并将其汇编成对象文件。
* @param ${1} 输入的文本符号列表文件 (例如 .tmp_vmlinux1.syms)。
* @param ${2} 输出的对象文件名前缀 (例如 .tmp_vmlinux1.kallsyms)。
*/
kallsyms()
{
# ... (kallsymopt 变量设置)

# 打印信息,例如 "KSYMS .tmp_vmlinux1.kallsyms.S"
info KSYMS "${2}.S"
# 调用 C 程序 'scripts/kallsyms',它读取文本符号列表 ${1}
# 并生成包含压缩二进制符号表的汇编代码,输出到 ${2}.S 文件。
scripts/kallsyms ${kallsymopt} "${1}" > "${2}.S"

# 打印信息,例如 "AS .tmp_vmlinux1.kallsyms.o"
info AS "${2}.o"
# 使用 C 编译器 (作为汇编器) 将生成的汇编文件 ${2}.S 编译成对象文件 ${2}.o。
${CC} ... -c -o "${2}.o" "${2}.S"

# 将新生成的对象文件名保存在一个全局变量中,以供后续链接步骤使用。
kallsymso=${2}.o
}

# ... (其他辅助函数)

/**
* @brief 从一个 ELF 文件中提取所有符号,并生成一个 System.map 格式的文本文件。
* @param ${1} 输入的 ELF 文件 (例如 .tmp_vmlinux1)。
* @param ${2} 输出的文本符号映射文件名。
*/
mksysmap()
{
# 打印信息,例如 "NM .tmp_vmlinux1.syms"
info NM ${2}
# 使用 'nm' 工具提取 ${1} 中的所有符号,并按地址排序 (-n)。
# 然后通过管道将输出传递给 'sed',使用 'scripts/mksysmap' 脚本
# 来过滤和格式化输出,最终结果重定向到 ${2}
${NM} -n "${1}" | sed -f "${srctree}/scripts/mksysmap" > "${2}"
}

/**
* @brief 一个封装函数,用于对一个临时的vmlinux文件执行完整的符号表提取和二进制化过程。
* @param ${1} 输入的临时 vmlinux ELF 文件。
*/
sysmap_and_kallsyms()
{
# 第一步:从输入的 vmlinux 文件 ${1} 生成文本符号映射表 ${1}.syms。
mksysmap "${1}" "${1}.syms"
# 第二步:将文本符号映射表 ${1}.syms 转换为可链接的对象文件 ${1}.kallsyms.o。
kallsyms "${1}.syms" "${1}.kallsyms"

# 将生成的文本符号映射文件名保存在一个全局变量中,用于后续的一致性检查。
kallsyms_sysmap=${1}.syms
}

scripts/mksysmap: 内核符号表的精炼过滤器

本代码是一个 sed (Stream EDitor) 脚本,在 Linux 内核的构建过程中扮演着一个至关重要的过滤器角色。当 link-vmlinux.sh 脚本执行 mksysmap 函数时,它会首先使用 nm 工具从链接好的 vmlinux ELF 文件中提取出原始的、包含所有符号的列表。然后,这个 mksysmap sed 脚本被用来处理这个原始列表,其核心功能是根据一系列预定义的规则,删除所有对于生成最终 System.map 文件和 kallsyms 符号表来说无用或不合适的符号,最终输出一个干净、精炼且有用的符号列表。

实现原理分析

此脚本的实现原理是基于正则表达式的行过滤。它遵循 sed 的基本工作模式,逐行读取由 nm 工具生成的 地址 类型 符号名 格式的文本流,并对每一行应用一系列 /pattern/d 命令。

  • 工作模式: 整个脚本是一个“黑名单”或“排除列表”。它不定义要保留什么,而是精确地定义要删除什么。任何没有被脚本中的任何规则匹配到的行,都会被原样输出,从而构成了最终的干净符号列表。
  • 匹配命令 (/pattern/d): 这是 sed 的一个核心命令。斜杠 / 包围的是一个正则表达式模式(pattern)。如果当前处理的行能够匹配这个模式,d 命令就会被执行,意味着“删除(delete)”当前行,并且 sed 会立即停止对该行的后续处理,开始读取下一行。
  • 精确匹配: 脚本中的模式经过精心设计,以确保精确匹配,避免误删。例如:
    • / [aNUw] /d: 模式中的空格确保了它只匹配代表符号类型的单个字符,而不会意外匹配到地址或符号名中的相同字母。
    • / \$/d: 模式开头的空格和 \ 转义的 $ 符号,精确地匹配以 $ 开头的局部符号名。
    • /_from_arm$/d: 模式末尾的 $ 是一个锚点,表示“行尾”,确保只删除以 _from_arm 结尾的符号。

通过组合这些精确的正则表达式,脚本能够像手术刀一样,从数以万计的符号中剔除所有编译器生成的内部标签、调试信息、模块版本数据以及其他对最终用户和调试工具无意义的“噪音”。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/bin/sed -f
# SPDX-License-Identifier: GPL-2.0-only
#
# 用于过滤 System.map 和 kallsyms 不需要或不合适的符号的 sed 脚本。
# 输入应该是 'nm -n <file>' 的输出。
# ... (注释描述了 System.map 的用途)

# ---------------------------------------------------------------------------
# 1. 根据符号类型进行过滤
# ---------------------------------------------------------------------------

# a: 局部绝对符号 (local absolute symbols)
# N: 调试符号 (debugging symbols)
# U: 未定义的全局符号 (undefined global symbols) - System.map只关心已定义的
# w: 局部的弱符号 (local weak symbols)
# 格式为 "/ [类型] /d",匹配 " 地址 类型 名称 " 中的类型字段并删除该行
/ [aNUw] /d

# ---------------------------------------------------------------------------
# 2. 根据符号名称的前缀进行过滤
# ---------------------------------------------------------------------------
# (注意每个模式前的空格,以确保从符号名的开头开始匹配)

# ARM, MIPS 等架构使用的局部符号,通常以 '$' 开头
/ \$/d

# 编译器生成的局部标签,用于内部跳转,对用户无意义。
# 如 .LBB, .Ltmpxxx, .L__unnamed_xx, .LASANPC 等
/ \.L/d

# arm64 EFI stub 的命名空间下的符号
/ __efistub_/d

# arm64 位置无关可执行文件 (PIE) 命名空间下的局部符号
/ __pi_\\$/d
/ __pi_\.L/d

# arm64 在非VHE模式下 KVM 的命名空间下的局部符号
/ __kvm_nvhe_\$/d
/ __kvm_nvhe_\.L/d

# lld 链接器为 arm/aarch64/mips 生成的 thunk 函数 (代码跳转存根)
/ __[[:alnum:]]*Thunk_/d

# 控制流完整性 (CFI) 的类型标识符
/ __kcfi_typeid_/d
/ __kvm_nvhe___kcfi_typeid_/d
/ __pi___kcfi_typeid_/d

# modversions 功能生成的 CRC 校验和
/ __crc_/d

# EXPORT_SYMBOL 宏生成的字符串表,存储符号的名称本身
/ __kstrtab_/d

# EXPORT_SYMBOL 宏生成的命名空间字符串表
/ __kstrtabns_/d

# MODULE_DEVICE_TABLE 宏生成的设备表符号
/ __mod_device_table__/d

# ---------------------------------------------------------------------------
# 3. 根据符号名称的后缀进行过滤
# ---------------------------------------------------------------------------
# (注意每个模式后的 '$',它匹配行尾)

# arm 架构特有的符号,用于标记来自 ARM 或 Thumb 模式的代码
/_from_arm$/d
/_from_thumb$/d
# arm 链接器生成的 veneer (代码片段,用于长跳转或模式切换)
/_veneer$/d

# ---------------------------------------------------------------------------
# 4. 根据精确匹配的符号名进行过滤
# ---------------------------------------------------------------------------
# (注意模式前后的空格和 '$',以确保完全匹配)

# 可能是 LoongArch 架构的特定标签
/ L0$/d

# PowerPC 架构的特定基地址符号
/ _SDA_BASE_$/d
/ _SDA2_BASE_$/d

# MODULE_INFO() 宏生成的唯一ID符号
/ __UNIQUE_ID_modinfo[0-9]*$/d

# ---------------------------------------------------------------------------
# 5. 根据包含的模式进行过滤
# ---------------------------------------------------------------------------
# (只要符号名中包含该模式就会被忽略)

# PowerPC 架构的跳转桩 (stub)
/\.long_branch\./d
/\.plt_branch\./d