3.程序员的自我修养---目标文件

1.目标文件的格式
	现在 PC 平台流行的可执行文件格式主要是:
	1.Windows 的 PE(Portable)
	2.Linux 的 ELF(Executable Linkable Format)
	它们都是 COFF(Common file format) 格式的变种。

	ELF文件类型
	可重定位文件(Relocatable File) : 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态库也可归为这一类。如Linux的 .so 文件,Windows的.obj
	可执行文件(Executable File) : 这类文件包含了可以直接执行的程序,它的代表就是 ELF 可执行文件,它们一般都没有扩展名,比如 /bin/bash 文件和 Windows 的 .exe
	共享目标文件(Shared Object File) : 这种文件包含了代码和数据,可以在一下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生
									 新的目标文件;第二件是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。如,Linux的 .so, windows的 DLL
	核心转储文件(Core Dump File) : 当进程意外终止时,系统可以将该进程的地址空间的内容以及终止时的一些其他信息转储到核心转储文件。如,Linux下的 core dump。

	可以用命令 file 查看文件类型。

	COFF 的主要贡献是在目标文件里面引入了 "段" 的机制,不同的目标文件可以拥有不同数量以及不同类型的 "段"。另外,它还定义了调试数据的格式。

2.目标文件是什么样的
	目标文件还包含了链接时所需要的一些信息,比如符号表,调试信息,字符串等。
	ELF 文件的开头是一个 "文件头",它描述了整个文件的文件属性,包含文件是否可执行,是静态链接还是动态链接及入口地址,目标硬件,目标操作系统等信息。
	文件头还包括一个段表(Section Table) ,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有
  信息。

  	一般 C 语言编译后执行语句都编译成机器代码,保存在 .text 段;已初始化的全局变量和局部变量保存在 .data 段;未初始化的全局变量和局部静态变量一般放在 .bss 段.它们本来也
  可以被放在 .data 段的,但因为它们都是0,所有为它们在 .data 段分配空间并且存放 0 是没有必要的。程序运行的时候,它们的确要占用空间,并且可执行文件必须记录所有未初始化的全局
  变量和局部静态变量的大小总和,记为 .bss 段。所以,.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

  BSS(Block Started by Symbol) 历史:
  	汇编中的一个伪指令,用于为符号预留一块内存空间。

  总的来说,程序源代码被编译后主要分为两种段: 程序指令和程序数据。代码段属于程序指令,而数据段和 .bss 段属于程序数据。
  
  把数据和指令分开的好处:
  	1.一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对进程来说是可读写的,而指令去对进程来说是只读的,所以这2个虚存区域的权限可以被分别设置成可读写和只读。
  	  这样可以防止有意或者无意的修改。
  	2.另一方面是对于现代的 CPU 来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中位置非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的
  	  局部性。现代CPU的缓存一般被设计成数据缓存和指令缓存,所以程序的指令和数据被分开存放对 CPU 的缓存命令中提高有好处。
  	3.第三个原因,其实也是最重要的原因。就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只需要保存一份程序的指令部分。对于指令这种只读区域来说是这样,对于其他只读
  	  数据也是一样的,比如程序里面带有的图标,图片,文本等资源也属于共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看共享指令这个概念,它在现代操作系统里面占据了极为
  	  重要的地位,特别是有动态链接的系统中,可以节省大量的内存。
3.挖掘SimpleSection.o
	objdump -h SimpleSection.o
	objdump -x SimpleSection.o
	readelf -a SimpleSection.o

	代码段(.text)
	数据段(.data)
	.bss
	只读数据段(.rodata)
	注释信息段(.comment)
	堆栈提示段(.notee.GUN-stack)

	objdump -x -s -d SimpleSection.o // -d 反汇编

	objcopy // 将一个二进制文件,如图片,音乐等作为目标文件中的一段

	自定义段:
		正常情况下,gcc 编译出来的目标文件中,代码会被放到 .text 段,全局变量和静态变量会被放到 .data 和 .bss 段。但有时候你可能希望变量或者
	  某些部分代码能够被放到你指定的段中去,以实现特定的功能。比如为了满足某些硬件的内存和 IO 地址布局,或者是像 Linux 操作系统内核中用来完成一些
	  初始化和用户空间复制时出现页错误异常等。gcc 提供了一个扩展机制,使得程序员可以指定变量所处的段:
	    __attribute__((section("FOO"))) int global = 42;

	    __attribute__((section("BAR"))) void foo()
	    {
	    }
	    我们在全局变量或者函数之前加上 __attribute__((section("name"))) 属性就可以把响应的变量或者函数放到以 "name" 为段名的段中。

