29、二进制分析工具:readelf、objdump、nm、ldd、strace的使用
说实话,搞C/C++开发这么多年,我见过太多同事在程序出问题时两眼一抹黑。链接报错、段错误、动态库找不到……这些坑我都踩过。后来我养成了一个习惯:遇到问题先不急着改代码,而是用二进制分析工具看看“尸体”长什么样。
今天这章,我就把几个压箱底的工具拿出来聊聊。它们分别是readelf、objdump、nm、ldd、strace。每个工具都有自己的绝活,配合起来用,基本能解决90%的二进制分析问题。
29.1 readelf:ELF文件的“身份证”
readelf是我个人最常用的工具之一。它专门用来解析ELF格式的文件——不管是可执行文件、目标文件(.o)还是共享库(.so),都能看得明明白白。
核心用法:
# 查看ELF文件头
readelf -h myprogram
# 查看节表(Section Headers)
readelf -S myprogram
# 查看段表(Program Headers)
readelf -l myprogram
# 查看符号表
readelf -s myprogram
# 查看重定位表
readelf -r myprogram
我记得有一次,同事编译出来的程序一运行就报“Segmentation fault”。他用gdb调了半天没找到原因。我让他跑了一下readelf -l,发现程序入口地址根本不对——链接脚本写错了。嗯,这种问题光看代码是看不出来的。
小技巧:用readelf -h可以快速确认文件是不是合法的ELF格式。如果输出乱码或者报错,那文件大概率损坏了。
29.2 objdump:反汇编和二进制内容查看
objdump是个全能选手。它不仅能看ELF结构,还能反汇编、查看二进制内容、甚至提取调试信息。
你想想看,有时候你拿到一个没有源码的库,想知道里面到底有哪些函数?objdump就是你的好帮手。
# 反汇编所有代码段
objdump -d myprogram
# 查看所有段的内容(十六进制)
objdump -s myprogram
# 查看动态符号表
objdump -T myprogram.so
# 查看重定位条目
objdump -r myprogram.o
我在项目中遇到过最经典的一个场景:一个第三方库在ARM平台上跑得好好的,换到x86上就崩。我用objdump -d反汇编了关键函数,发现里面有个内联汇编指令是ARM特有的。说白了,就是移植的时候没做平台适配。
注意:objdump反汇编出来的代码是AT&T格式的,如果你习惯Intel格式,可以加-M intel参数。我个人更习惯Intel格式,看着直观。
29.3 nm:符号表查看利器
nm这个工具名字很短,功能却很实在。它专门用来列出目标文件中的符号——包括函数名、全局变量、静态变量等。
# 列出所有符号
nm myprogram
# 只显示动态符号
nm -D myprogram.so
# 显示符号大小
nm -S myprogram
# 按地址排序
nm -n myprogram
nm输出的符号前面有个字母,表示符号类型:
| 符号类型 | 含义 |
|---|---|
| T | 代码段中的全局函数 |
| t | 代码段中的局部函数(static) |
| D | 已初始化的全局变量 |
| d | 已初始化的局部变量(static) |
| B | 未初始化的全局变量(BSS段) |
| U | 未定义的符号(需要链接时解析) |
我曾经遇到一个链接错误,报“undefined reference to xxx”。我直接用nm -u列出所有未定义符号,然后去各个库里面用nm -D搜索,很快就找到了是哪个库没链接进来。比在代码里翻来翻去快多了。
29.4 ldd:动态库依赖分析
ldd这个工具,说白了就是帮你查一个程序运行时需要加载哪些动态库。它会把所有依赖的库路径和加载地址都列出来。
# 查看动态库依赖
ldd myprogram
# 输出示例
linux-vdso.so.1 (0x00007ffe3b7f0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1a200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a1a400000)
如果某个库显示“not found”,那就说明运行时找不到这个库。常见原因有:
- 库没安装
- LD_LIBRARY_PATH没设置对
- 库版本不匹配
避坑指南:我曾经在生产环境上遇到过一个问题——程序在开发机上跑得好好的,部署到服务器上就报“cannot open shared object file”。用ldd一查,发现有个库的路径是开发机的绝对路径。后来我改用-rpath链接选项,才彻底解决。
29.5 strace:系统调用跟踪器
strace是个神器。它能跟踪程序执行过程中的所有系统调用和信号。程序为什么卡住?为什么崩溃?为什么打不开文件?strace都能告诉你答案。
# 跟踪程序执行
strace ./myprogram
# 只跟踪文件操作相关的系统调用
strace -e trace=file ./myprogram
# 跟踪网络相关调用
strace -e trace=network ./myprogram
# 统计系统调用耗时
strace -c ./myprogram
# 跟踪指定PID的进程
strace -p 12345
我记得有一次,一个后台服务每隔几分钟就卡死一次。代码看了好几遍没发现问题。我开了strace跟踪,发现它在某个时间点反复尝试连接一个已经断开的数据库连接——每次connect都超时,导致整个线程卡住。嗯,这种问题靠看代码是看不出来的,必须看运行时行为。
注意:strace会大幅降低程序运行速度(可能慢10倍以上),生产环境慎用。我一般先在测试环境复现问题,再用strace定位。
29.6 实战组合拳
这几个工具单独用已经很强大,但组合起来威力更大。我分享一个我常用的排查流程:
- 程序启动就崩?先用
strace看最后几个系统调用,通常能直接定位到问题 - 链接报错?用
nm和readelf -s查符号定义,用ldd查库依赖 - 怀疑库版本不对?用
objdump -p查看库的SONAME,用readelf -d查看动态段信息 - 段错误?先
strace看触发点,再用objdump -d反汇编关键函数
一句话总结:readelf看结构,objdump看内容,nm看符号,ldd看依赖,strace看行为。这五个工具配合起来,基本没有搞不定的二进制问题。
好了,这章的内容就到这里。这些工具我每天都在用,真心建议你花点时间熟悉它们。下次遇到奇怪的问题,别急着改代码,先拿这些工具“解剖”一下程序,说不定答案就在眼前。
公众号:蓝海资料掘金营,微信deep3321