编译器将一个源文件生成目标文件时,会在目标文件中生成符号表 和重定位表 。
符号表包含在文件中定义的全局符号 以及在文件中引用的外部符号 (外部函数或变量)。
重定位表告诉链接器在哪些位置要进行重定位操作。
编译器生成的目标文件在文件的开始处会有一个elf头,描绘了整个文件的组织结构。它还包括很多节(section)。这些节有的是系统定义好的,有些是用户在文件在通过.section命令自定义的,链接器会将多个输入目标文件中的相同的节合并。
链接器对编译生成的目标文件进行链接时,A. 首先进行符号解析,找出外部符号在哪定义。如果外部符号在一个静态库中定义,则直接将对应的定义代码复制到最终生成的目标文件中。B. 接着链接器进行符号重定位。编译器在生成目标文件时,通常使用从零开始的相对地址,而在链接过程中,链接器从一个指定的地址开始,根据输入目标文件的顺 序,以段(segment)为单位将它们拼装起来。其中每个段可以包括很多个节(section)。除了目标文件的拼装,重定位过程中还完成了下面两个任 务:一是生成最终的符号表,二是对代码段(.text)中的某些位置进行修改,要修改的位置由编译器生成的重定位表指出。
链接过程中还会生成两个表:got表和plt表。
got表中每一项都是本运行模块要引用的全局变量或函数的地址,可以用got表来间 接引用全局变量。函数也可以把got表的首地址作为一个基准,用相对该基准的偏移量来(直接)引用静态函数。由于动态链接器(ld-linux.so)不 会把运行模块(*.so)加载到固定地址,在不同进程的地址空间中各运行模块的绝对地址、相对地址都不同。这种不同反映到got表上,就是每个进程的每个 运行模块都有独立的got表,所以进程间不能共享got表(内容)。
plt表中每一项都是一小段汇编代码,对应于本运行模块要引用的每一个全局函数。当链接器发现某个符号引用是位于其它共享目标文件(动态连接库*.so)中的一个全局函数时,就在文件的plt表里创建一个项目,以便将来重定位。
链接器生成的目标文件在文件开头也有一个elf头,描绘了整个文件的组织结构,这个文件中会有多个段(segment),每个段都由相应的节(section)拼装而成。
对由链接器链接生成的可执行目标文件进行加载运行时,内核首先读取elf文件头。根 据头部数据指示分别读入各种数据结构,找出可加载的段并调用mmap()函数将其加载到内存。内核找到标记为PT_INTERP的段,这个段对应着动态链 接器的名称,然后加载动态链接器(linux中通常是/lib/ld-linux.so.2)。接着内核将控制权交给动态链接器。动态链接器检查程序对外 部文件(共享库)的依赖性,并在需要时对其进行加载。之后动态链接器开始对程序中的外部引用(即全局变量/函数)进行重定位,即定位出程序其引用的外部变 量/函数的真实内存地址。R_386_GLOB_DAT类型的重定位项目涉及到got表。R_386_JMP_SLOT类型的重定位项目涉及到plt表。 动态链接还有一个延迟(lazy)特性,即真正引用时才进行重定位(环境变量LD_BIND_NOW为空值NULL时)。接下来动态链接器执行elf文件 中标记为.init节的代码,进行程序运行的初始化。最后动态链接器把控制权交给程序,从elf文件头中定义的入口处开始执行程序。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
ELF文件格式(翻译有些问题,部分修正)
3 ELF文件格式
3.1 介绍
目标文件有三种类型:
a. 可重定位文件(Relocatable File) (*.o) 包含适合于与其他目标文件一起链接来生成可执行文件或者可共享目标文件的代码和数据。
3.1 介绍
目标文件有三种类型:
a. 可重定位文件(Relocatable File) (*.o) 包含适合于与其他目标文件一起链接来生成可执行文件或者可共享目标文件的代码和数据。
b. 可执行文件(Executable File) (*.exe) 适合于执行的程序,此文件规定了 exec() 如何创建一个程序的进程映像。
c. 可共享目标文件(Shared Object File) (*.so) 包含了在两种使用环境中链接的代码和数据。首先链接器(ld)可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件(比如:编译器和 连接器 把*.o和*.so一起装配成一个*.exe文件)。其次,动态链接器(Dynamic Linker)可将它与某个可执行文件以及其它共享目标文件组合在一起,创建进程映像(比如:动态加载器把exe程序和*.so加载进内存执行)。
目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。
3.4.1 节区头部表格
表 5 节区头部表格中的特殊下标
SHN_COMMON OXFFF2 此节区定义的符号是公共符号。如 FORTRAN 中 COMMON 或者未分配的 C 外部变量。
(《linux内核完全剖析》P42 汇编器1次只处理1个源程序,如果变量不在本文件里定义,汇编器就留个记号,交给连接器处理。连接器对未定义的符号进行特殊处理。1。如果未定义符号里数 值是0,则表示该符号在本汇编源程序里没有定义,连接器会尝试到其他连接的文件里确定它的数值。比如:在一个源程序里使用了一个符号,但没有对符号进行定 义,汇编器处理汇编源程序时就会产生这种未定义符号,其值等于0。 2。如果未定义符号的数值不为0,那么该符号的值就表示是 .comm 公共声明(as汇编器汇编命令:.comm symbol,length 该命令在bss节区里,声明一个命名的公共存储区域)的需要保留的公共存储空间的字节长度。符号指向该存储空间的第1个地址处。汇编器1个1个的处理源文 件,产出多个.o文件,多个.o文件里可能有多个同名公共符号。连接器在连接过程中,会把多个目标文件中的同名的公共符号合并。如果连接器没有找到1个符 号的定义(而且此符号的值不为0),而是找到1个或多个公共符号,那么连接器就会分配长度为length字节的未初始化内存。length必须是一个绝对 值表达式,如果连接器找到多个长度不同但同名的公共符号,连接器就会按最大的length分配空间。)
3.4.2.2 sh_flags 字段
SHF_ALLOC: 表示此节区在进程执行过程中需要占用内存。某些(用于)控制(的)节区并不加载进目标文件的内存映像中,则其sh_flags位应设置为 0。
SHF_ALLOC: 表示此节区在进程执行过程中需要占用内存。某些(用于)控制(的)节区并不加载进目标文件的内存映像中,则其sh_flags位应设置为 0。
3.4.2.3
表 10 sh_link和sh_info字段解释
SHT_DYNAMIC sh_link: 此节区中条目所用到的字符串表(所在)的节区号 sh_info:0
SHT_HASH sh_link: 此哈希表所适用的符号表(所在)的节区号 sh_info:0
SHT_REL/SHT_RELA sh_link: 相关联的符号表(所在)的节区号 sh_info: 需要重定位操作的节区的节区号。表示该节区里包含有地址不明确的内容(变量/或函数符号地址不明确),需要进行重定位操作。
SHT_SYMTAB/SHT_DYNSYM sh_link: 相关联的字符串表(所在)的节区号 sh_info: 所有局部符号(LOCAL)的个数 + 1
(用readelf –a ./test 查看:.rel.plt节区 的sh_link=4 对应符号表 .dynsym section;sh_info=b 是16进制,对应10进制的11,对应 .plt section)
3.6 符号表(Symbol Table)
表 13 符号表项字段
st_shndx 一般是每个符号表项所对应的节区号。但某些数值具有特殊含义。参见3.6.3
表 13 符号表项字段
st_shndx 一般是每个符号表项所对应的节区号。但某些数值具有特殊含义。参见3.6.3
3.6.2 符号类型
在共享目标文件(*.so)中,类型为 STT_FUNC的函数符号具有特别的重要性。当其他目标文件引用了另1个共享目标文件(*.so)中的函数时,链接器(/bin/ld)自动为所引用的 符号创建过程链接表项(plt项)。在共享目标文件(*.so)中,类型不是 STT_FUNC的函数符号不会自动通过过程链接表进行引用。
3.6.4 符号取值
不同的目标文件类型中符号表项对 st_value 成员具有不同的解释:
在可重定位文件中,st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。
在可重定位文件中,st_value 是已定义符号的在本节区内的偏移量。就是说,st_value 是 st_shndx 所标识的节区内,从节区头部开始到符号位置的偏移。
在可执行和共享目标文件中,st_value 是一个虚拟地址。为了让动态链接器更好的使用文件里这些符号的值,st_value不再使用节区内的偏移量表示(是针对文件的描述视图),而使用虚拟地址(是针对内存的描述视图),因为这时与节区无关。
3.7.1 重定位表项
表 17 重定位表项字段说明
r_offset
表示重定位操作后,需要填写修改的具体位置(地址或者偏移量)。对于一个可重定 位文件(*.o)而言,此值是所在节区内的偏移量-表示本节区在此偏移量处的存储单元里包含的内容是要被重定位修改的。对于可执行文件(*.exe)或者 共享目标文件(*.so)而言,此值是个内存虚拟地址,表示此地址处的存储单元里包含的内容是要被重定位修改的(一般这些虚拟地址都指向内存中的.got 表里)。
r_offset
表示重定位操作后,需要填写修改的具体位置(地址或者偏移量)。对于一个可重定 位文件(*.o)而言,此值是所在节区内的偏移量-表示本节区在此偏移量处的存储单元里包含的内容是要被重定位修改的。对于可执行文件(*.exe)或者 共享目标文件(*.so)而言,此值是个内存虚拟地址,表示此地址处的存储单元里包含的内容是要被重定位修改的(一般这些虚拟地址都指向内存中的.got 表里)。
r_info
表示要进行重定位操作的(变量/函数)符号 在符号表(一般是.dynsym符号表)中的索引,以及将实施的重定位类型。对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。
表示要进行重定位操作的(变量/函数)符号 在符号表(一般是.dynsym符号表)中的索引,以及将实施的重定位类型。对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
note:重定位的4个属性决定了重定位的具体位置和重定位类型。
a. 重定位表.rel.text section的2个属性:
sh_link: 表示需要重定位的符号属于哪个符号表。 sh_info: 表示要修改的节区。也即需要对哪个section里的内容进行重定位处理。
b. 重定位表项的2个属性:
r_offset: 表示重定位计算出的最终地址,要填写到哪里。 r_info: 表示要重定位的符号在符号表里的索引,以及将实施的重定位类型。
note:重定位大致分2步,1. 对哪些内容 进行重新地址定位。 2. 重定位操作完成后,把结果填写到哪里保存。不同的重定位类型 情况会有所不同,一般重定位计算出的地址会填写到.got表里,以供以后符号解析使用,以后再使用这个符号时,直接查.got表就知道其最终地址,不需要 浪费时间重定位该符号的地址了。
3.7.2 重定位类型
重定位表项描述如何对指令和数据字段里的地址/指针内容进行修改。一般,共享目标文件(*.so)在创建时,其基本虚拟地址是0,但在内存中的执行地址将随着动态加载而发生变化。
3.8.1.2 基地址(Base Address)
基地址用来对程序的内存映像进行重定位。
可执行文件或者共享目标文件的基地址在执行过程中从3个数值计算的:内存加载地址、最大页面大小、程序的可加载段的最低虚地址。
程序文件头部中的虚拟入口地址(entry point)不1定是程序在内存中映像的实际虚地址。要计算内存中映像的基地址,首先要找到所有 PT_LOAD 段的最低 p_vaddr 值所对应的的内存地址。按最大页面大小的整数倍,对内存地址进行取整截断,取最接近的数值就可以得到基地址。根据文件的不同类型,加载进内存的地址可能与 p_vaddr 相同也可能不同。
如前所述,“.bss”节区的类型为 SHT_NOBITS。尽管它在文件中不占据空间,但在内存映像里却会占用空间。通常,这些未初始化的数据位于段(segment)在内存中的映像的末尾,所以 p_memsz 会比 p_filesz 大。
3.8.2 程序加载
主要技术
* 程序头部(Program Header):描述与可执行程序执行直接相关的目标文件结构信息。用来定位各个段在文件中的位置。同时包含一些为可执行程序创建内存进程映像所必需的信息。
* 程序加载:给定一个目标文件,操作系统加载该文件到内存中,启动程序执行。
* 动态链接:操作系统加载了文件以后,必须对构成进程的各个目标文件之间的符号引用进行解析,以便完整地构造进程映像。
当进程在执行过程中真正引用到相应的逻辑页面,操作系统才会请求真正的物理页面。通常进程会包含很多在执行中从来未引用过的逻辑页面, 因此,操作系统用这种延迟物理读操作会避免很多不必要的物理页面读取工作,从而提高系统性能。但要想实际获得这种效率,可执行文件和共享目标文件的段 (segment)必须符合如下条件:段在文件内的偏移量和段在内存中的虚拟地址 对页面大小取模后余数相同。
在这个例子中,文件的4个页面里包含的数据不是纯粹属于正文段或者数据段。(每个页面大小是0x1000=4096)
(1). 文件的第一个页面中包含了 ELF 头部、程序头部表、以及其它信息
(2). 文件的最后一个页面包含数据开始部分的一个副本
(3). 数据段的第一个数据页面里包含了正文段的末尾部分
(4). 数据段的最后一个数据页面里可能包含与运行进程无关的文件信息
不过系统对这些页面一般会做两次映射,以保证每个段的内存访问读写许可权限是相同的。加载到内存里时,数据段的末尾需要对未初始化数据(.bss section)进行特殊处理,操作系统应该将这些数据初始化为0。
可执行文件与共享目标文件的段加载进内存有一点不同。可执行文件的段里通常都是绝对地址的代码,为了能够让进程正确执行,段加载进内存所使用的段地址必须是连接器创建可执行文件时所使用的虚拟地址。因此系统会使用p_vaddr作为虚拟地址。
共享目标文件(*.so)的段里通常都是位置无关的代码。这使得在不同的进程内存映像中,共享目标文件的段的内存虚拟地址也不同,但并不影响其 正常执行。尽管操作系统为每个进程内存映像分配独立的虚拟地址空间,仍能维持段的相对位置。位置独立的代码在段(代码段)与段(数据段)之间使用相对寻 址,内存里虚地址之间的偏移量必须与文件中虚拟地址之间的偏移量相匹配。
下表给出同1个共享目标文件(*.so)在不同进程内存映像中的虚拟地址方案,说明了这种重定位问题。
3.8.3 动态链接
3.8.3.1 程序解释器
可执行文件中可以包含PT_INTERP类型的程序头表项目。在系统调用 exec()执行过程中,操作系统从PT_INTERP段中检索解释器文件的路径名(例如:/lib/ld-linux.so),用解释器文件创建初始的 进程映像。也就是说,操作系统并不使用原来可执行文件的段映像,而是用解释器构造一个内存映像。接下来是解释器从操作系统接收控制,为真正的应用程序创造 执行环境。
3.8.3.1 程序解释器
可执行文件中可以包含PT_INTERP类型的程序头表项目。在系统调用 exec()执行过程中,操作系统从PT_INTERP段中检索解释器文件的路径名(例如:/lib/ld-linux.so),用解释器文件创建初始的 进程映像。也就是说,操作系统并不使用原来可执行文件的段映像,而是用解释器构造一个内存映像。接下来是解释器从操作系统接收控制,为真正的应用程序创造 执行环境。
解释器可以有两种方式接收控制:
* 操作系统为解释器提供一个文件描述符,解释器读取可执行文件并将其映射到内存中。
* 根据可执行文件的格式,操作系统可能把可执行文件加载到内存中,而不是为解释器提供一个已经打开的文件描述符。
* 操作系统为解释器提供一个文件描述符,解释器读取可执行文件并将其映射到内存中。
* 根据可执行文件的格式,操作系统可能把可执行文件加载到内存中,而不是为解释器提供一个已经打开的文件描述符。
解释器可以是一个可执行文件,也可以是一个共享目标文件(例如:/lib/ld-linux.so)。
可共享目标文件(正常的情形)是位置无关地加载进内存,在不同进程中的(加载)地址也各不相同;系统把可共享目标文件的段的内存映像创建在动态 的段区域中,动态段区域被mmap(KE_OS) 和相关服务例程 所使用。因而,如果解释器是一个共享目标文件,解释器本身的加载地址将不会和(要加载的)可执行文件的初始段地址发生冲突。
可执行文件被加载到内存中固定地址,系统使用其程序头表项中的虚拟地址在内存中创建各个段映像。因此,可执行文件类型的解释器的虚拟地址可能会与(要加载的)可执行文件的虚拟地址发生冲突。解释器要自己负责解决这种冲突。
3.8.3.2 动态加载程序
连接器在构造创建使用动态链接库的可执行文件时,向可执行文件中添加一个类型为PT_INTERP的程序头表项,告诉系统要使用动态链接器作为程序解释器。动态链接器的具体路径位置是和操作系统相关的。
连接器在构造创建使用动态链接库的可执行文件时,向可执行文件中添加一个类型为PT_INTERP的程序头表项,告诉系统要使用动态链接器作为程序解释器。动态链接器的具体路径位置是和操作系统相关的。
3.8.3.4 共享库的依赖关系(共享库=动态链接库 *.so)
当连接器(/bin/ld)处理一个静态库(*.a)时,它取出库 里相关内容并且把它们拷贝到输出文件(*.so或*.exe)中。这些被静态连接的服务例程函数在程序执行期间是直接可用的,不需要动态链接器的参与。共 享库文件也提供了服务例程函数,动态连接器必须把适当的共享库内容放在进程映象中以便执行。因此,可执行文件和共享库文件记述了相互之间明确的依赖关系。
当连接器(/bin/ld)处理一个静态库(*.a)时,它取出库 里相关内容并且把它们拷贝到输出文件(*.so或*.exe)中。这些被静态连接的服务例程函数在程序执行期间是直接可用的,不需要动态链接器的参与。共 享库文件也提供了服务例程函数,动态连接器必须把适当的共享库内容放在进程映象中以便执行。因此,可执行文件和共享库文件记述了相互之间明确的依赖关系。
当动态连接器(/lib/ld-linux.so)为一个object文件创建 内存段 时,依赖关系(记录在动态连接数组中的d_tag=DT_NEEDED类型的多个数组项中)说明需要哪些动态链接库文件来为程序提供服务例程函数。依照这 些被引用的动态链接库文件和他们的依赖关系(# 用ldd程序可以读取并显示这些依赖关系),动态连接器(1次连接1个动态库)通过多次连接建立起一个完整的进程映象。当解析符号引用的时候,动态连接器 以宽度优先搜索(算法)来检查符号表,也即,动态连接器先查看可执行程序自身的符号表,然后查看 DT_NEEDED类型动态连接数组项中记录的“动态库文件”的符号表(按顺序),再接下来查看第二级DT_NEEDED类型动态连接数组项记录的“动态 库文件”的符号表,依次类推。动态库文件必须对进程是可读的;其他权限则不是必需的。
即使某个共享库文件在依赖表中出现多次,动态链接器也仅会对其连接一次。