4.ELF 文件结构描述
	ELF Header(ELF 文件头)
	.text
	.data
	.bss
	...
	other sections
	Section header table(段表)
	String Tables
	Symbol Tables

	ELF 目标文件格式的最前部分是 ELF 文件头,它包含了描述整个文件的基本属性,比如 ELF 文件版本,目标机器型号,程序入口地址等。紧接着是 ELF 文件各个段。
  其中 ELF 文件中与段有关的重要结构就是 段表(Section Header Table),该表描述了 ELF 文件包含的所有段信息,比如每个段的段名,段的长度,在文件中的偏移,
  读写权限及段的其他属性。

  ELF 魔术:这16个字节被 ELF 标准规定用来标识 ELF 文件的平台属性.
  	最开始的4个字节是所有 ELF 文件都必须相同的标识码,分别为 7f 45 4c 46, 第一个对应 ASCII 字符里面的 DEL 控制符,后面3个字节刚好是 ELF 这3个字节的 
  ASCII 码。这4个字节又被称为 ELF 文件的魔数,几乎所有的可执行文件格式的最开始几个字节都是魔数。比如 a.out 格式最开始的字节为 0x01, 0x07; PE/COFF 文件
  最开始的2个字节为 0x4d, 0x5a,即 ASCII 字符的 MZ。 这种魔术用来确定文件的类型,操作系统在加载可执行文件的时候会确定魔术是否正确,如果不正确会拒绝加载。
    接下来的一个字节是用来标识 ELF 的文件类的,01 表示32位,02 表示64位;
    第6位是字节序,规定该 ELF 文件是大端序和小端序。
    第7位字节规定 ELF 文件的主版本号,一般是1,因为 ELF 标准自 1.2 版以后就没有更新了。
    后面的9个字节 ELF 标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

  文件类型:e_type 成员表示 ELF 文件类型,每个文件类型对应一个常量。系统通常通过这个常量来判断 ELF 的真正文件类型,而不是通过文件的扩展名。相关常量以 "ET_"
  开头。
  	ET_REL 1 可重定位文件,一般为 .o 文件
  	ET_EXEC 2 可执行文件
  	ET_DYN 3 共享目标文件,一般为 .so

  机器类型:
  	ELF 文件格式被设计成在多个平台下使用。这并不表示ELF文件可以在不同的平台下使用(就像Java的字节码文件那样),而是表示不同平台下的ELF文件都遵守同一套ELF标准。
  e_machine 成员就表示该 ELF 文件的平台属性。	
5.段表
	段表就是保存这些段的基本属性结构。它描述了 ELF 的各个段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限以及段的其他属性。
  也就是说,ELF 文件的段结构就是由段表决定的。编译器,链接器,装载器都是依靠段表来定位和访问各个段的属性。段表在 ELF 文件中的位置由 ELF 文件
  头的 "e_shoff"成员决定。

  	段的类型:
  		段的名字只是在链接和编译过程中有意义,但它不能真正的表示段的类型。我们也可以将一个数据段命名为 ".text",对于编译器和链接器来说,主要决定段的
  	  属性的是段的类型(sh_type) 和段的标志位(sh_flags)。


6.重定位表
	有个叫 ".rel.text" 的段,它的类型为 "SHT_REL",也就是说它是一个重定位表。链接器在处理目标文件时,必须要对目标文件中的某些部位进行重定位,即代码段
  和数据段中哪些对绝对地址的引用的位置。这些重定位信息都记录在 ELF 文件的重定位表中,对于每个需要重定位的代码段或者数据段,都会有一个相应的重定位表.比如,
  ".rel.text" 就是针对".text"段的重定位表。因为 ".text"段中至少有一个绝对地址的引用,那就是对 "printf"函数的调用。而".data"段则没有绝对地址的引用,
  它只包含几个常量。所以没有 针对 ".data"段的重定位表 ".rel.data"。

7.字符串表
	ELF 文件中用到了很多字符串表,比如段名,变量名等。因为字符串长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到
  一个段表,然后使用字符串在表中的偏移来引用字符串。
  	通过这种方法,在 ELF 文件中引用字符串只需给出一个数字下标即可,不用考虑字符串长度问题。一般字符串表在 ELF 文件中也以段的形式保存,常见的段名为 ".strtab"
  或者 ".shstrtab"。这2个字符串分别为 字符串表和段表字符串表.顾名思义,字符串表是用来保存普通的字符串的,比如符号的名字;段表字符串用来保存段表中用到的字符串,
  最常见的就是段名。

  Section header string table index: 10 
  只要分析 ELF 文件头,就可以得到段表和段表字符串表的位置,从而解析整个 ELF 文件。

