程序员的自我修养三目标文件里有什么

 

编译器编译源代码后生成的文件叫做目标文件
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是没有经过链接的过程。

3.1目标文件的格式

现在PC平台流行的是可执行文件格式,主要是win下的PE和Linux的ELF,它们都是COFF格式的变种。

不光是可执行文件按照可执行文件格式存储。动态链接库和静态链接库文件都是按照可执行文件格式存储。

 

ELF文件类型说明实例
可重定位文件 包含代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也是这种 Linux的.0。win的.obj
可执行文件 包含可以直接执行的程序,ELF可执行文件,一般没有扩展名 win的.exe文件
共享目标文件 包含代码和数据,可以在两种情况下使用:1) 链接器可使用这种文件和其他可重定位文件和共享目标文件链接,产生新的目标文件 Linux的.so,win的DLL
核心转储文件 当进程意外终止,系统可以将进程的地址空间的内容以及终止时的其他信息转储到核心转储文件 Linux下的core dump

3.2目标文件是什么样的

目标文件中包含机器指令代码,数据,还包括里链接时所需要的一些信息:符号表、调试信息、字符串等。目标文件将这些信息按节的形式存储,也叫做段。

程序源代码编译后的机器指令放在代码段里(.code .text),全局变量和局部静态变量数据经常放在数据段(.data),未初始化的全局变量和局部静态变量一般放在叫(bss)段里,.bss中只是为未初始化的全局变量和局部静态变量预留位置,它没有内容,所以在文件中也不占据空间。
文件头描述了整个文件的属性,文件头还包括一个段表,段表就是一个描述文件中各个段的数组。
程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,数据段和bss属于程序数据。

3.3挖掘SimpleSection.o

真正了不起的程序员对自己的程序的每一个字节都了如执掌

