使用binutils工具分析目标文件(壹)
目录
1.前言
2.目标文件的格式
3.安装binutils工具
4.样例代码
5.分析目标文件
6.结合objdump输出信息分析段的分布
7.结合objdump分析text代码段
8.结合objdump分析数据段和只读数据段
9.结合objdump分析bss段
前言
本篇博客主要是介绍使用binutils工具中的objdump来分析生成的目标文件,也会介绍一些关于不同平台目标文件的知识。
目标文件的格式
现在PC端流行的可执行文件格式分别是Windows下的PE格式和Linux中的ELF格式,但是这两种格式都是COFF格式的变种。而目标文件其实就是源代码经过汇编阶段但是没有进行链接的中间文件(预编译,编译,汇编,链接),其与可执行文件的内容和结构相似,所以一般跟可执行文件格式一起采用一种格式存储。这其中包含了可执行文件(Windows中的exe和Linux下的ELF),动态链接库(Windows中的.dll和Linux下的.so)和静态链接库(Windows中的.lib和Linux下的.a)文件都按照可执行文件格式进行存储。具体的ELF文件类型如下:
ELF文件类型 | 说明 | 示例 |
可重定位文件 | 该类文件包含了代码和数据,可以链接成可执行文件或共享目标文件,其中静态链接库也可归为该类 | Linux中的.o和Windows中的.obj |
可执行文件 | 该类文件包含了可以直接执行的程序,一般都没有扩展名 | /bin/bash文件和Windows的.exe |
共享目标文件 | 该类文件包含了代码和数据,链接器可使用该类文件跟其他的可重定位文件和共享目标文件链接生成新的目标文件,也可以通过动态链接器将几个共享目标文件与可执行文件结合生成进程映像来运行 | Linux的.so和Windows的.dll |
核心转储文件 | 当进程意外终止(崩溃或卡死)时,系统可以将该进程的地址空间的内容以及终止时的一些信息存储到核心转储文件中 | Linux中的core dump和Windows中的.dmp文件 |
表1.ELF格式的文件分类
安装binutils工具
MinGW - 适用于 Windows 的极简 GNUhttps://sourceforge.net/projects/mingw/files/MinGW/Base/binutils/binutils-2.28/
点击该链接,跳转至sourceforge网站进行下载,如图选择该压缩包下载后解压即可
图1.下载binutils工具
在下载该压缩包以后,在解压后的文件夹下的bin目录中可以找到objdump和readelf两种工具。这两种工具的功能不同,本篇文章重点讲解objdump,其次会介绍一些readelf(在Windows下使用readelf输出信息,可能会因为ELF文件的魔术字不同导致指令运行失败。后续将详细讲解,此处留意即可)。当然,你也可以通过电脑->高级设置->修改path路径,添加objdump工具的路径至path中,这样的话可以直接在命令提示符窗口使用。
图2.objdump和readelf工具
样例代码
为了更好的结合样例分析,本篇博客使用的C语言代码如下(读者需要自行下载GCC编译工具来编译代码):
int printf(const char* format, ...);int global_inti_var = 84;
int global_uninit_var;void func1(int i){printf("%d\n",i);
}int main(void){static int static_var = 85;static int static_var2;int a = 1;int b;func1(static_var + static_var2 + a + b);return a;
}
分析目标文件
在分析目标文件之前,我们需要使用GCC编译工具将.C文件执行到汇编阶段,而不需要进行链接阶段生成可执行文件(预编译,编译,汇编,链接。这四个阶段需要牢记,防止混淆)。
gcc -c test1.c
//此处作者的.c文件命名为test1,读者自行修改
此时在代码文件夹下会生成一个.o为后缀的目标文件,这时我们就可以使用objdump工具来分析目标文件了,具体的指令如下:
objdump -h test1.c
这条命令的作用是输出目标文件的段信息(简短的),在执行该命令以后会输出如下图中的信息,具体的输出信息的每一列代表的含义作者也在土著标注清楚了,后续也会再对其进行讲解。
图3.objdump输出信息分析
在图3左侧标记中可以看到目标文件有很多段,每一个段的作用都不同,用于存储不同的信息,例如text段主要存储代码,而data段主要存储已初始化的全局或静态变量的信息。具体的每一个段代表的信息如下表:
段名 | 含义 |
text | 可执行代码存储段 |
data | 已初始化的全局和静态变量存储段 |
bss | 未初始化的全局和静态变量存储段 |
drectve | 链接器指令存储段 |
rdata | 只读数据存储段 |
xdata | 异常处理信息存储段 |
pdata | 异常处理表存储段 |
表1.段信息
这里再说明一下为什么要分段?主要原因是分段存储信息,需要那一部分的数据就访问哪一个段,例如使用static声明的对象,假如该对象被初始化了则会存储到data段,未初始化则会存储到bss段(这个bss段只是为对象预留一个为未定义的全局变量符号,等到链接阶段再去分配空间。并且是一个初始化为0的全局对象否存储在data段也有区别,因为有些编译器会将初始化为0的对象认为是未初始化的(编译器优化,类似于未初始化的值赋值为0),此时这个对象就会存储到bss段)
此外在每一个段的第二行都有不同的标志位,这些标志位代表的是各个段的属性,具体含义如下:
属性 | 含义 |
CONTENTS | 代表该段在文件中包含实际数据(即占用物理存储空间) |
ALLOC | 代表该段在程序运行时需分配内存空间 |
LOAD | 代表该段需从文件加载到内存才能执行 |
RELOC | 代表该段包含重定位信息(需链接器修正地址) |
READONLY | 代表该段在内存中为只读,不可修改 |
CODE | 该段包含可执行机器指令 |
结合objdump输出信息分析段的分布
根据图3所输出的File off列的信息中,我们可以具体的分析出各个段在目标文件中的分布情况,具体如图所示:
图4.目标文件中段分布
在图3中你可以发现txt段的偏移量是0x00000154,而在0x00000000到0x00000154这片空间主要是存储了目标文件的头信息,后续小节将会对这里的内容进行讲解,此处先了解即可。我们还可以从输出的信息中发现data段的偏移量是0x00000164,这里的0x00000154到0x00000164的地址就是存储text段的地址,总大小为0x00000060,也就是我们看到的Size列输出的信息。对此我们也可以通过objdump指令输出各个段的大小,指令如下:
siez test1.o
执行该段命令以后,输出的信息如下:
图5.各个段的大小
此时你会发现text的大小是224,而图3中显示的text的大小为74(0x00000060),这是因为text的大小包含了text段,rdata段,xdata段,pdata段和rdata$zzz段的大小
结合objdump分析text代码段
分析各个段的内容都离不开objdump这个工具,而objdump中的“-s”参数可以将所有段的内容以十六进制的方式打印出来,而“-d”参数可以将所有包含指令的段进行反汇编,具体的指令如下:
objdump -s -d test1.o
在执行该命令后,我们会在命令提示框中见到很多输信息,而我们这小节只针对以下信息进行分析:
Contents of section .text:0000 554889e5 4883ec20 894d108b 5510488d UH..H.. .M..U.H.0010 0d000000 00e80000 00009048 83c4205d ...........H.. ]0020 c3554889 e54883ec 30e80000 0000c745 .UH..H..0......E0030 fc010000 008b1504 0000008b 05000000 ................0040 0001c28b 45fc01c2 8b45f801 d089c1e8 ....E....E......0050 acffffff 8b45fc48 83c4305d c3909090 .....E.H..0]....Disassembly of section .text:0000000000000000 <func1>:0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbp4: 48 83 ec 20 sub $0x20,%rsp8: 89 4d 10 mov %ecx,0x10(%rbp)b: 8b 55 10 mov 0x10(%rbp),%edxe: 48 8d 0d 00 00 00 00 lea 0x0(%rip),%rcx # 15 <func1+0x15>11: R_X86_64_PC32 .rdata15: e8 00 00 00 00 callq 1a <func1+0x1a>16: R_X86_64_PC32 printf1a: 90 nop1b: 48 83 c4 20 add $0x20,%rsp1f: 5d pop %rbp20: c3 retq0000000000000021 <main>:21: 55 push %rbp22: 48 89 e5 mov %rsp,%rbp25: 48 83 ec 30 sub $0x30,%rsp29: e8 00 00 00 00 callq 2e <main+0xd>2a: R_X86_64_PC32 __main2e: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)35: 8b 15 04 00 00 00 mov 0x4(%rip),%edx # 3f <main+0x1e>37: R_X86_64_PC32 .data3b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 41 <main+0x20>3d: R_X86_64_PC32 .bss41: 01 c2 add %eax,%edx43: 8b 45 fc mov -0x4(%rbp),%eax46: 01 c2 add %eax,%edx48: 8b 45 f8 mov -0x8(%rbp),%eax4b: 01 d0 add %edx,%eax4d: 89 c1 mov %eax,%ecx4f: e8 ac ff ff ff callq 0 <func1>54: 8b 45 fc mov -0x4(%rbp),%eax57: 48 83 c4 30 add $0x30,%rsp5b: 5d pop %rbp5c: c3 retq5d: 90 nop5e: 90 nop5f: 90 nop
在这段输出信息中,你可以发现很多指令都是汇编代码,但是你也可以分析出这个目标文件里面有两个函数分别是func1()和main(),并且你也可以看出text段的第一个字节0x55就是func1()函数的第一条“push %rbp”指令而最后一个字节“0x90”则是main()函数的最后一条指令"nop"。作者也在图中标出了具体的func1()函数的范围,具体的可由读者自行结合自己的输出信息进行分析,如图:
图6.func1()函数的范围分布
结合objdump分析数据段和只读数据段
通过前几个小节,我们找到data段保存的是初始化了的全局静态变量和局部静态变量,而rdata段则保存只读的数据。在我们使用“objdump -s -d test1.o”指令输出的信息中,我们可以在data段输出的信息中发现代码定义的global_init_var和static_var两个变量的值是存储到data段的,具体的十六进制值为0x54和0x55,对应的正好就是85和85。具体如下图:
图7.data段分析
可能有读者会有疑问,为什么是存储到data中的前四个字节0x54,0x00,0x00,0x00而不是0x00,0x00,0x00,0x54这种形式?这主要涉及了CPU的字节序采用的是大端还是小端存储的方式(有兴趣的读者可以AI一下)。而对于rdata段,我们可以在func1()函数中的printf中发现其"%d\n"对应的就是rdara段中显示的ASCII字节的排序,并且最后以\0作为结尾(为什么是以\0作为结尾后续在讲解到字符串表时会进行解释)。具体如下图所示:
图8.rdata段分析
结合objdump分析bss段
对于bss段,我们知道该段存储的都是未初始化的全局变量和局部静态变量。编译器的行为对于变量存储到bss段还是data段有很大的影响,有些编译器会将全局位初始化的比哪里存放在目标文件的bss段,有些则不存放,只是预留一个未定义的全局变量符合,等到最终链接成可执行文件的时候再在bss段分配空间。但是值得一提的是编译党员内部可见的静态变量则是存储到bss段的(例如给global_uninit_var加上static修饰)
本篇博客先讲解到此,后续的博客将会对目标文件中的其他段和目标文件的头部信息进行描述