nm 是 Linux 中用于列出目标文件(可执行文件、对象文件、库文件)中的符号的命令行工具。它是二进制分析和调试的重要工具,可以显示函数、变量等符号的名称、类型和地址信息。
📦 基本语法
bash
nm [选项] [文件...]
默认行为 :如果不指定文件,nm 会尝试读取 a.out 文件。
🎯 常用选项详解
| 选项 | 说明 |
|---|---|
| 符号类型过滤 | |
-a, --debug-syms |
显示所有符号,包括调试符号 |
-g, --extern-only |
只显示外部(全局)符号 |
-u, --undefined-only |
只显示未定义的符号 |
--defined-only |
只显示已定义的符号 |
| 符号排序 | |
-n, --numeric-sort |
按符号地址排序(数字顺序) |
-r, --reverse-sort |
反序排序 |
-p, --no-sort |
不排序,按符号表顺序显示 |
-v, --numeric-sort |
按符号地址排序(同 -n) |
| 输出格式 | |
-f, --format=格式 |
指定输出格式(bsd, sysv, posix) |
-t, --radix=基数 |
设置地址基数(d=十进制, o=八进制, x=十六进制) |
-l, --line-numbers |
显示符号的行号信息(需要调试信息) |
-S, --print-size |
显示符号的大小 |
-s, --print-armap |
显示归档文件的索引 |
-A, --print-file-name |
在每个符号前显示文件名 |
-o, --print-file-name |
同 -A(BSD 格式) |
| 符号名称处理 | |
-C, --demangle |
解码 C++ 符号(demangle)为可读格式 |
--no-demangle |
不解码 C++ 符号 |
--special-syms |
显示特殊符号 |
| 其他选项 | |
-D, --dynamic |
显示动态符号,而不是普通符号 |
--with-symbol-versions |
显示符号版本信息 |
--size-sort |
按符号大小排序 |
--help |
显示帮助信息 |
--version |
显示版本信息 |
🔍 符号类型说明
nm 输出的符号类型字符(第二列):
| 字符 | 类型 | 说明 |
|---|---|---|
| A | 绝对地址符号 | 值不会因重定位而改变 |
| B | 未初始化数据段(BSS) | 位于 .bss 节 |
| C | 公共符号 | 未初始化的数据 |
| D | 已初始化数据段 | 位于 .data 节 |
| G | 已初始化数据段(小对象) | 位于 .sdata 节(某些架构) |
| I | 间接引用另一个符号 | |
| N | 调试符号 | |
| R | 只读数据段 | 位于 .rodata 节 |
| S | 未初始化数据段(小对象) | 位于 .sbss 节(某些架构) |
| T | 文本(代码)段 | 位于 .text 节 |
| U | 未定义符号 | 在当前文件中未定义 |
| V | 弱符号 | 弱定义符号 |
| W | 弱符号(未标记) | 弱引用符号 |
| - | 调试符号(stabs) | |
| ? | 未知类型 |
小写字母 表示局部符号,大写字母表示全局(外部)符号。
💡 核心用法示例
1. 基本符号列表
bash
# 列出目标文件中的所有符号
nm myprogram.o
# 列出可执行文件中的符号
nm /bin/ls
# 列出动态库中的符号
nm /lib/x86_64-linux-gnu/libc.so.6
# 列出静态库中的符号
nm libmylib.a
# 列出多个文件的符号
nm *.o
2. 按类型过滤符号
bash
# 只显示未定义的符号(需要其他文件提供)
nm -u myprogram.o
# 只显示已定义的符号
nm --defined-only myprogram.o
# 只显示外部(全局)符号
nm -g myprogram.o
# 显示所有符号,包括调试符号
nm -a myprogram.o
# 显示动态符号(用于共享库)
nm -D libmylib.so
3. 排序和格式控制
bash
# 按地址排序(数字顺序)
nm -n myprogram.o
# 反序排序(地址从高到低)
nm -r myprogram.o
# 不排序,按符号表顺序显示
nm -p myprogram.o
# 显示符号大小
nm -S myprogram.o
# 显示每个符号的文件名(用于多个文件)
nm -A *.o
# 以十六进制显示地址和大小
nm -t x myprogram.o
# 以十进制显示地址
nm -t d myprogram.o
4. 符号名称处理
bash
# 解码 C++ 符号(demangle)
nm -C myprogram.cpp.o
# 不解码 C++ 符号
nm --no-demangle myprogram.cpp.o
# 显示符号的行号信息(需要编译时加 -g)
nm -l myprogram.o
5. 输出格式控制
bash
# BSD 格式(默认)
nm -f bsd myprogram.o
# System V 格式
nm -f sysv myprogram.o
# POSIX 格式
nm -f posix myprogram.o
# 自定义格式:地址、类型、大小、名称
nm -t d -S -f posix myprogram.o
📊 输出示例解读
示例 1:简单目标文件
bash
# 编译示例程序
echo 'int global_var = 42; static int static_var = 100; void func() {} int main() { return 0; }' > test.c
gcc -c test.c -o test.o
# 查看符号
nm test.o
输出:
0000000000000000 T func
0000000000000000 D global_var
0000000000000000 T main
0000000000000004 d static_var
解读:
func:全局文本(代码)符号(T),地址 0x0global_var:全局已初始化数据(D),地址 0x0main:全局文本符号(T),地址 0x0static_var:局部已初始化数据(d),地址 0x4
示例 2:C++ 程序
bash
# C++ 示例
echo 'class MyClass { public: void method(); }; void MyClass::method() {}' > test.cpp
g++ -c test.cpp -o test.o
# 未解码的符号
nm test.o
# 输出:_ZN7MyClass6methodEv
# 解码后的符号
nm -C test.o
# 输出:MyClass::method()
🔧 实用场景示例
场景 1:查找未定义符号(解决链接错误)
bash
# 查找对象文件中未定义的符号(需要链接的符号)
nm -u myprogram.o
# 查找共享库中未定义的符号
nm -Du libmylib.so
# 查找可执行文件中未定义的符号(通常很少)
nm -u /bin/ls
# 查找特定未定义符号
nm -u myprogram.o | grep "printf"
场景 2:分析库文件
bash
# 查看静态库中的符号
nm libmylib.a
# 查看动态库中的符号
nm -D libmylib.so
# 查看库中定义的函数
nm -g libmylib.a | grep " T "
# 查看库中的数据符号
nm -g libmylib.a | grep " D "
# 查看库的大小(通过符号大小估算)
nm -S libmylib.a | awk '{sum += strtonum("0x"$2)} END {print sum}'
场景 3:调试符号信息
bash
# 显示带行号信息的符号(需要编译时加 -g)
gcc -g -c test.c -o test.o
nm -l test.o
# 显示所有符号,包括调试符号
nm -a test.o
# 查找特定函数的符号信息
nm -C test.o | grep "my_function"
# 查看 C++ 类的成员函数
nm -C test.o | grep "MyClass::"
场景 4:比较两个版本
bash
# 比较两个对象文件的符号差异
diff <(nm -n old.o) <(nm -n new.o)
# 比较两个库文件的导出符号
diff <(nm -g libold.a) <(nm -g libnew.a)
# 检查新版本是否缺少某些符号
comm -13 <(nm -g old.o | awk '{print $3}' | sort) \
<(nm -g new.o | awk '{print $3}' | sort)
⚙️ 实用脚本示例
脚本 1:符号检查工具
bash
#!/bin/bash
# 符号检查工具
FILE="$1"
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
echo "用法: $0 <二进制文件>"
exit 1
fi
echo "=== 符号检查: $FILE ==="
echo ""
# 1. 基本符号统计
echo "1. 符号类型统计:"
nm "$FILE" | awk '
{
type = substr($2, 1, 1)
count[type]++
total++
}
END {
for (t in count) {
printf(" %s: %d (%.1f%%)\n", t, count[t], count[t]/total*100)
}
printf(" 总计: %d\n", total)
}
'
# 2. 未定义符号列表
echo -e "\n2. 未定义符号:"
UNDEFINED=$(nm -u "$FILE" | wc -l)
if [ "$UNDEFINED" -gt 0 ]; then
nm -u "$FILE" | head -20
if [ "$UNDEFINED" -gt 20 ]; then
echo " ... 还有 $((UNDEFINED - 20)) 个未显示"
fi
else
echo " 无"
fi
# 3. 全局符号列表
echo -e "\n3. 全局符号(前20个):"
nm -g "$FILE" | head -20
# 4. 按大小排序的符号
echo -e "\n4. 最大的符号(前10个):"
nm -S --size-sort "$FILE" 2>/dev/null | tail -10
# 5. 动态符号(如果是共享库或可执行文件)
if file "$FILE" | grep -q "dynamically linked"; then
echo -e "\n5. 动态符号数量:"
nm -D "$FILE" | wc -l
fi
脚本 2:库文件分析器
bash
#!/bin/bash
# 库文件分析器
LIB_FILE="$1"
if [ -z "$LIB_FILE" ] || [ ! -f "$LIB_FILE" ]; then
echo "用法: $0 <库文件>"
echo "支持: .a (静态库), .so (动态库)"
exit 1
fi
echo "=== 库文件分析: $LIB_FILE ==="
echo "文件类型: $(file "$LIB_FILE")"
echo ""
if [[ "$LIB_FILE" == *.a ]]; then
# 静态库分析
echo "1. 静态库内容:"
ar t "$LIB_FILE"
echo -e "\n2. 符号表摘要:"
nm -g "$LIB_FILE" | awk '
{
type = substr($2, 1, 1)
if (type == "T") { code++ }
else if (type == "D" || type == "B" || type == "R") { data++ }
else if (type == "U") { undef++ }
else { other++ }
total++
}
END {
printf(" 代码符号: %d\n", code)
printf(" 数据符号: %d\n", data)
printf(" 未定义符号: %d\n", undef)
printf(" 其他符号: %d\n", other)
printf(" 总计: %d\n", total)
}
'
echo -e "\n3. 按对象文件统计:"
for obj in $(ar t "$LIB_FILE"); do
if [[ "$obj" == *.o ]]; then
COUNT=$(nm -g "$LIB_FILE"("$obj") 2>/dev/null | wc -l)
echo " $obj: $COUNT 个符号"
fi
done
elif [[ "$LIB_FILE" == *.so ]]; then
# 动态库分析
echo "1. 动态符号表:"
nm -D "$LIB_FILE" | head -20
echo -e "\n2. 版本信息:"
objdump -p "$LIB_FILE" | grep -A5 "Version References:"
echo -e "\n3. 依赖库:"
ldd "$LIB_FILE" 2>/dev/null || echo " 无法获取依赖"
else
echo "错误: 不支持的文件类型"
exit 1
fi
脚本 3:符号查找工具
bash
#!/bin/bash
# 符号查找工具
PATTERN="$1"
DIR="${2:-.}"
EXTENSIONS="${3:-o,a,so}"
if [ -z "$PATTERN" ]; then
echo "用法: $0 <符号模式> [目录] [文件扩展名]"
echo "示例: $0 my_function . o,so,a"
exit 1
fi
echo "搜索符号: $PATTERN"
echo "目录: $DIR"
echo "文件扩展名: $EXTENSIONS"
echo ""
IFS=',' read -ra EXT_ARRAY <<< "$EXTENSIONS"
for ext in "${EXT_ARRAY[@]}"; do
echo "=== 搜索 .$ext 文件 ==="
find "$DIR" -name "*.$ext" -type f | while read -r file; do
# 使用 nm 查找符号
if nm "$file" 2>/dev/null | grep -q "$PATTERN"; then
echo "文件: $file"
nm "$file" 2>/dev/null | grep "$PATTERN" | while read -r line; do
echo " $line"
done
echo ""
fi
done
done
🔄 相关命令对比
| 命令 | 用途 | 特点 |
|---|---|---|
nm |
列出目标文件符号 | 专业符号分析,支持多种格式 |
objdump -t |
显示符号表 | 更详细的符号信息 |
readelf -s |
显示 ELF 文件符号表 | ELF 专用,信息最全 |
strings |
提取文件中的字符串 | 查找可打印字符串 |
ldd |
显示共享库依赖 | 动态库依赖关系 |
file |
确定文件类型 | 快速查看文件信息 |
⚠️ 重要注意事项
- 文件类型 :
nm只能用于目标文件(.o)、静态库(.a)、动态库(.so)和可执行文件。 - 调试信息 :需要编译时加
-g选项才能显示行号信息。 - C++ 符号 :使用
-C选项解码 C++ 符号,否则显示混淆后的名称。 - 动态符号 :使用
-D查看动态符号表(.dynsym 节),而不是常规符号表(.symtab 节)。 - 跨平台 :不同架构的目标文件可能需要对应的
nm版本。
🎯 最佳实践
- 调试链接错误 :使用
nm -u查找未定义符号。 - 库文件分析 :使用
nm -g查看库提供的接口。 - 版本比较:比较不同版本二进制文件的符号变化。
- 安全审计:检查二进制文件中的可疑符号。
- 性能分析:通过符号大小分析代码膨胀问题。
nm 是二进制分析和调试的重要工具,特别在解决链接问题、理解库接口和逆向工程中非常有用。掌握 nm 命令可以帮助你更好地理解程序的内部结构。