/*XimpleSection.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(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 SimpleSection.c
得到一个1104字节的SimpleSection.o目标文件,使用binutils查看SimpleSection.o内部结构:

上面除了最基本的代码段、数据段、BSS段以外,还有只读数据段、注释信息段、堆栈提示段。
每个段的第二行中“CONTENTS”,”ALLOC”表示段的各种属性,BSS段没有CONTENTS,表示它在ELF中没有内容。

3.3.1 代码段

3.3.2 数据段和只读数据段

.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。两个变量共四字节,一共就是data段大小8字节。
字符串常量”%d/n”,被放到了”.rodata”段,”.rodata”段四个字节刚好是字符串常量的ASCII字节序。
“.rodata”段存放的是只读数据,是程序里面的只读变量和字符串常量。

3.3.3  BBS段

.bss段存放的是未初始化的全局变量和局部静态变量,上述代码global_uninit_var和static_var2 就被存放在.bss段中。但有些编译器不会把未初始化的全局变量存放在目标文件.bss段中,只是预留一个未定义的全局变量。但编译单元内部可见的静态变量(给global_uninit_var加上static修饰)则是存放在bss段的。

变量存放位置
static int x1=0;//f3放在BBS段中,被认为未初始化的
static int x2=1;

3.3.4 自定义段

GCC提供一个扩展机制,可以指定变量所处的段:

_attribute_((section("FOO"))) int global=42;
_attribute_((section("FOO"))) int FOO()

全局变量或函数之前加上”attribute((section(“name”)))”属性就可以把相应的变量或函数放到以”name”作为段名的段中。

3.4 ELF文件结构描述

3.4.1 文件头

可以使用readelf命令来详细查看ELF文件。

ELF文件有32位版本和64位版本,文件头结构也有两个版本,叫做”Elf32_Ehdr”和”Elf64_Ehdr”.文件头内容是一样的,不过有些成员大小不一样。为了对每个成员大小做出明确规定以便于在不同的编译环境下都拥有相同的字段长度,”elf.h”使用typedef定义了一套自己的变量体系:

ELF文件头中各个成员含义与readelf输出结果的对照表:

ELF魔数

几乎所有可执行文件格式的最开始几个字节都是魔数,比如a.out开始两个字节为0X01,0X07。这种魔数用来确定文件类型,操作系统加载可执行文件的时候会确定魔数是否正确。

文件类型

e_type成员表示ELF文件类型。系统通过这个常量来判断ELF的真正文件类型,而不是通过扩展名。

机器类型

ELF文件格式被设计成可以在多个平台下使用,但不表示同一个ELF文件可以在不同平台下使用,是不同平台下的ELF文件遵循同一套ELF标准。e_machine成员就表示该ELF文件的平台属性。

3.4.2 段表

段表就是保存ELF中各种段的基本属性的结构。它描述了各个段的信息。ELF文件的段结构就是由段表决定的,编译器,链接器和转载器都是依靠段表来定位和访问各个段的属性的。ELF文件中的位置由ELF文件头的”e_shoff”成员决定。
使用readlf工具查看段表结构:

段表结构是一个以”Elf32_Shdr”结构体为元素的数组。数组元素的个数等于段的个数,每个结构体对应一个段。”Elf32_Shdr”又被描述为段描述符。ELF段表的第一个元素是无效的段描述符,类型为NULL。

段表的位置:

 

段的类型

段的名字只是在链接和编译过程中有意义,它不能真正表示段的类型。

段的标志位

段的标志位表示该段在进程虚拟地址空间的属性,相关常量以SHF_开头

段的链接信息

如果段的类型与链接相关,那么sh_link和sh_info这两个成员所包含的意义如下图:(其他类型的段,这两个成员没有意义)

3.4.3 重定位表

“.rel.text”的段,类型是”SHT_REL”它是一个重定位表。链接器处理目标文件的时候必须对目标文件中某些部位进行重定位,这些重定位信息就存在重定位表中。”.rel.text”就是针对”.text”段的重定位表。

3.4.4 字符串表

因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。常见的做法是把字符串集中起来存放一个表,然后使用字符串在表中的偏移来引用字符串。

3.5 链接的接口——符号

链接过程的本质就是要把多个不同的目标文件之间相互”粘”到一起,为了使不同目标文件之间能够相互粘合,这些目标文件必须有固定的规则才行。目标文件之间的相互拼合实际上时对地址的引用。链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名
每一个目标文件都有一个对应的符号表,这个表里面记录了目标文件所用到的所有符号,每个定义的符号有一个值,这个值叫做符号值
将符号表中所有符号进行分类,它们可能是下面类型中的一种:

  • 定义在本目标恩健的全局符号,可以被其他目标文件引用。
  • 在本目标文件中引用的全局符号,没有定义在本目标文件内,一般叫做外部符号
  • 段名。这种符号往往由编译器产生,它的值就是该段的起始地址。
  • 局部符号,这类符号只能在编译单元内部可见
  • 行号信息,即目标文件指令与源代码中代码行的对应关系。

链接过程只关系全局符号的相互”粘合”。
使用nm查看符号结果如下:

3.5.1 ELF符号表结构

ELF文件中的符号表往往是文件中的一个段,段名一般叫”.symtab”,每个Elf32_Sym结构对应一个符号。

符号类型和绑定信息

该成员低4位表示符号的类型,高28位表示符号绑定信息。

符号所在段

如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标,如果符号不是定义在本目标文件中或者对于有些特殊符号,st_shndx的值有些特殊。

符号值

每个符号都有一个对应的值。如果这个符号是一个函数或者变量的定义,那么符号值就是这个函数或变量的地址。

3.5.2 特殊符号

当我们使用id作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但时你i可以直接声明并且引用它,我们称为特殊符号。

3.5.3 符号修饰与函数签名

编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字一样的,将会产生冲突。为了防止冲突,C语言源代码文件中的所有全局变量和函数经过编译以后,相对应的符号名前加上下划线”_”,后来增加了名称空间的方法来解决多模块的符号冲突问题。

C++符号修饰

C++符号修饰为了更好的区分两个函数,看下面代码:

int func(int);
float func(float);

class C
{
int func(int);
class C2{
int func(int);
};
};

namespace N{
int func(int);
class C{
int func(int);
};
}

函数签名:函数签名包含一个函数的信息,包括函数名,参数类型,所在类,名称空间和其他信息。函数签名用于识别不同的函数。
在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称
上面6个函数签名在GCC编译器下,相对应的修饰后名称如下图:

GCC的基本C++名称修饰方法如下:所有的符号都以”_Z”开头,对于嵌套的名字,后面紧跟”N”,然后时各个名称空间和类的名字,每个名字前名字字符串长度,再以”E”结尾。参数列表紧跟在”E”后面,对于int类型来说,就是字母”i”。
名称修饰机制也被用来防止静态变量的名字冲突。
不同的编译器厂商名称的修饰方法可能不同。

3.5.4 extern "C"

C++为了与C兼容,在符号管理上,C++有一个用来声明或定义一个C符号的”extern C”关键字。
用法:

extern "C"{
int func(int);
int var;
}

C++编译器将在extern “C”的大括号内部的代码当作C代码处理。
单独声明某个函数或者变量为C语言的符号,使用如下格式:

extern "C" int func(int);
extern "C" int var;

3.5.5 弱符号与强符号

多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误,这中符号称为强符号,有些符号可以定义为弱符号。对于C/C++,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量未弱符号。

针对强弱符号的概念,连接器就会按如下规则处理与选择被多次定义的全局符号:

  • 规则1:不允许强符号被多次定义
  • 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号
  • 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个

3.6 调试信息

目标文件里面还有可能保存调试信息。

编译器提前将源代码与目标代码之间的关系保存到目标文件中。

posted on 2017-08-10 10:13  Mr.Tan&  阅读(1452)  评论(0编辑  收藏  举报

导航