大多数时候,我们研究的是如何阅读源代码。但在一些情况下,比如源代码不公开或得到源代码的代价很高的情况下,我们又不得不需要了解程序的行为,这时阅读二进制文件就非常重要。假设现在有一个二进制可执行文件,我们木有源代码,但要了解它的实现,这里仅简单列出一些常用的工具。阅读方式可分为两个方面:静态阅读和动态阅读。
静态阅读
首先,file命令可以查看可执行文件的大体信息。比如是哪种格式的,哪个体系结构的,有没有调试信息等。这些决定了需要用哪个版本的工具进行进一 步查看。比如如果是arm体系的可执行文件就可用arm toolchain里的工具,如arm-eabi-objdump,arm-eabi-gdb等。
众所周知,GNU Binutils提供了一系列解析和操作二进制文件的工具,最常用的如objdump,其最主要的功能之一是反汇编:
objdump -d libxxx.so
其它常用选项包括:
-x 打印所有头信息
-s 打印所有段的内容
-S 将反汇编和源代码一起打印
-r 打印重定位表
-R 打印动态重定位表
-D 打印所有段,即不止代码段
-t 打印符号表,和nm的功能类似,但格式不一样
-T 只打印动态链接的符号表项
等等。
readelf命令主要用于查看elf文件的元信息(如段信息等)。如查看可执行文件的头信息:
readelf -h libxxx.so
其它常用选项包括:
-s 打印符号表
-r 打印重定位表
-l 打印装载属性
-S 打印段表
等等。
除了以上两个用得最多的,其它一些辅助工具有时也必不可少:
nm 打印符号表
c++filt 把c++符号unmangle
strip 把调试信息删除
strings 看文件中的字符串
ldd: 查看依赖关系
等等
动态阅读
要了解可程序的行为,最可靠的还是看二进制文件跑起来时的样子。很多时候,由于运行环境复杂,特别是多线程情况下,动态阅读能发现很多静态阅读所不可能发现的东西。
首先,在/proc/pid(pid为进程号)下记录了二进制文件跑起来后的很多信息,如通过/proc/pid/maps可以知道该程序链接了哪些库, 分别被加载在了什么地方(这样,知道了错误地址,理论上就可以找到相应库的对应反汇编代码。),/proc/pid/mem是进程所用的内存镜像, /proc/pid/stat则记录了中断统计信息等。/proc/pid/cmdline则记录了启动程序的命令行。
其次,通过trace(strace,lstrace)可以看程序有哪些系统调用。strace用于跟踪系统调用,ltrace用于跟踪动态库调用。
如对于在/proc/pid/maps中看到的一些匿名内存映射,想要看它们是在什么时候或是在哪被创建可以用:
strace -f -p 3742 -e trace=mmap2,open,mprotect |tee tmp.trace
其中3742为进程ID。这里只过滤出关于内存映射的函数。
最后,指令级动态阅读的神器还属gdb。
在gdb中查看寄存器信息可用:
(gdb) i r
在调试过程中打印出反汇编代码可用
(gdb) disass addr
或者
(gdb) x/10i addr
然后就可以和objdump出来的反汇编结合着看了。如GOT表之类的信息在静态阅读时是无意义的,只有这时才能看。
各种break point能让我们迅速定位到我们关注的地方:
watch point 在指定数据被访问时程序停止。
break point 在指定代码被执行时程序停止。 因为很多时候库不带symbol,所以得直接设在地址上 break *addr。
catch point 在指定事件发生时程序停止,如进程创建,动态链接库加载及异常。
条件断点有时很有用:break ... if <condition>,但这玩意一旦设上不是一般的慢啊。。。
gdb中的bt和info frame命令常用来看堆栈信息和函数调用关系。但有时因为bug栈被写坏了,但如果不是被写得很坏,查看栈的内容常通常能找到一些线索:
(gdb) x/40x $sp
该命令是查看$sp指向区域的40个byte的内存内容。该命令也可用来查看任意指定地址的内存内容。
成为一名更好的程序员:如何阅读源代码:http://www.linuxdiyf.com/linux/16113.html
阅读Docker源代码的神兵利器:http://www.linuxdiyf.com/linux/14512.html
打造阅读Linux源代码利器:http://www.linuxdiyf.com/linux/13607.html