第二章 静态链接
疑问:
- 问什么静态链接不会把所有代码链接进程序
- 为什么要静态链接
被隐藏的过程
gcc helloc.c 包含了 预处理、编译、汇编、链接4个过程
编译
编译是经过词法分析、语法分析、语义分析等操作后生成汇编代码文件。执行gcc命令时,会根据参数调用预编译程序,Object-C的就是ccl obj c是ccl
汇编
汇编器将汇编文件转成机器码。汇编器as。.o结尾的一般就是汇编器最后生成的目标文件。
中间语言(源码级优化器)
编译器分为前端和后端,前端生成与机器无关的中间代码,后端将中间代码转为机器码
目标代码生成与优化
编译器后端包括代码生成器和目标代码优化器
- 代码生成器将中间语言转成目标机器代码
- 优化器将上述代码优化,如删除多余指令,选择合适寻址方式
静态链接
不同模块需要知道其他模块函数地址,变量地址,需要通过链接的过程确认。
链接包括
- 地址和空间分配
- 符号决议
- 重定位(将未链接时的占位地址(书中是0)在链接后替换成真正的目标地址)
注:后面 .o的文件都称为目标文件
第三章 目标文件
目标文件如上面说的,是汇编过程生成的,从结构上,他跟最后的可执行程序是差不多的,只是没进行链接的过程,有些符号和地址还没被调整。可以说,目标文件的格式和可执行的文件格式是一样的。Window下为PE(.exe) Linux下为ELF,动态链接库和静态链接库的格式也是。
目标文件组成
目标文件的段
- 机器指令码(代码段,.text)
- 数据(数据段,以初始化在.data,未初始化在.bss)
- 符号表
- 调试信息
- 字符串等
BSS段
存放未初始化的全局变量和局部静态变量
EFL文件描述
ELF文件最开头为统一魔数 0x7f 0x45 0x4c 0x4, 0x7f为ASCII里的DEL控制符,后三个为ELF字母的ASCII(a.out的魔数为0x01 )
重定位表
ELF文件里.text .data段一般都对应一个重定位表段,用来在链接器处理目标文件时,对代码段和数据段中那些对绝对地址引用的位置进行重定位
字符串表
将字符串集中存放到一个表,再用偏移量来引用不同的字符串。字符串表用来保普通的字符串,如符号的名字
链接的接口----符号
称函数名和变量名为符号名。每一个目标文件都有对应的一个符号表,这个表定义了目标文件中用到的所有符号,每个定义的符号都有个对应的值,叫符号值,对于变量和函数来说,符号值就是他们的地址,符号包含:
- 定义在本目标文件的全局符号,可以被其他引用
- 本目标文件的全局符号,却没有定义在本目标文件,称外部符号
- 段名
- 局部符号(编译单元内部,局部变量名),局部符号对链接过程无用,
- 行号信息
链接过程只关系全局符号的相互引用
ELF符号表结构
符号表段名一般为".symtab",符号表的结构为一个Elf32_Sym结构体数组。
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;
符号修饰与函数签名
如C语言源代码中的 全局变量和函数经过编译后,相对应的符号名前会加下划线(如果是GCC,不会加入下划线),主要用来减少符号冲突,C++则有名称空间
C++符号修饰
C++里语言特性比较复杂,如重载func(int)和func(double),对于这些复杂的语法,使用符号修饰或符号改编的机制。C++函数名只是函数签名一部分,C++将源代码编译时,会将函数名和变量的名字进行修饰,形成符号名。
如
- int func(int) _Z4funci
- int C::func(int) _Z4funcf
上面的例子包含了命名空间,函数名,变量类型等信息,形成符号名,对于全局变量,规则也是一样,只是全局变量没有变量类型信息。
extern "C"
C++为了与C兼容,在符号管理上,C++用extern "C"来声明或定义一个C的符号,括号内的代码会当作c语言来处理,c++的机制会不起作用。
如果一个c函数库被C++文件引用,C++文件里调用c函数库里的函数就会被认为是调用C++的函数,会使用C++的符号修饰,如memset,会被修饰成_Z6memsetPvii,这样链接器就无法与C语言库中的memset符号进行链接。一个好的方法是使用_cplusplus宏
#ifdef __cplusplus
extern "c" {
#endif
void *memset (void *, int , size_t)
#ifdef __cplusplus
}
#endif
调试信息
ELF文件段中有debug信息的段。ELF文件采用DWARF(Debug With Arbitrary Record Format)的标准调试信息格式。调试信息往往必代码和数据本身大几倍,realse时记得去掉调试信息
第四章 静态链接
空间与地址分配
将两个不同的目标文件合并成一个可执行文件,一般会将他们ELF文件的同名段合并,一般采用两步链接法
- 空间与地址分配(扫描所有输入文件,合并所有符号,计算输出文件各个段合并后的长度与位置,并建立映射关系)
- 符号解释与重定位 (链接后的地址是进程中的虚拟地址,Linux下ELF可执行文件的默认地址从0x08048000开始分配)
符号地址的决定
合并后的符号可以根据偏移量进行计算
符号解析与重定位
重定位
目标文件里代码段引用的外部函数和变量,一般以0填充,链接过后,会替换成链接后确定的虚拟地址
重定位表
ELF文件里可以包含一个或者多个重定位表(段),如.text的段的重定位表一般为.rel.text,.data的一般对于.rel.data.重定位表是Elf32_Rel的结构数组
typedef struct {
Elf32_Addr r_offset; //重定位入口的偏移,表示该入口在要被重定位的段中的位置,简单来说就是代码要被调整的位置
Elf32_Word r_info //低8位表示重定位入口类型,高24位表示重定位入口的符号在符号表中的下标
} Elf32_Rel
符号解析
链接时,链接器会对需要重定位的符号(在段里的类型为GBLBAL),在合并后的符号表里寻找对应的符号,如果没找到,会报符号未定义的错误
指令修正方式
指令修正方式很多种,对于32位x86平台下的ELF只有两种
- 绝对近址32位寻址 R_386_32 (S+A)
- 相对近址32位寻址 R_386_PC32 (S+A-P)
S是符号实际的位置,A是被修正未知的值,P是被修正位置的虚拟地址
common块
common块机制用于处理弱符号冲突问题,当链接器检测到多个文件里有不同类型的相同定义的全局变量的符号,会以最大类型的大小分配空间。
为什么不给未定义的全局变量在BSS段分配空间,而给未初始化的局部静态变量分配?
因为编译器将编译单元编译成目标文件时,弱符号所占空间的大小是未知的,只有经过链接阶段后才知道,最终链接后的BSS段,还是有占空间的
C++相关问题
重复代码消除
C++编译过程,模板、外部内联函数和虚函数表可能在不同的编译单元产生相同的代码
函数级别链接
让所有函数单独保存到一个段,当链接器需要用到某个函数时,他就合并到输出文件中,对于没用的函数则抛弃
全局构造与析构
C++全局构造在main函数执行前,全局析构在main函数执行完再执行。对此ELF文件还有两个对应的段
- .init main函数调用前需要执行的指令
- .fini main函数结束后需要执行的指令
C++与ABI(Application Binary Interface)
把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI。ABI相互不兼容,目标文件无法相互链接,受硬件平台、编程语言、编译器、链接器和操作系统影响
静态库链接
.a静态库其实是由“ar”压缩程序将所有.o文件打包起来,并且对其进行编号和索引。
如何在众多目标文件里找到指定的目标文件?可以使用"objdump"或者"readelf"加上文本查找grep
QA
为什么静态运行库里的一个目标文件只包含一个函数?如libc.a里面的printf.o只有print函数?
链接器在链接阶段以目标文件为单位,如果一个目标文件里函数过多,会容易链接很多无用的目标文件
链接过程控制
链接控制脚本
最小的程序
TinyHelloWorld.c
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o
上面两段,是使用gcc将c文件编译成目标文件,链接器使用静态链接方式,指定nonmain函数为入口并输出为可执行文件
使用ld链接脚本
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -T TinyHelloWorld.lds(脚本) -o TinyHelloWorld TinyHelloWorld.o
链接控件脚本可以合并或去除输入文件里ELF文件里各段,就是可以指定链接过程段的转换
BFD库
统一接口处理不同的目标文件格式,以适应各种不同软硬件平台
第六章 可执行文件的装载与进程
进程虚拟地址控件
虚拟地址空间一般由硬件平台决定,现在一般为32位或者64位,决定了地址空间的上限,一般c语言指针大小的位数与虚拟空间的位数一样
pae
通过pae(Physical Adress Extensio) ,如32位cpu有36地址线,可以通过映射程序扩展内存访问
装载的方式
覆盖装入
比较古老的技术,互相不依赖的模块可共用内存,用到哪个模块加载哪个模块,如果原有内存有一个模块,新的模块将覆盖他
页映射
将内存和磁盘中的数据和指令按页为单分成若干个页,页的大小一般为4096字节、8192字节、2MB、4MB等,也是用到哪个加载哪个,如果内存满了,将按如FIFO,LUR等法则替换内存
从操作系统角度看可执行文件的装载
进程的建立
从操作系统的角度,一个进程拥有独立的地址空间。创建一个进程,然后装载相应的可执行文件并且执行这个过程的步骤有
- 创建一个独立的虚拟地址空间(由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,是虚拟空间到物理内存的映射关系)
- 读取可执行文件头,并且建立虚拟空间与控制可执行文件的映射关系。(这种映射关系只是保存在操作系统内部的一个数据结构)
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行(ELF文件头中保存的入口地址)
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件(Image)
Linux中将进程虚拟地址空间中的一个段叫做虚拟内存区域(VMA,Virtual Memory Area)
页错误
上面三个步骤执行完,可执行文件真正的指令和数据都没有被装入内存中。操作系统只是通过可执行文件的头部信息建立起可执行文件和进程虚拟内存之间的映射关系。cpu开始执行指令时,发现入口是个空页面,就会认为是一个页错误(Page Fault)。CPU将控制权交给操作系统,操作系统将查询虚拟内存与可执行文件映射的数据结构,找到空页面在可执行文件的偏移,然后在物理内存分配一个物理页面,将进程中的虚拟页与物理内存页建立映射关系(MMU)。
进程虚存空间分布
ELF文件链接视图与执行视图
ELF文件映射到虚拟内存时,是以页面为单位的,但是ELF文件有多个section,如果一个section单独一个VMA占多个页时,容易出现内存碎片,因此,系统在装载可执行文件时,会根据section的读写权限来划分虚拟内存区域(VMA)。ELF文件section的读写权限一般有:
- 以代码段为代表的权限可读可执行的段
- 以数据段和BSS段为代表的权限为可读可写的段
- 以制度数据为代表的权限为只读的段
像上面那种将多个ELF section合并到一起的概念叫做 segment,在将目标文件链接成可执行文件的时候,链接器会尽量把权限相同的段分配在同一个空间,因此,系统是按segment来映射可执行文件的,而不是ELF文件中的section。总的来说,Segment和Section是从不同角度来划分ELF文件,Section的角度来看就是链接视图,Segment的角度是执行视图
堆和栈
VMA除了被用来映射可执行文件各个segment外,进程中的堆和栈也被映射成VMA,这种VMA称为匿名虚拟内存区域(Anonymous Virtual Memory Area)
堆的最大申请数量
Linux下虚拟地址空间给 进程本身的是3GB,windows 是2GB,但实际的大小受动态库数量、程序本身大小和操作系统版本等各种因素影响
第七章 动态链接
把链接这个过程推迟到运行时再进行,就是动态链接(dynamic Linking)的基本思想
动态链接的基本实现
Linux中,ELF动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects),一般以.so结尾
Windows中,动态链接文件被称为动态链接库(Dynamical Linking Library),一般以.dll结尾
程序与动态链接库直接链接的工作由动态链接器完成,而不是静态链接库的由ld完成。动态链接把链接过程由程序装载前推到程序装载后。但每次装载时链接动态库,会有一点时间的消耗,,但是这个过程可以优化,比如延迟绑定(lazy binding)等方法
动态链接程序运行时地址空间分布
在整个虚拟地址空间中,会多处引用的动态库和动态库链接器部分。在系统开始运行程序时,首先会把控制权交给动态链接器,由它完成所有的动态链接工作后再把控制器交给程序。动态链接模块在ELF文件中的装载地址是无效地址0x000000000,但最终装载地址并不是这个
地址无关代码(PIC,Position-independent Code)
装载时重定位
程序模块在编译时目标地址不确定时需要在装载时重定位。称装载时重定位(Load Time Relocation)
地址无关代码
装载时重定位无法解决共享动态链接库绝对地址引用的问题。解决这问题方案为地址无关代码技术(Position-independent Code),让共享指令部分在装载时不需要因为装载地址的改变而改变,基本思想就是将这部分指令分离出来,跟数据部分放在一起(数据部分可以拷贝副本,不同程序因此可以任意修改)
地址无关代码的地址引用方式
指令跳转、调用 | 数据访问 | |
---|---|---|
模块内部 | 相对跳转和调用 | 相对地址访问 |
模块外部 | 间接跳转和调用(GOT) | 间接访问(GOT) |
上面的GOT称为全局偏移表(Global Offset table),专门用于其他模块数据和指令调用的映射表,就是上面说的,存在数据段的映射表,当代码需要引用全局变量时,可以通过GOT中对应的项间接引用
共享模块的全局变量问题
当一个可执行文件引用一个动态链接库的全局变量时,可执行文件是无法使用GOT的,编译时就要确认全局变量的地址。解决方法是当共享模块装载时,如果某个全局变量在可执行文件中拥有副本,动态链接器会把GOT中的相应地址指向该副本。如果全局变量在程序主模块中没有副本,那么将执行GOT
数据段地址无关性
static int a;
static int *b = &a
如果共享对象存在上面的代码,会产生绝对地址的引用。对于这种绝对地址引用,使用装载时重定位的方法,编译器和链接器会产生一个重定位表,包含“R_386_RELATIVE”类型的重定位入口,动态链接器就会对该共享对象进行重定位。
装载时重定位比GOT要快,省去了GOt中每次访问全局数据和函数时做一次计算当前地址以及间接地址的寻址过程
延迟绑定
ELF使用PLT(Procedure Linkage Table)的方法实现,当调用某个外部模块函数时,常规做法是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定,加了一个中间跳转,不直接通过GOT,而是通过一个PLT结构进行跳转。原理是调用一个外部函数,PLT会调用_dl_runtime_relsove,传入模块id和函数来进行解析。
动态链接相关数据结构
.interp 段
interpreter缩写,表示解析器,ELF里面的一个段,存放动态链接器的路径字符串
.dynamic段
.dynamic段跟静态链接的ELF头文件信息类似,包含了动态链接符号表的地址、动态链接字符串表地址、依赖的共享对象文件名等
动态符号表
为了表示动态链接这些模块之间的符号导入导出关系,ELF专门有一个焦动态符号表(Dynamic Symbol Table)来保存这些信息,这个段为.dynsym。与.symtab保存所有符号,包括内部变量,而.dynsym只保存动态链接相关的符号。
.dynsym的辅助表有动态符号字符串表和符号哈希表(用于提高程序运行时符号查找速率)
动态链接重定位表
动摇链接的可执行文件使用PIC方法,代码段都是地址无关,但是数据段里的GOT需要重定位,数据段里一些绝对地址引用也需要
动态链接重定位相关结构
重定位表
代码段 | 数据段 | |
---|---|---|
静态链接 | .rel.text | .rel.data |
动态链接 | .rel.dyn | .rel.plt |
.rel.dyn修正的位置位于.got以及数据段 ,.rel.plt用于修正.got.plt