资源描述
Linux下二进制代码的阅读
大多数时候,我们研究的是如何阅读源代码。但在一些情况下,比如源代码不公开或得到源代码的代价很高的情况下,我们又不得不需要了解程序的行为,这 时阅读二进制文件就非常重要。假设现在有一个二进制可执行文件,我们木有源代码,但要了解它的实现,这里仅简单列出一些常用的工具。 阅读方式可分为两个方面:静态阅读和动态阅读。
静态阅读
首先,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的内存内容。该命令也可用来查看任意指定地址的内存内容。
关于gdb常用的功能:
其它的一些gdb使用备忘:
一些相关资料
http://www.linuxforums.org/articles/understanding-elf-using-readelf-and-objdump_125.html
elfutils-0.148: http://www.linux.it/~rubini/docs/binfmt/binfmt.html
GNU binutils & libbfd: http://www.gnu.org/software/binutils/
参考:
Binutils是GNU工具之一,主要是二进制代码的处理维护工-文章出自LCD论坛
Binutils是GNU工具之一,主要是二进制代码的处理维护工具。其工具部件简介如下add2line:将地址转换成文件名或行号对,以便调试程序。
ar:从体系文件中创建、修改、扩展程序代码。
as:生成汇编程序代码。
c++filt:建立低级语言和用户级语言的名称符号联接,并保持它们的相互关系。
gasp:汇编宏处理器。
ld:目标代码联接,联接各目标代码块,它是生成可执行代码的最终步骤。
nm:从目标代码文件中枚举所有调试符号名。
objcopy:使用GNU BSD库,把目标代码从一文件格试拷贝成另一种格试。
objdump:显示目标文件信息。
readelf:显示elf文件信息。
ranlib:生成索引以加快对归档文件的访问。
size:列出目标模块或文件的代码尺寸。
strings:打印可打印的目标代码字符(至少4个字符),打印字符多少可以控制。对于其它格试的文件,打印字符串。
strip:放弃所有符号联接。
GNU开发工具
为了有效地进行嵌入式开发,至少需要了解和掌握如下几类工具:
编译开发工具:即能够把一个源程序编译生成一个可执行程序的软件,如gcc等。
调试工具:即能够对执行程序进行源码或汇编级调试的软件,如gdb等。
软件工程工具:用于协助多人开发或大型软件项目的管理的软件,如make、cvs等。
具体来说,我们需要对如下软件有一定了解:
(1)GCC
很多人把GCC看成只是一个C编译器,其实GCC是GNU Compiler Collection的简称,目前,GCC可以支持C、C++、ADA、Object C、JAVA、Fortran、PASCAL等多种高级语言。GCC主要包括如下一些工具。
cpp,GNU预处理器
gcc,符合ISO标准的C编译器
g++,符合ISO标准的C++编译器
gcj,gcj是GCC的java前端,可以生成执行速度更快的二进制本地执行码,而不是java byte code。gcj为把java程序编译成机器代码提供了试验性的支持。要做到这点,用户还需要安装相关的java运行时库。
gnat,是GCC的GNU ADA 95前端,该软件包括开发工具、文档及ADA 95编译器。
(2)binutils
binutils是一组二进制工具程序集,它包括addr2line、ar、as、gprof、ld、nm、objcopy、objdump、ranlib、size、strings、strip等工具,是辅助GCC的主要软件。
as,GNU汇编器(Assembler),用于把汇编代码转换成二进制代码,并存放到一个object文件中。
ld,GNU链接器(Linker),主要用于确定相对地址,把多个object文件、起始代码段、库等链接起来,并最终形成一个可执行文件。
addr2line,把执行程序中的地址映射到源文件中的对应行。
ar,创建归档文件(Archive)、修改/替换库中的object文件,向库中添加/提取object文件。
c++filt,解码C++符号名。
nm,列出object文件中的符号名。
objcopy,复制和转换object文件。
objdump,用来显示对象文件的信息。
ranlib,根据归档文件(Archive)中内容建立索引。
readelf,显示elf格式执行文件中的各种信息。
size,显示object文件和执行文件各节(Section)的大小。
strings,显示可执行文件中字符串常量。
strip,去掉执行文件中多余的信息(如调试信息),可以减少执行文件的大小。
gprof,用来显示调用图表档案数据。
(3)gdb
gdb是GNU调试器,它允许调试用C、C++或其它语言编写的程序。它的基本运行方式是在一个shell环境下用命令行方式调试程序和显示数据。如果加上一些图形前端(如DDD等软件),则可以在此一个更方便的图形环境下调试程序。
(4)make
GNU make是一个用来控制可执行程序的生成过程,从其它的源码程序文件中生成可执行程序的GNU工具。GNU make允许用户生成和安装软件包,而无需了解生成、安装软件包的具体执行过程。
(5)diff/diff3/sdiff
diff/diff3/sdiff是比较文本差异的工具,也可以用来产生补丁。
(6)patch
patch是补丁安装程序,可根据diff生成的补丁来更新程序。
(7)CVS(Concurrent Version System)
CVS是一个版本控制系统。它能够记录文件的修改历史(通常但并不总是包括源码)。CVS只存储版本之间的区别,而不是你创建的文件的每一个版本。CVS还保留一个记录改变者、改变时间以及改变原因的日志。CVS对于管理发行版本和控制在多个作者间同时编辑源码文件很有帮助。CVS为一个层次化的目录提供版本控制,目录由修改控制的文件组成,而不是在一个目录中为一组文件提供版本控制。这些目录和文件可以被合并起来构成一个软件发行版本。
binutils是一组二进制工具程序集,它包括addr2line、ar、as、gprof、ld、nm、objcopy、objdump、ranlib、size、strings、strip等工具,下面分别介绍其中一些常用的软件。
nm工具:
nm软件主要功能是列出目标文件中的符号,这样可以帮助程序员定位和分析执行程序和目标文件中的符号信息和它的属性。如果没有目标文件作为参数被列出,nm假定目标文件为”a.out”,通过下面的命令可以得到nm的一般帮助信息#nm -h。
对于每一个符号,nm将显示下面的内容:
符号的值:用某种基数表示的数值。默认情况下用十六进制表示。
符号的类型:至少要用到下面的类型。也可以用其它类型,这依赖于目标文件的格式。
1. A:符号的值是绝对值,并且不会被将来的链接所改变。
2. B:符号位于未初始化数据部分(被认为是BSS)。
3. C:符号是公共的。公共符号是未初始化的数据。在链接时,多个公共符号可能以相同的名字出现。如果符号在其他地方被定义,该符号会被当作未定义的引用来处理。
4. D:符号位于已初始化数据部分。
5. G:符号位于小型对象的已初始化数据部分,相对于大型的全局array,一些对象文件格式允许对小型数据对象更有效的存取,例如全局的int型变量。
6. I:符号是另一个符号的间接引用。这是对很少用到的a.out目标文件格式的GNU扩展。
7. N:符号是调试符号。
8. R:符号位于只读数据部分。
9. S:符号位于小型对象的未初始化数据部分。
10. T:符号位于文本(代码)部分。
11. U:符号未被定义。
12. W:符号是弱定义符号(Weak Symbol),也称弱符号。当一个弱定义符号和一个已经定义的普通符号链接时,使用该已定义的普通符号不会引起错误。当一个未定义的弱符号被链接且该符号未被定义时,该weak符号的值被无错误的变为0。
13. -:符号是一个a.out目标文件中的stabs符号。这种情况下,接下来要打印的值是stabs other域,stabs desc域,以及stab类型。stabs符号用于保留调试信息。
14. ?:符号类型是未知的,或目标文件格式特殊。
15. o:符号名
nm的命令行参数选项的长格式和短格式是等价的,下面列出了可供选择的命令行参数格式。
1. -A/-o/--print-file-name: 在找到的各个符号的名字前加上输入文件名(或归档文件元素),而不是在此文件中的所有符号前只出现一次输入文件名。
2. -a/--debug-syms:显示所有的调试符号,即使是仅用于调试的符号。通常情况下这些符号是不被列出的。
3. -B:等价于“--format=bsd”(为了兼容MIPS nm)。
4. -C/--demangle:将低级符号名解码成用户级名字。另外去除任何由系统预先生成的初始的下划线,这样可以使C++函数名具有可读性。
5. --no-demangle:不解码低级符号名。这是默认的处理方式。
6. -D/--dynamic:显示动态符号而不是普通符号。该选项仅对动态目标文件有意义,例如特定类型的共享库。
7. -f format/--format = format:使用format格式输出,format可以是bsd、sysv或posix。默认为bsd。仅当format的第一个字符是有意义的,可以是大写或小写。
8. -g/--extern-only:只显示外部符号。
9. -l/--line-numbers:对每个符号,使用调试信息去试图找到文件名和行号。对于已定义的符号,查找符号地址的等号。对于未定义符号,查找指向符号重定位入口的行号。如果可以找到行号信息,在其他符号信息之后显示行号信息。
10. -n/-v/--numeric-sort:按符号对应地址的顺序排序,而不是按符号名的字符顺序。
11. -p/--no-sort:不以任何顺序对符号进行排序,按目标文件中遇到的符号顺序显示。
12. -P/--portablility:使用POSIX.2标准输出格式代替默认的输出格式。等价于“-f posix”。
13. -s/--print-armap:当列出归档文件中成员的符号时,包含索引,即名字和包含该名字定义的模块的映射(通过ar或ranlib保存在归档文件中)。
14. -r/--reverse-sort:反转排序的顺序(按数字或字母顺序)显示。
15. --size-sort:按大小排列符号顺序。该大小是按照一个符号的值与它下一个符号的值进行计算的。
16. -t radix/--radix = radix:使用radix进制显示符号值。radix只能为“d”表示十进制、“o”表示八进制或“x”表示十六进制。
17. --target = bfdname:指定一个目标代码的格式,而非使用系统默认格式。
18. -u/--undefined-only:仅显示没有定义的符号(那些每个目标文件的外部符号)。
19. -defined-only:仅显示每个目标文件中已经定义的符号。
20. -V/--version:显示nm的版本号然后退出。
21. --help:显示nm的所有选项然后退出。
下面介绍一个简单的使用,按照我们刚才介绍ar命令时的例子,执行如下命令:
#nm test.o
输出结果如下:
U Add
00000000 T main
U Minus
U printf
执行命令:
#nm add.o
输出结果如下:
00000000 T Add
执行命令:
#nm minus.o
输出结果如下:
00000000 T Minus
命令nm test.o的输出说明了test.o定义了main函数,但没有定义Add、Minus和printf函数符号。命令nm add.o的输出说明add.o定义了Add函数符号。命令nm minus.o的输出说明了minus.o定义了Minus函数符号。test.o中没有定义但使用了printf函数符号,printf函数实际上定义在libc.a库中。
objdump工具:
objdump显示一个或多个目标文件的信息。由其选项来控制显示哪些特定的信息。这些信息只对那些编写编译工具的程序员有帮助,而对那些只想让自己编写的程序编译和运行起来的程序员来说没有更多意义。但在嵌入式系统级开发时,通过这个软件可以方便地查看执行文件或库文件的信息。如我们可以通过objdump软件反汇编执行程序,看到执行程序的汇编格式。
当目标文件是归档文件时,objdump显示的是归档文件中每个成员文件的信息。选项的长格式和短格式是等价的。下面描述了作为可选择的参数格式(除“-l”之外,至少要给出一个参数选项)。
1. -a/--archive-header:如果任何一个由object-file指定的文件是归档文件,则显示该归档文件的头信息(类似于“ls -l”的格式)。除了“ar -tv”能显示的信息外,“objdump -a”还可以显示每个归档文件成员的目标文件格式。
2. --adjust-vma=offset:当转储信息时,首先给所有的节地址加上一个偏移量offset。如果节地址对应不上符号表时,就可以使用该选项。当节被放在特殊的地址,而采用的是一种不能表示节地址的格式,例如a.out,就会发生节地址对应不上符号表的情况。
3. -b bfdname/--target = bfdname:指定目标文件的目标代码格式为bfdname。该选项可能不是必需的,因为objdump能够识别很多格式。如命令“objdump -b oasys -m vax -h fu.o”执行后,将显示节头中的概要信息。“-b oasys”表示用的是Oasys编译器产生的目标文件格式。“-m vax”表示目标文件是VAX计算机上的目标文件。
4. -C/--demangle:将低级符号名解码成用户级名字。另外去除任何由系统预先生成的初始的下划线,这样可以使得C++函数名具有可读性。
5. --debugging:显示调试信息。该选项试图解析保存在文件中的调试信息并且用C语言风格的语法打印这些信息。仅能对特定类型的调试信息实现这个功能。
6. -d/--disassemble:显示目标文件中的机器指令使用的汇编语言。该选项仅仅反汇编那些应该含有指令机器码的节。
7. -D/--disassemble-all:类似于“-d”,但是反汇编所有节的内容。该选项仅对那些应该含有指令机器码的节有意义。
8. --prefix-addresses:反汇编时,打印每行的完整地址。这是一种更古老的反汇编格式。
9. --disassemble-zeroes:通常情况下,反汇编的输出会跳过大块的零。该选项将指引反汇编器去反汇编那些大块的零,就像处理其他数据一样。
10. -EB/-EL/--endian={big|little}:指定目标文件的字节顺序。该选项只会影响反汇编。当反汇编像S-records这样没有描述字节顺序的文件格式时,该选项是有用的。
11. -f/--file-header:显示每个由object-file指定的所有目标文件的文件头概要信息。
12. -h/--section-header/--header:显示目标文件的节头概要信息。ld使用了“-Ttext”、“-Tdata”或“-Tbss”这些选项时,可能会使得目标文件的各个节被重定向到非标准的地址。这样通过这个选项,可以查出目标文件的各节的起始地址。然而,一些像a.out这样的目标文件格式并没有存储文件段的起始地址。在这些情况下,尽管ld正确地重定位了每个节。但是使用“objdump -h”列出文件节头时,并不能显示其正确地址。
13. --help:显示objdump的所有选项的概要信息然后退出。
14. -i/--info:列表显示所有对“-b”或“-m”可用的体系结构和目标格式。
15. -j name/--section=name:只显示由name指定的节。
16. -l/--line-numbers:用对应于目标代码的文件名和行号来标注要显示的信息(使用调试信息),仅仅和-d、-D或-r一起使用才有效。
17. -m machine/--architecture = machine:当反汇编由object-file指定的目标文件时,标明所使用的体系结构。当反汇编一个像S-records这样本身并没有描述体系结构信息的文件的时候,这个选项是有用的。可以用-i选项列出可用的体系结构。
18. -p/--private-headers:显示目标文件格式的特定信息。要显示的信息依赖于目标文件的格式。对于某些目标文件格式,没有附加的信息可供显示。
19. -r/--reloc:显示文件的重定位入口。如果和“-d”或者“-D”一起使用,重定位部分以反汇编后的格式显示出来。
20. -R/-dynamic-reloc:显示文件的动态重定位入口,仅仅对于动态目标文件有意义,例如特定类型的共享库。
21. -s/--full-contents:显示任何指定节的全部内容。
22. -S/--source:尽可能显示与反汇编混和的源代码。隐含了“-d”参数。
23. --show-raw-insn:反汇编机器指令的时候,用十六进制和符号形式同时显示机器指令码。然而并非所有的目标都能这样正确地出来。
24. --no-show-raw-insn:反汇编机器指令的时候,不显示指令类型。这是指定--prefix-addresses选项时的默认设置。
25. --stabs:显示任何指定节的全部内容。显示ELF格式目标文件中的.stab、.stab.index和.stab.excl节的内容。一般用于Solaris操作系统。在其他大部分执行文件格式中,调试符号表入口与链接符号交织在一起,并在打开了“--yms”参数选项时,在objdump的输出中是可见的。
26. --start-address = address:从指定地址开始显示数据,该选项影响打开-d、-r和-s选项时的输出。
27. --stop-address = address:显示数据直到指定地址为止,该选项影响打开-d、-r和-s选项时的输出。
28. -t/--syms:显示文件中的符号表入口。类似于“nm”程序提供的信息。
29. -T/--dynamic-syms:显示文件中的动态符号表入口。仅仅对于动态目标文件有意义,例如特定类型的共享库。类似于打开了“-D”选项的“nm”程序提供的信息。
30. --version:显示objdump的版本号然后退出。
31. -x/--all-header:显示所有可用的头信息,包括符号表和重定位入口,使用“-x”等价于指定了“-a -f -h -r -t”参数。
32. -w/--wide:对超过80列的输出设备指定一些行的格式。
对于刚才生成的test执行文件,我们执行
$objdump -f test
显示执行文件文件头概要信息。
使用“-d”或“-D”参数反汇编生成的目标代码:
$objdump -d add.o
readelf工具:
readelf软件显示一个或多个ELF格式的目标文件信息。可通过各种参数选项来控制readelf软件显示目标文件中的特定信息。
size工具: List section sizes and total size
size显示一个目标文件或者链接库文件中的目标文件的各个段的大小。
1、输出格式
size有两种输出格式,一种为"sysv",另一种为"berkeley",默认为berkeley的格式。第一种格式可以用"-A"或者"--format=sysv"指定,第二种格式用"-B"或"--format=berkeley"指定
2、数字输出格式
有三种格式,octal, decimal及hex,对应的参数为"-o",
"-d"及"-x",也可以用"--radix=8","--radix=10"及"--radix=16"指定
3、汇总多个文件的各个段合计长度
"-t" 或者"--total",合计值将在最后输出。
ar工具: Create, modify, and extract from archives
ar用来管理一种文档。这种文档中可以包含多个其他任意类别的文件。这些被包含的文件叫做这个文档的成员。ar用来向这种文档中添加、删除、解出成员。成员的原有属性(权限、属主、日期等)不会丢失。
实际上通常只有在开发中的目标连接库是这种格式的,所以尽管不是,我们基本可以认为ar是用来操作这种目标链接库(.a文件)的。
1、创建库文件
我不知道怎么创建一个空的库文件。好在这个功能好像不是很需要。通常人们使用“ar cru liba.a a.o"这样的命令来创建一个库并把a.o添加进去。"c"关键字告诉ar需要创建一个新库文件,如果没有指定这个标志则ar会创建一个文件,同时会给出一个提示信息,"u"用来告诉ar如果a.o比库中的同名成员要新,则用新的a.o替换原来的。但是我发现这个参数也是可有可无的,可能是不同版本的ar行为不一样吧。实际上用"ar -r liba.a a.o"在freebsd5上面始终可以成功。
2、加入新成员
使用"ar -r liba.a b.o"即可以将b.o加入到liba.a中。默认的加入方式为append,即加在库的末尾。"r"关键字可以有三个修饰符"a", "b"和"i"。
"a"表示after,即将新成员加在指定成员之后。例如"ar -ra a.c liba.a b.c"表示将b.c加入liba.a并放在已有成员a.c之后;
"b"表示before,即将新成员加在指定成员之前。例如"ar -rb a.c liba.a b.c";
"i"表示insert,跟"b"作用相同。
3、列出库中已有成员
"ar -t liba.a"即可。如果加上"v"修饰符则会一并列出成员的日期等属性。
4、删除库中成员
"ar -d liba.a a.c"表示从库中删除a.c成员。如果库中没有这个成员ar也不会给出提示。如果需要列出被删除的成员或者成员不存在的信息,就加上"v"修饰符。
5、从库中解出成员
"ar -x liba.a b.c"
6、调整库中成员的顺序
使用"m"关键字。与"r"关键字一样,它也有3个修饰符"a","b", "i"。如果要将b.c移动到a.c之前,则使用"ar -mb a.c liba.a b.c"
展开阅读全文