1, 编译器编译源代码生成的文件叫做目标文件。
从结构上说,是编译后的可执行文件,只不过还没有经过链接
3.1 目标文件的格式
1,可执行文件的格式: Windows下的PE 和 Linux下的ELF
2,从广义上说,目标文件与可执行文件的格式几乎是一样的,所以广义上可以将目标文件与可执行文件看成是一种类型的文件。
3,可执行文件,动态链接库,静态链接库都按照可执行文件格式存储(Windows下是 PE-COFF格式,Linux下是ELF格式)。
4,Linux下命令: $: file *** 显示出对应文件的类型
目标文件是什么样的
1,目标文件包含的内容:编译后的机器指令代码、数据,还有链接时要的一些信息(比如符号表、调试信息、字符串等)。
1, 一般目标文件把上述信息按不同的属性,以节(section)的形式存储。有时也叫段(segment)
2, 代码段常见的名字有 “.code” “.text”,编译后的机器指令就放在代码段,
数据段:一般名字都是 “.data”,已初始化全局变量和局部静态变量数据放这
3, ELF文件的开头是一个”文件头”,它描述了整个文件的文件属性(是否可读可写可执行,是静态链接还是动态链接及入口地址)、目标硬件、目标操作系统等信息。
4, 文件头还有一个段表(section table)。描述文件各个段的数组(各个段在文件中的偏移和属性)。
5, 关于bss段:未初始化的全局变量和静态局部变量一般放在一个”.bss”段的地方。
它只是为未初始化的全局变量和静态局部变量预留位置而已,并没有内容,在文件中不占空间。
6, 总体来说:程序源代码被编译后主要分成两种段:程序指令(代码段),程序数据(数据段,bss段)。
3.3挖掘simplesection.o
1,objdump 查看各种目标文件的结构和内容, objdump –h main.o -h 表示把ELF文件的各个段的基本信息打出来
readelf 专门针对ELF文件格式的解析器
size 可以用来查看ELF文件的代码段、数据段和bss段的长度 size main.o
3.3.1代码段
1,objdump 的参数 –s 将所有的段的内容以十六进制的方式打印出来
-d 将所有包含指令的段反汇编 比如 objdump –s –d main.o
-h 把关键的段显示了出来,忽略了辅助性段
3.3.2数据段和只读数据段
1, “.data” :保存的是已初始化全局变量和局部静态变量
2,,”rodata”:存放的是只读数据,一般是程序中的只读变量(如用const修饰的)和字符串常量。
3.3.3 bss段
1,存放的是未初始化的全局变量和静态局部变量。不占磁盘空间
3.3.4 其他段
还可以自定义段GCC提供的扩展机制
3.4 ELF文件结构描述
3.4.1 文件头
1,查看 readelf –h main.o
2,ELF文件中定义了:ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、
ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
3,ELF魔数:文件头最开始的4个字节是所有ELF都必须相同的标识码。又称为魔数。
魔数用来确认文件类型,操作系统在加载可执行文件的时候会确认魔数是否正确,若不正确则拒绝加载。
4,ELF文件类型:分为 可重定位、可执行、共享目标文件、核心转储文件
3.4.2 段表(除了文件头以外最重要的结构)
1,描述了ELF各个段的信息(比如每个段的段名、段的长度、在文件中的偏移,读写权限及其他属性)。
2,编译器、链接器和装载器都是靠段表来定位和访问各个段的属性的。
3,readelf –S main.o 显示真正的段表结构
4,上述结果是一个以 “ELF32_Shdr” 结构体为元素的数组。
3.4.3 重定位表
1,一个叫”rel.text”的段,类型是”SHT_REL”,就是重定位表
2,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位(即代码段和数据段中那些对绝对地址的引用的位置)。
3,这些重定位信息都记录在ELF文件的重定位表里。对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。
3.4.3 字符串表
1,把ELF文件中用到的字符串(段名、变量名等)集中起来存放到一个表中。然后使用字符串在表中的偏移来引用字符串。这个表就是字符串表。
2,一般字符串表在ELF文件中也以段的形式保存。常见的段名有”.strtab”, ”.shstrtab”
3,”.strtab”:字符串表:保存普通的字符串
4,”.shstrtab”:段表字符串表:保存段表中用到的字符串,最常见的就是段名。
3.5 链接的接口——符号
1,链接过程的本质就是要把多个不同的目标文件之间相互粘滞到一起。
2,为了使不同目标文件之间能够相互粘合,这写目标文件必须要有固定的规则才行。
3,在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。
4,在链接中,我们将函数和变量统称为符号(symbol),函数名或变量名就是符号名(symbol name)。
5,每一个目标文件都会有一个相应的符号表(symbol table),记录着目标文件中所用到的所有的符号。
6,每个定义的符号有一个对应的值叫做符号值(symbol value)。对于变量或函数来说,符号值就是它们的地址。
8, 符号的分类: (1)和(2)比较需要关注
(1) 定义在本目标文件的全局符号,可被其他目标文件引用。
(2) 在本目标文件中引用的全局符号,在别的目标文件中定义。一般叫做外部符号(external symbol)。
(3) 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
(4) 局部符号,这类符号只在编译单元内部可见,
(5) 行号信息,即目标文件指令与源代码中代码行的对应关系,是可选的。
3.5.1 ELF符号表结构
1,ELF文件中的符号表往往是文件中的一个段,y一般叫做 “.symtab”。是一个Elf32_Sym的数组,数组中每个元素对应一个符号。
2,结构体定义如下
typedef struct{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Half st_shndx; }Elf32_Sym;
3.5.2 特殊符号
1,ld链接器产生可执行文件时,会给我们定义很多符号(没有在自己的程序中定义),但是可以直接声明并且引用它,我们称之为特殊符号。
3.5.3 符号修饰与函数签名
1,c++增加了名称空间(namespace)的方法来解决多模块之间的符号冲突问题。
2,c++符号修饰
(1) 函数签名:包含了一个函数的信息(函数名、参数类型、所在的类和名称空间和其他信息)。函数签名用于识别不同的函数。
3.5.4 extern”C”
1,c++为了与c兼容,在符号的管理上,c++有一个用来声明或定义一个C的符号extern”C”的关键字用法。
2,可以让c++的名称修饰机制不作用。
3.5.5 弱符号与强符号
1,多个目标文件含有相同名字的全局符号的定义,在链接时将会出现符号重复定义的错误。
2,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
3,强弱符号都是针对定义来说的,不是针对符号的引用。 假如 extern int ext; ext不是强符号也不是弱符号,因为它是一个外部变量的引用。
4,链接器按下面规则处理与选择被多次定义的全局符号。
(1) 不允许强符号被多次定义,否则报错。
(2) 若一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
(3) 若一个符号早所有的目标文件中都是若符号,那么选择其中占用空间最大的那一个。
5,弱引用与强引用:
强引用:假如链接时没找到该符号的定义,链接器就会报符号未定义错误。
弱引用:假如链接时没找到该符号的定义,链接器不会报错,默认其为0或是一个特殊值。
6,GCC中可以通过 “__attribute__((weakref))”这个扩展关键字来声明一个外部函数为弱引用 p93
用法: __attribute__((weakref)) void foo(); 这样就说明foo为弱符号。
7,弱符号和弱引用对库来说非常有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖。
8,补充: Linux中支持多线程时需要在编译时加上 –lpthread 选项, 比如 gcc test.c -lpthread
3.6 调试信息
1,在gcc编译时加上 –g 参数就会在产生的目标文件里面加上调试信息。 目标文件会多些 debug段。
2,假设有个目标文件 test, Linux下可以用 ”strip” 命令来去除调试信息, 用法: $ strip test
3.7 本章小结
参考《程序员的自我修养》