深入linux c/c++调试工具之nm(一眼看穿符号)
目录一、前言——nm 工具简介1.1 什么是 nm1.2 与相关工具对比二、使用场景——何时使用 nm1、链接报错 undefined reference2、函数存在但运行时找不到3、对比 strip 前后 / 发布版与调试版4、内核模块 / 驱动开发5、C 符号看不懂mangled name6、查静态库 .a 里有没有某个符号三、核心原理——工作原理深度解析3.1 ELF 符号表从哪来3.2 地址含义3.3 静态符号 vs 动态符号3.4 与 strip 的关系四、命令参数——常用选项详解4.1 基本用法4.2 常用选项4.3 按符号类型过滤BSD 风格4.4 输出格式4.5 组合示例五、使用实战——实际案例分析六、常见问题——疑难解答七、总结——要点回顾一、前言——nm 工具简介1.1 什么是 nmnmname list是 GNU Binutils 中的 符号表查看工具用于列出 目标文件.o、静态库.a、可执行文件、内核模块.ko 中的 符号名称、类型、地址 等信息。一句话定位nm 是 C/C 二进制文件的「符号 X 光」—— 不用运行程序就能看清里面有哪些函数、变量以及它们是否在符号表中。1.2 与相关工具对比工具作用典型场景nm查看符号表函数是否存在、是否被 strip、模块导出符号objdump反汇编 段 符号看汇编、段布局、更详细readelf解析 ELF 结构段头、动态链接、重定位ldd查看动态库依赖运行时 .so 依赖cfiltC 符号 demangle还原_ZN...可读名nm 优势 轻量、输出简洁、脚本友好是驱动/内核模块/逆向排查的 第一反应工具。二、使用场景——何时使用 nm1、链接报错 undefined reference典型现象undefined reference to my_initnm 价值看你的.o里有没有my_init是T还是U。nm my_module.o | grep my_init2、函数存在但运行时找不到典型现象dlsym返回 NULL或内核模块 insmod 后/proc/kallsyms无符号。nm -D ./libfoo.so | grep foo_init # 动态导出 nm ./driver.ko | grep probe # 内核模块3、对比 strip 前后 / 发布版与调试版nm ./app.debug | grep main strip ./app.debug -o ./app.release nm ./app.release | grep main # 可能无输出4、内核模块 / 驱动开发nm driver.ko | grep cyg_profile nm driver.ko | grep __init objdump -t driver.ko | grep probe # 与 nm 互补5、C 符号看不懂mangled namenm ./app | grep MyClass nm -C ./app # demangle显示可读 C 名6、查静态库 .a 里有没有某个符号nm libxxx.a | grep target_func # 或 ar t libxxx.a | xargs -I{} sh -c echo {} ; nm {} | grep target_func三、核心原理——工作原理深度解析3.1 ELF 符号表从哪来编译 C/C 时编译器在.o中生成符号表symtab和字符串表strtab源码: int g_val 1; void foo() { ... } ↓ 编译器 gcc ↓ .o 文件 ├── .text (foo 机器码) ├── .data (g_val) └── 符号表: foo → T, g_val → D ↓ 链接器 ld ↓ 可执行文件 / .so / .ko └── 合并符号表解析 U → 其他 .o 的 Tnm 做的事读取 ELF 的符号表节格式化输出不运行程序。3.2 地址含义文件类型nm 显示的地址可执行文件已链接虚拟地址VMA.o 重定位文件often 相对偏移未最终链接.ko 内核模块常为链接前偏移 0或需结合 load 地址因此.ko里两个符号都是0000000000000000 T不一定表示都在 NULL可能是未重定位的 ET_REL见 ASan 排查一文。3.3 静态符号 vs 动态符号表nm 选项说明静态符号表.symtab默认nm含 static/local 符号动态符号表.dynsymnm -D仅动态链接可见符号导出/导入nm ./app # 全部符号含 static nm -D ./app # 仅 dynamic类似 ldd/PLT 关心的3.4 与 strip 的关系strip会删除.symtab / .strtab调试符号表但通常保留 .dynsym动态链接必需。file ./app # not stripped → nm 信息多 # stripped → nm 只剩 dynamic 或很少保留调试信息用strip --strip-debug而非全 strip或分离 debugobjcopy --only-keep-debug。四、命令参数——常用选项详解4.1 基本用法nm [选项] 文件...nm ./a.out nm driver.o driver.ko nm libfoo.a4.2 常用选项参数说明-a,--debug-syms显示仅用于调试器的符号-A,--print-file-name在每个符号前打印输入文件名-B等同于--formatbsd-C,--demangle[STYLE]将底层符号名解码为用户可读名称若指定 STYLE可为auto默认、gnu、lucid、arm、hp、edg、gnu-v3、java或gnat-C --no-demangle不对底层符号名进行 demangle 解码-D,--dynamic显示动态符号表而非普通符号表-D,--defined-only仅显示已定义的符号-f,--formatFORMAT使用指定输出格式 FORMAT。FORMAT 可为bsd、sysv或posix默认为bsd-g,--extern-only仅显示外部全局符号-l,--line-numbers利用调试信息为每个符号查找对应的源文件名和行号-n,--numeric-sort按地址数值排序符号-o等同于-A-p,--no-sort不对符号进行排序-P,--portability等同于--formatposix-r,--reverse-sort反转排序顺序-r, --plugin NAME加载指定插件-S,--print-size打印已定义符号的大小-s,--print-armap对来自归档库.a成员的符号一并显示索引信息-s,--size-sort按符号大小排序-s,--special-syms在输出中包含特殊符号-s,--synthetic同时显示合成符号synthetic symbols-t,--radixRADIX指定打印符号值时使用的进制RADIX-t,--targetBFDNAME将目标对象格式指定为 BFDNAME-u,--undefined-only仅显示未定义的符号4.3 按符号类型过滤BSD 风格nm -f posix ./app # 标准格式 # 部分系统支持: # nm -u file 仅 undefined # nm --extern-only 同 -g4.4 输出格式SysV 格式默认Linux 常见0000000000401136 T main 0000000000601040 D g_initialized U printfGLIBC_2.2.5字段含义地址符号值/地址类型T/D/B/U/...名称符号名POSIX 格式nm -f posix ./app # name value type size4.5 组合示例# C 可读 行号 nm -Cl ./cpp_app | grep MyClass # 只看未定义符号链接缺什么 nm -u *.o # 内核模块里找钩子 nm driver.ko | grep -E probe|init|exit # 按地址排序看布局 nm -n ./app | tail -20 # 多库批量查符号 nm -A -g *.a | grep foo_bar五、使用实战——实际案例分析示例代码nm_demo.c#include stdio.h /* 全局已初始化 → D (.data) */ int g_initialized 42; /* 全局未初始化 → B (.bss) */ int g_uninitialized; /* 全局只读 → R (.rodata) */ const char g_message[] hello nm; /* 静态全局 → 小写/local 符号 */ static int s_hidden_counter; /* 普通全局函数 → T/t */ void public_func(void) { printf(public_func: %d\n, g_initialized); } /* static 函数 → 通常 t (local text) */ static void static_func(void) { s_hidden_counter; printf(static_func: %d\n, s_hidden_counter); } /* 未使用函数 → 仍可能有符号视链接器/编译选项 */ static void unused_func(void) { } int main(void) { public_func(); static_func(); return 0; }Makefile:CC ? gcc CFLAGS ? -g -O0 -Wall TARGET : nm_demo .PHONY: all strip-demo nm-demo nm-all help all: $(TARGET) $(TARGET): nm_demo.c $(CC) $(CFLAGS) -o $ $ nm-demo: $(TARGET) echo nm $(TARGET) nm $(TARGET) echo echo nm -C (demangle) nm -C $(TARGET) 2/dev/null || nm $(TARGET) echo echo nm -l (with line numbers, needs -g) nm -l $(TARGET) | head -20 strip-demo: $(TARGET) cp $(TARGET) $(TARGET).stripped strip $(TARGET).stripped echo after strip nm $(TARGET).stripped nm-all: nm-demo strip-demo help: echo make make nm-demo查看可执行文件全局/局部符号nm nm_demo00000000000038c r __abi_tag 0000000000004014 B __bss_start 0000000000004014 b completed.0 w __cxa_finalizeGLIBC_2.2.5 0000000000004000 D __data_start 0000000000004000 W data_start 0000000000001090 t deregister_tm_clones 0000000000001100 t __do_global_dtors_aux 0000000000003dc0 d __do_global_dtors_aux_fini_array_entry 0000000000004008 D __dso_handle 0000000000003dc8 d _DYNAMIC 0000000000004014 D _edata 0000000000004020 B _end 00000000000011cc T _fini 0000000000001140 t frame_dummy 0000000000003db8 d __frame_dummy_init_array_entry 0000000000002188 r __FRAME_END__ 0000000000004010 D g_initialized 0000000000003fb8 d _GLOBAL_OFFSET_TABLE_ 0000000000002008 R g_message w __gmon_start__ 0000000000002034 r __GNU_EH_FRAME_HDR 0000000000004018 B g_uninitialized 0000000000001000 T _init 0000000000002000 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable U __libc_start_mainGLIBC_2.34 00000000000011b1 T main U printfGLIBC_2.2.5 0000000000001149 T public_func 00000000000010c0 t register_tm_clones 000000000000401c b s_hidden_counter 0000000000001060 T _start 0000000000001170 t static_func 0000000000004018 D __TMC_END__ 00000000000011a6 t unused_func符号类型对应源码g_initializedD已初始化全局g_uninitializedBBSS 未初始化g_messageRconst 只读s_hidden_counterdstatic 全局static_functstatic 函数public_funcT全局函数printfUlibc 提供链接时解析六、常见问题——疑难解答问题原因解决nm 无输出 / 很少文件被strip用未 strip 构建file查看地址全是 0.o / .ko未重定位正常load 后再看 kallsyms看不到 static 函数用了nm -g去掉-gC 名乱码name manglingnm -C无行号未-g编译gcc -g后用nm -lno symbols非 ELF 或 strip 干净file、readelf -h与 gdb 函数名不一致内联/优化/thumb对照nm -l与objdump -d两个 T 同地址 0重定位 .ko用objdump -dr看 call 目标七、总结——要点回顾nm 读的是 ELF 符号表列出函数/变量的名字、类型、地址无需运行程序。大写全局小写局部T 代码 D 数据 B BSS U 未定义—— 记牢这五个就够用大部分场景。与objdump/readelf/gdb配合nm 做快速符号存在性检查链接/驱动/模块问题第一反应。