1.目标文件是什么
编译阶段分为预处理,编译,汇编,链接。Linux下汇编之后生成的.o文件就是目标文件,此时的目标文件经过链接之后才会生成最后的可执行文件。实际上,目标文件和可执行文件的结构基本上是相同的,区别就是目标文件中对一些符号的引用和地址还没有确定,只有等到链接之后才能确定。可以把目标文件和可执行文件看成是同一类型的文件。
2.目标文件的格式
Windows下可执行文件的格式是PE(Portable Executable),Linux下的为ELF(Executable Linkable Format),他们都是COFF(Common file format)格式的变种。目标文件一般采用和可执行文件一样的格式存储。Windows下目标文件后缀为.obj,Linux下为.o。还有一些其他文件如动态链接哭DLL(Windows的.dll和Linux的.so)。静态链接库(Windows的.lib和Linux的.a)文件都按照可执行文件的格式存储。
Linux下的ELF文件格式的4中分类:
(1)可重定位文件:Linux的.o,Windows的.obj。
(2)可执行文件:Linux的/bin/bash,Windows的.exe。
(3)共享目标文件:Linux的.so,Windows的DLL。
(4)核心转储文件:Linux下的core dump。当进程意外终止时,系统可以将进程的地址空间的内容以及终止时的一些其他信息转储到核心转储文件。
Linux下可以用file查看文件的文件格式:
3.目标文件的内容
这里介绍的Linux下ELF文件的结构。目标文件的内容是按段来存储的,如机器指令存放在代码段".code"或".text",数据存放在数据段".data",还有一些其他段。一个简易图如下:
可以看到开头包含一个文件头,它包含了整个文件的文件属性,包括文件是否可执行,是静态链接还是动态链接及入口地址,目标硬件平台等信息。文件头还包含一个重要的信息就是段表,段表其实就是一个数组,数组的元素是段描述符,段描述符描述了每个段在文件中的偏移和它的属性。文件头之后就是各个段(section)的信息。注意:段表并不是包含在文件头里面,文件头中包含的是段表在文件中的偏移,通过这个偏移可以找到段表。
机器指令放在.text段,初始化的全局变量和局部静态变量保存在.data段,未初始化的全局变量和局部静态变量一般放在.bss段。实际上未初始化的全局变量和局部静态变量的默认值是0,为它们分配空间并赋上0没必要,所以.bss段只是记录所有未初始化的全局变量和局部静态变量的大小总和,只是为它们预留位置,实际上.bss并没有内容,它在文件中不占据空间。
指令和数据分开存放是很有必要的:
(1)程序被加载后,数据和指令映射到不同的虚存区域,可以设置不同的权限分别是可读写和只读,可以防止指令被修改。
(2)配合硬件,cpu分别设有指令cache和数据cache。
(3)当有多个程序副本时候,可以在内存中只保留一份指令副本。
4.挖掘test.o
test.c如下:
int printf(const char * format,...);
int global_init_var=84;
int global_uninit_var;
void func1(int i) {
printf("%d\n",i);
}
int main() {
static int static_var=85;
static int static_var2;
int a=1;
int b;
func1(static_var+static_var2+a+b);
return a;
}
编译命令:gcc test.c -o test.o -c
-c表示只生成目标文件,不链接。可以用过objdump查看目标文件的内部结构。
-h表示打印各个段的基本信息。其中.rodata表示只读数据段,.comment注释信息段,.note.GNU-stack堆栈提示段。
Size段的大小,File off段的偏移。CONTENTS表示段在文件中存在,可以看到.bss没有CONTENTS。
4.1代码段
查看各个段内容的命令:objdump -s -d test.o
-s表示将所有段的内容以16进制的方式打印出来,-d表示将所有包含指令的段反汇编。
最左边一列是偏移,中间四列是16进制的内容,最后一列是各个段的内容的ASCII码形式。可以看到.text段的大小确实是0x55。在看.text段反汇编的结果,正是源代码中的两个函数。第一个字节0x55正是"push %ebp"指令,最后一个字节0xc3代表main最后一条指令"ret"。
4.2数据段和只读数据段
可以看到.data段存放的就是初始化的全局变量global_init_var和局部静态变量static_var,刚好8个字节。字符串常量"%d\n"存放在.rodata段中。加上\0一共4个字节。常量存放在.rodata段,.rodata语义上就支持了C++的const关键字。
4.3BSS段
static int x1=0;
static int x2=1;
x2会被放在.data中,因为x2被初始化了。而x1虽然被初始化了,但值是0,而未初始化的静态变量默认也是0,所以编译器优化会把它当成未初始化的,将它放入.bss中,因为.bss并不占用磁盘空间。
4.4其他段
ELF还有可能包含其他段,如下所示。
可以通过objcopy将一个二进制文件作为目标文件的一个段。
比如有一个Image.jpg:
_binary_image_jpg_start,binary_image_jpg_end,_binary_image_jpg_size分别表示图片文件在内存中的起始地址,结束地址和大小。
4.5自定义段
__attribute__((section("FOO"))) int global=42;
__attribute__((section("BAR"))) void foo() {}
__attribute__((section("name")))可以将相应的变量或函数放到以"name"作为段名的段中。
5.ELF文件结构描述
下面是ELF文件结构更详细的结构图:
5.1文件头
使用readelf查看ELF文件头:
ELF文件头结构及相关常数定义在"/usr/include/elf.h"里。ELF文件有32位版本的和64位版本的,分别对应的数据结构是Elf32_Ehdr和Elf64_Ehdr。以下介绍其中的一些字段。
Magic
Magic对应的16个字节,最开始的4个字节0x7f,0x45,0x4c,0x46,是所有ELF文件都必须相同的标识码。第一个字节是ASCII字符里面的DEL控制符,后面三个是ELF这个三个字符的ASCII,这4个字节称为ELF文件的魔数。几乎所有的可执行文件一开始的几个字节都是魔数,可以通过魔数确认可执行文件的类型。操作系统在加载可执行文件时会确认魔数是否正确。
第五个字节0x01表示32位,0x02表示64位,0x00无效文件。第六个字节0x01小端格式,0x02大端格式,0x00无效格式。第七个字节规定ELF的主版本号,一般是1。都面的9个字节ELF标准没有定义,一般是0。
类型
5.2段表
Start of section headers表示段表在文件中的偏移。objdump -h只是将几个关键的段显示出来,省略了其他的辅助性的段,如符号表,字符串表,段名字符串表,重定位表等。可以使用readelf查看所有的段。
以上输出内容就是段表的内容。段表就是一个数组,数组元素是段表描述符(Elf64_Shdr或Elf32_Shdr)。数组的第一个元素是无效的段描述符,类型是NULL,除此之外其他的段描述符对应一个段。
5.3重定位表
类型为REL的为重定位表,如上面的.rela.text。链接器在处理目标文件时,需要对代码段或者数据段里对绝对地址引用的位置进行重定位,这些重定位信息就记录在重定位表里。对于每一个需要重定位的代码段或数据段,都会有对应的重定位表,重定位表的名字是在原始段名前加上".rel"前缀。
5.4字符串表
ELF文件用到的字符串一般用字符串表存储。一种常见的做法是把字符串集中存放到一个表,然后通过偏移来引用字符串。如下:
.strtab表示字符串表,.shstrtab表示段表字符串表。字符串表用来保存普通的字符串,如符号的名字。段表字符串表用来保存段表中用到的字符串,最常见的就是段名。在文件头信息中的最后一个字段就表示段表字符串表在段表中的索引。
6链接的接口——符号
每个目标文件都有一个符号表,记录了目标文件中用到的所有符号,每个符号对应一个值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。上图中的.symtab就是符号表。
符号的分类,以为test.c为例:
(1)定义在本目标文件的全局符号:func1,main,global_init_var。
(2)在本目标文件中引用的全局符号,却没有定义在本目标文件中,即外部符号:printf。
(3)段名,由编译器产生:.text,.data等。
(4)局部符号:static_var,static_var2。
(5)行号信息,即目标文件指令与源代码中代码行的对应关系。
使用nm查看目标文件的符号结果:
6.1ELF符号表结构
符号表是一个数组,数组的元素类型是Elf32_Sym(32位)或Elf64_Sym(64位)。每个元素代表一个符号,下标0的元素为“未定义”的符号。
查看符号表的内容:
6.2弱符号和强符号
在C/C++中,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。强符号和弱符号是针对定义来说的,不是针对符号引用。可以通过__attribute__((weak))来定义一个强符号为弱符号。
extern int ext;
int weak;
int strong=1;
__attribute__((weak)) int weak2=2;
int main() {
return 0;
}
其中,weak,weak2是弱符号,strong和main是强符号。针对强弱符号,有以下规则:
(1)不允许强符号多次定义。
(2)如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,则选择强符号。
(3)如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的。
6.3弱引用和强引用
在链接时,链接器要对每个符号进行决议,如果没有找到该符号的定义,链接器会报错,这种称为强引用。在处理弱引用时,如果该符号有定义,链接器回将该符号的引用决议,如果未定义,链接器不会报错,一般默认其是0或者是一个特殊的值。
使用__attribute__((weakref))可以用来声明对一个外部函数的引用为弱引用:
__attribute__((weakref)) void foo();
int main() {
if(foo)
foo();
}
7调试信息
编译时加上-g参数,将会往目标文件里面加上调试信息,可以看到,这将会多出很多debug相关的段。
、
通过strip可以去掉调试信息:strip test.o