8.链接的接口---符号
	链接的过程本质就是要把多个不同的目标文件之间互相 "粘" 到一起,才可以拼成一个整体。为了使不同的目标之间能够粘合,这些目标文件之间必须有固定的规则才行。在链接中,
  目标文件之间互相拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。在链接中,我们把函数和变量统称为符号,函数名和变量名就是符号名。
    我们可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能够完成的。链接过程中很关键的一部分就是符号的管理,每个目标文件都会有一个相应的符号表,这个表里记录
  了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值。对于变量和函数来说,符号值就是它们的地址。
    符号类型:
    1.定义在本目标文件的全局符号,可以被其他目标文件引用。"func1", "main" 等
    2.在目标引用的全局符号,却没有定义在本目标文件中,这一般叫做外部符号(external symbol),也就是符号引用。如, printf
    3.段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如 simplesectio.so 里面的 .text, .data 等
    4.局部符号,这类符号只是在编译单元内部可见。如 "static_var"等。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往忽略
      它们。
    5.行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。

    链接过程只关心全局符号的互相"粘合",局部符号,段名,行号都是次要的,它们对于其他目标文件来说是 "不可见的"

    用 nm 查看符号表

    ELF 文件中的符号表往往是文件中的一个段,段名一般叫 ".symtab" 。

    符号类型和绑定信息:
    	STB_LOCAL : 0, 局部符号,对于目标文件的外部是不可见的
    	STB_GLOBAL : 1, 全局符号,外部可见
    	STB_WEAK : 2, 弱引用


    特殊符号:
    	当我们使用 ld 作为链接器生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但你可以直接声明并引用它,我们称之为特殊符号。
      这些特殊符号是被定义在 ld 链接器的链接脚本中的。
      __executable_start, 该符号位程序的起始地址,注意,不是入口地址,是程序的最开始的地址
      __etext, 该符号位代码段结束地址
      _edata 该符号位数据段结束地址
      _end 该符号为程序结束地址

9.符号修饰与函数签名
	gcc 编译器通过参数 -fleading-underscore 或 -fno-leading-underscore 来打开或者关闭是否在 C 语言符号前加 _ 下划线。

	函数签名:它包含了一个函数的信息,包括函数名,它的参数类型,它所在的类和名称空间以及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字
  			 只是函数签名的一部分。在编译器及链接器处理符号时,它们使用某种名称修饰方法,使得每个函数签名对应一个修饰后名称。编译器在将 C++ 源代码编译成目标文件时,将
  			 函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++ 编译器和链接器都使用符号来
  			 识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。

    gcc 的基本 C++ 名称修饰方法如下:所有的符号都以 "_Z “ 开头,对于嵌套的名字(在名称空间或者类里面的),后面紧跟 "N", 然后是各个名称空间和类的名字,每个名字前面是名字
  字符串的长度,再以 "E" 结尾。比如,N::C::func, 经过修饰后就是 _ZN1N1C4funcE. 对于一个函数来说,它的参数列表紧跟在 "E" 后面,对于 int 类型来说,就是字母 "i"。所以
  整个 N::C::func(int) 函数签名经过修饰为 _ZN1N1C4funcEi
  	c++filt 可以用来解析被修饰过的名称.

10.extern "C"
	C++ 为了兼容C,在符号管理上,C++有一个用来声明或定义一个 C 的符号的 "extern "C" "的关键字用法
	extern "C"
	{
		int func(int);
		int var
	}
	C++ 编译器会将 大括号内部的代码当作 C 语言代码。C++ 的名称修饰机制将不会起作用。

10.弱符号与强符号
	对于 C/C++ 语言来说,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过 gcc 的 "__attribute__((weak))" 来定义任何一个强符号为
  弱符号。注意,强符号和弱符号都只是针对定义来说的,不是针对符号的引用的。
  	1.不允许强符号被重复定义
  	2.如果一个符号在某个目标文件中是强符号,在其他目标文件中是弱符号,那么选择强符号
  	3.如果一个符号所在的目标文件都是弱符号,那么选择占用空间最大的一个。

  	强引用和弱引用:
  		目前我们所看到的对外目标文件的符号引用在目标文件被最终链接成可执行文件时,它们必须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义,这被称为强引用。
  	  与之对应的还有弱引用,在处理弱引用的时候,如果该符号未定义,则链接器将该符号的引用决议:如果该符号未别定义,则链接器对该引用不报错。
  	    在 gcc 中,我们可以通过 __attribute__((weakref)) 关键字声明对一个外部函数的引用为弱引用。
  	    __attribute__((weakref)) void foo(); // 弱引用
  	    
  	    这种弱符号和弱引用对于库来说十分重要,比如在库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本发库函数。


11.调试信息
	目标文件里面是可以包含调试信息的,几乎所有现代的编译器都支持源代码级别的调试。编译的时候加上 -g 选项。现在的 ELF 文件采用一个叫 DWARF 的标准调试信息格式。
	strip foo // 去掉调试信息

1.目标文件格式

2.目标文件是什么样的

4.ELF 文件结构描述

5.链接的接口---符号

6.调试信息

猜你喜欢

转载自blog.csdn.net/enlyhua/article/details/83090619