目标文件的格式:可重定位文件(.o与.obj),可执行文件(/bin/bash与.exe),共享目标文件(.so与DLL),核心转移文件(core dump)
包含:代码段(.text或.code)、数据段(.data,已初始化的数据)、BSS段(未初始化的全局变量和局部静态变量)其他段(.rodata,.comment(编译器和系统版本信息))等
段表:readelf -h 文件名,可以查看段表头的信息,-a,-h,-S等指令可以用于查看信息。
重定位表:(.rel.text)
字符串表:(.strtab)偏移引用
链接的接口——符号,函数和变量统称为符号,函数名或变量名称为符号名。
函数签名:包含了一个函数的信息,包括参数类型,函数名,它所在的类和名称空间及其他信息。
修饰后和修饰前很大不同:int C::func(int)转变后——>_ZN1C4funcEi
编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号,延伸出强引用和弱引用。
ELF文件Magic:0x7f,0x45,0x4c,0x46,0x01(决定是32位还是64位),0x01(决定小大端序)0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0
a.out的魔数为0x01,0x07,
编译器(cl)
静态链接:
一、空间与地址分配:
1、按序叠加
2、相似段(执行权限)合并(linux下,ELF可执行文件默认从地址0x8048000开始分配)
3、符号地址的确定
二、符号解析与重定位:
1、重定位:刚开始使用的都是VMA(虚拟地址),当真正链接时,就会用到真实地址了,这个过程叫重定位。
2、重定位表:.rel.text和.rel.data都是重定位表。
3、符号解析:链接器通过查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
4、指令修正方式:(x86基本重定位类型)
绝对寻址修正(R_386_32,计算是:实际地址+被修正位置的值),得到是该符号的实际地址,相对寻址修正(R_386_PC32,计算是:实际地址+被修正位置的值-被修正的位置),得到的是该符号距离被修正位置的地址差。
COMMON块:由于链接器不支持符号类型,即链接器无法判断各个符号类型是否一致。未初始化的全局变量暂时存在COMMON块中。
ELF文件中两个段:.init和.fini这两个段,一个是存初始化代码,一个是存终止代码。
讨论了一些C++编译器对于二进制代码的兼容性的问题,
ld链接器用于链接目标文件(xxx.o),使用ld链接脚本就相当于把目标文件看成是输入段,然后通过链接变成输出段,即可执行文件。
BFD库相当于一种抽象的接口,支持将近50种目标文件格式,25种处理器平台。
COFF文件是PE和ELF的老祖宗,COFF文件中有一个段叫“.drectve段”,这个段保存的是编译器传递给链接器的命令行参数,可以通过这个段实现指定运行库等功能。.debug是调试信息,最后还有符号表。PE文件结构兼容DOS"MZ"可执行文件结构,被称为改进版本的COFF。
装载与动态链接:
一、进程虚拟地址空间(以32位4G为例)
Intel的新型地址扩展方式:PAE,访问内存的一种方式:AWE
装载的方式:动态装入的基本原理:把最常用的装入内存,不太常用的放在磁盘。
覆盖装入(Overlay)(大地址可以覆盖小地址)和页映射(Paging)(页面置换算法,FIFO 和LUR)是两种典型的动态装载方法。
进程最关键的特征是它拥有独立的虚拟地址空间,往往创建时有3步:
1、创建一个独立的虚拟地址空间(VMA,有个虚拟段)
2、读取虚拟的可执行文件头,并且建立虚拟空间与可执行文件的映射关系(装载时用)
3、将CPU的指令寄存器设置成为可执行文件的入口地址,启动运行(移交控制权)
页错误(也叫缺页处理)
二、进程虚存空间分布
ELF文件链接视图和执行视图:
对于相同权限的段,把它们合并到一起当做一个段进行映射。(Segment)
堆栈与堆的空间申请(堆向上扩展(高地址),栈向下扩展(低地址))
段地址对齐:
可执行文件最终是要被操作系统装载运行的,这个装载过程一般是通过虚拟内存的页映射机制来完成。只要不超过4096B,就可以合并多个段在一个段中,地址记得取4的整数倍,然后每一个要进行对齐操作,重合的段映射了两次。
ELF和PE的装载过程~
动态链接:(Dynamic Linking):把程序模块拆分成相对独立的部分,把链接这个过程推迟到运行时再进行,有共享模块,可动态加载程序模块(插件由来)。
静态链接浪费内存空间,不方便更新,于是动态链接应运而生。
问题:DLL Hell(导致其他程序无法使用)
Linux下的动态共享对象(.so),windows下的动态链接库(.dll)
C语言glibc库的动态链接形式就是libc.so,由动态链接器完成程序与libc.so的链接工作。
延迟绑定机制(Lazy Binding)
ld-2.6.so(linux下的动态链接器)
装载时重定位(基址重置):-shared,主要解决动态模块中有绝对地址引用,但是指令部分无法在多个进程中共享。
地址无关代码(PIC):
call E8 FF FF FF(目的地址相对于下一条指令的偏移);
mov eax,8;
(相对偏移调用指令,由于小端序,所以偏移为0xFFFFFFE8)
4种情况下:
一、模块内部的函数调用、跳转等
二、模块内部的数据访问,比如模块中定义的全局变量、静态变量。
三、模块外部的函数调用、跳转等
四、模块外部的数据访问,比如其他模块中定义的全局变量。
GOT表:全局偏移表(指向变量的指针数组),在模块间的数据访问中会用到。
链接器在装载模块时查找每个变量所在的地址,然后填充GOT中的各个项,确保指针指向的地址正确,GOT本身放在数据段,可在模块装载时被修改。产生地址无关代码指令(-fPIC)。gcc编译时,动态链接器默认使用地址无关代码技术(PIC)
延迟绑定(PLT):当函数第一次被用到时才进行绑定,没有用则不绑定。
bar@plt:
jmp *(bar@GOT)
push n
push moduleID
jump _dl_runtime_resovle
为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入该项,而是将push n的地址填入到bar@GOT,很明显这样一来,jmp *(bar@GOT)的功能就是跳转到push n的位置,相当于没有进行任何操作。然后就是参数入栈,调用函数_dl_runtime_resolve()进行一系列的工作(包括调用该bar函数),最后完成符号解析和重定位工作,最终把bar()的真实地址填入到bar@GOT中。第二次调用bar@plt时,bar函数返回时会根据堆栈保存的EIP直接返回到调用者就不再继续执行那个push n什么的了。
ELF将GOT分为:
.got:保存全局变量引用的地址
.got.plt:保存函数引用的地址
第二项和第三项看是0,实际上在装载共享模块时才初始化
linux下动态链接中,操作系统加载完ld.so(动态链接器)后,把控制权给ld.so的入口地址,ld.so完成对可执行文件的链接工作后再把控制权给可执行文件的入口地址,程序开始正式执行。
ldd 文件:可以查看可执行文件的所有依赖共享库。
动态链接相关结构:
.interp段:存放动态链接器的地址
.dynamic段:ELF“文件头”
.dynsym段:动态符号表,只保存与动态链接相关的符号
动态链接重定位表:.rel.text(代码段的重定位表),.rel.data(数据段的重定位表)
*(我们知道数据段是无法做到地址无关的,他可能会包含绝对地址的引用)
动态链接时进程堆栈初始化信息:。。。
动态链接基本分为3步:
1、启动动态链接器本身
2、装载所有需要的共享对象
3、重定位和初始化
动态链接器的自举(自身独立完成内部任务)
装载共享对象(全局符号表),全局符号介入(覆盖第二次进来的相同的符号)
.init(初始化),.finit(退出)、/lib/ld-linux.so.2是个软链接,它指向/lib/ld-x.y.z.so,这个才是真正的动态链接文件,是可执行的其实,可以直接./运行。
显式运行时链接:也叫运行时加载
dlopen()、dlsym()、dlerror()、dlclose()
符号优先级:装载序列(先装入的优先),依赖序列(广搜)
libc.so.x.y.z(主,次,发布版本号)
共享库系统路径:FHS规定
/lib
/usr/lib
/usr/local/lib
环境变量:
LD_LIBRARY_PATH
共享库的创建和安装