五分钟系列-nm工具
目录
1. nm 工具是什么?
2. 为什么需要 nm?主要用途
3. 基本语法和输出格式
基本语法
输出格式
4. 重要的符号类型(Symbol Types)
5. 常用选项和示例
示例代码 (test.c)
1. 默认输出(列出所有符号)
2. 只显示未定义的符号 (-u)
3. 只显示已定义的符号 (--defined-only)
4. 按符号值排序 (-n)
5. 显示调试符号 (-a)
6. 显示外部/动态符号 (--extern-only)
7. 用于动态库 (.so)
8. 与 grep 组合使用(非常实用)
6. 注意事项和局限性
总结
1. nm 工具是什么?
nm
(Name List 的缩写)是 GNU Binutils 工具集中的一个命令行程序,用于显示目标文件(如库文件 .a
、.so
和可执行文件)中的符号信息。
所谓符号,通常包括:
-
函数名
-
全局变量名
-
静态变量名
-
以及其他一些在链接和调试过程中至关重要的标识符。
简单来说,nm
可以让你“窥探”一个二进制文件内部,看看它定义了哪些函数和变量,又引用了哪些外部的函数和变量。这对于调试、链接错误排查和反向工程来说极其有用。
2. 为什么需要 nm?主要用途
-
排查链接错误:这是最常见的用途。当链接器报错说
undefined reference to 'function_name'
时,你可以用nm
来检查:-
你的目标文件(
.o
)是否真的引用了这个符号(会标记为U
)。 -
你链接的库文件(
.a
或.so
)是否真的定义了这个符号(应该标记为T
或D
等)。
-
-
分析库文件内容:你想知道一个静态库(
.a
)或动态库(.so
)提供了哪些函数接口,就可以用nm
来查看。 -
解决符号冲突:当出现“多重定义”错误时,
nm
可以帮助你找到多个定义了相同符号的文件。 -
逆向工程与调试:在缺乏源代码的情况下,分析二进制文件的结构,了解程序大致的执行流程。
3. 基本语法和输出格式
基本语法
nm [选项] <文件名>
输出格式
默认情况下,nm
的输出通常包含三列:
<符号值> <符号类型> <符号名称>
-
符号值:通常是符号在内存或段中的地址偏移量。对于未定义的符号,该值为空。
-
符号类型:一个关键的字母,说明了符号的类型和属性(详见下文)。
-
符号名称:符号本身的名称。
4. 重要的符号类型(Symbol Types)
nm
输出的符号类型是理解其功能的核心。以下是一些最常见和重要的类型:
类型字母 | 含义 | 示例/说明 |
---|---|---|
A | 绝对地址 - 该符号的值是绝对的,在链接过程中不会被改变。 | 通常用于特殊用途的符号。 |
B | BSS 段 - 该符号位于未初始化的数据段(BSS)中。 | 例如未初始化的全局变量 |
D | 数据段 - 该符号位于已初始化的数据段中。 | 例如已初始化的全局变量 |
T | 文本段 - 该符号位于代码段(text)中。 | 这是最常见的函数符号。例如函数 |
U | 未定义 - 该符号在当前文件中被引用,但未被定义。 | 需要从其他库或目标文件中链接进来。这是链接错误排查的关键。 |
W | 弱引用 - 一个未定义的弱符号。如果找不到定义,不会导致链接错误。 | 常用于可选的扩展功能。 |
R | 只读数据段 - 该符号位于只读数据段中。 | 例如字符串常量 |
C | 公共段 - 该符号是公共的,未初始化的数据。 | 类似于 |
I | 间接引用 - 另一个符号的间接引用(常见于动态链接)。 | |
N | 调试符号 - 这是一个调试符号。 | |
V | 弱对象 - 一个弱的已定义符号(弱定义)。 | |
- | stabs 符号 - 用于调试的符号信息。 | |
? | 未知 - 符号类型未知,或者格式无法识别。 | 有时出现在特殊格式的文件中。 |
注意:字母的大小写有时也表示不同的含义:
-
小写:符号是局部的(local),其作用域仅限于当前目标文件。
-
大写:符号是全局的(global/external),可以被其他目标文件使用。
例如:
-
T
:全局函数 -
t
:静态函数(文件内部函数) -
D
:全局变量 -
d
:静态变量
5. 常用选项和示例
假设我们有一个简单的 C 程序,编译成目标文件 test.o
。
示例代码 (test.c
)
#include <stdio.h>int global_var = 10; // 已初始化全局变量 (D)
static int static_var; // 未初始化静态变量 (B)
const int read_only_var = 100; // 只读变量 (R)void my_function() { // 全局函数 (T)static int inside_static; // 静态局部变量 (b/d, 位于BSS段)printf("Hello\n");
}void undefined_function(); // 声明一个未定义的函数int main() { // 全局函数 (T)my_function();undefined_function(); // 引用一个未定义的函数 (U)return 0;
}
使用 gcc -c test.c
编译生成 test.o
。
1. 默认输出(列出所有符号)
nm test.o
输出可能如下(地址和顺序可能不同):
0000000000000000 T main
0000000000000000 T my_functionU printfU undefined_function
0000000000000000 D global_var
0000000000000004 R read_only_var
0000000000000000 B static_var
可以看到 printf
和 undefined_function
是 U
(未定义),main
和 my_function
是 T
(代码),global_var
是 D
(数据),等等。
2. 只显示未定义的符号 (-u
)
用于快速检查缺少哪些依赖。
nm -u test.o
输出:
U printf
U undefined_function
3. 只显示已定义的符号 (--defined-only
)
nm --defined-only test.o
4. 按符号值排序 (-n
)
按地址顺序显示,对于分析内存布局很有用。
nm -n test.o
5. 显示调试符号 (-a
)
通常会显示更多信息,包括调试用的符号。
nm -a test.o
6. 显示外部/动态符号 (--extern-only
)
通常只显示大写类型的符号(全局符号)。
nm --extern-only test.o
7. 用于动态库 (.so)
用法完全相同。
nm /lib/x86_64-linux-gnu/libc.so.6 | head -20 # 查看glibc的前20个符号
8. 与 grep 组合使用(非常实用)
查找是否定义了某个特定函数:
nm test.o | grep main
查找所有未定义的函数:
nm test.o | grep ' U '
6. 注意事项和局限性
-
剥离的二进制文件:如果可执行文件或库被
strip
工具处理过(去除了符号表),nm
的输出会大大减少,甚至显示nm: testfile: no symbols
。这在发布版本中很常见。 -
C++ 名称修饰:对于 C++ 代码,函数名会被“修饰”,包含命名空间、类、参数类型等信息,导致
nm
输出的符号名难以阅读。可以使用-C
或--demangle
选项来将其还原为可读形式。nm -C my_cpp_program
-
不同平台:
nm
主要用于 ELF 格式的文件(Linux 的标准格式),虽然它也支持其他格式,但行为和输出可能不同。
总结
nm
是 Linux 开发者和系统管理员工具箱中一个不可或缺的、简单而强大的工具。它通过揭示二进制文件内部的符号信息,为调试、链接问题排查和二进制分析提供了关键的洞察力。掌握 nm
的基本用法和符号类型含义,能极大地提高解决底层编译和链接问题的效率。