本篇基于<
>延伸章节。
10.3.2 静态线程局部变量
10.3.3 TLS链接器优化
10.4 内核支持
10.5 编码示例
10.5.1 间接分支
11 备用代码序列的安全性
11.1 没有PLT的代码序列
11.1.1 通过GOT槽间接呼叫
11.1.2 没有PLT的线程本地存储
12 英特尔MPX扩展
12.1 参数传递和返回值
12.1.1 归类
12.1.2 通过
12.1.3 返回值
12.1.4 变量参数列表
12.2 程序加载和动态链接
附录A
A.1 32位程序的执行
A.2 AMD64 Linux内核约定
A.2.1 调用约定
A.2.2 堆栈布局
A.2.3 其他评述
附录B
B.1 合并GOTPLT和GOT插槽
B.2 优化GOTPCRELX重新定位
目录预览
对于静态线程局部变量x:
static __thread int x;
本地动态模型将x的地址加载到%rax中
or
对于具有TLSDESC的代码序列,局部动态模型与一般动态模型相似。同样的编码要求也适用于法律指令。
局部动态模型,加载值x到%edi
本地Exec模型加载x到%rax的地址
or
本地执行模型,II装载x到%edi的值
本地执行模型,III装载x到%edi的值
General Dynamic To Initial Exec加载x到%rax的地址
or
将x的值装入%edi。
本地动态到本地Exec加载x的地址到%rax
or
本地动态到本地执行,II加载x的值到%edi。
内核应该限制从系统调用返回的堆栈和地址在0x00000000到0xffffffff之间。
尽管ILP32二进制文件以64位模式运行,但并不是所有64位指令都受支持。本节讨论与64位模式不同的基本操作示例代码序列。
由于通过内存的间接分支在内存位置加载64位地址,因此在ILP32中不支持这种方式。应该改用通过寄存器的间接分支。内存中的32位地址被加载到寄存器的下32位,这将自动对寄存器的上32位进行零扩展。然后可以通过64位寄存器执行间接调用。
PLT (Linkage Table)用于访问共享对象和support中定义的外部函数,具体操作请参见5.2节
懒惰的符号解析 函数地址只有在运行时第一次被调用时才会被解析。
规范函数地址 外部函数的PLT条目被用作它的地址,也就是函数指针。
PLT表项中的第一条指令是通过全局偏移表(GOT)的间接分支,详细信息参见5.2节,外部函数的表项,它的设置是这样的,当函数第一次被调用时,它将被更新到函数体的地址。由于GOT条目是可写的,所以任何地址都可能在运行时写入它,这是一个潜在的安全风险。
对于中小型机型,采用不同的代码序列来避免PLT:
直接分支被通过GOT插槽的间接分支所取代,这类似于PLT插槽中的第一条指令。
与使用PLT槽作为函数地址不同,函数地址是从GOT槽中检索的。
如果链接器确定该函数是在本地定义的,它将通过GOT插槽的间接分支转换为带有nop前缀的直接分支,并将通过GOT插槽的加载转换为立即或lea加载,详见B.2节。
动态连接器通过更新带有符号地址的GOT表项来解析所有符号后,就可以使GOT变为只读,覆盖GOT立即成为一个硬错误。由于PLT不再用于调用外部函数,延迟符号解析被禁用,函数只能在启动时符号解析期间插入。依赖于惰性符号解析的工具和特性将无法正常工作。然而,这样做也有一些好处:
没有额外的直接分支到PLT入口 由于间接分支的长度为6字节,而直接分支的长度为5字节,当使用通过GOT插槽的间接分支来调用本地函数时,每次调用的代码大小将增加一个字节。由于一个PLT槽有16个字节,当使用通过GOT槽的间接分支调用外部函数超过16次时,代码大小将会增加。
自定义调用约定 由于外部函数是通过GOT插槽直接调用的,所以在第一次调用时,不需要调用动态连接器来查找函数符号,传递参数的方式可以与本文中指定的不同。
一般模型和本地动态模型的TLS代码序列可以通过更新来取代通过PLT条目直接调用__tls_get_addr,而通过GOT槽位间接调用__tls_get_addr,如图11.3所示。由于直接调用指令长度为4字节,间接调用指令长度为5字节,因此必须正确处理额外的一个字节。
全局变量的通用动态模型
对于一般的动态模型,在调用指令前去掉一个0x66前缀,为间接调用腾出空间:
extern __thread int x;
下面的替代代码序列加载地址x到%rax没有PLT:
静态局部变量
对于局部动态模型,使用间接调用代替直接调用:
static __thread int x;
下面的替代代码序列加载模块的TLS块的地址,其中包含变量x,到%rax没有PLT:
TLS链接器优化
由于间接调用通用动态模型的代码序列与直接调用的代码序列长度相同,因此链接器只需要识别新的指令模式就可以将通用动态访问转换为初始exec或本地exec访问。
初始执行常规动态 将x地址加载到%rax中:
本地执行常规动态 将x地址加载到%rax中:
本地动态到本地执行 对于本地动态模型到本地执行模型的转换,linker在LP64的mov指令之前生成4个0x66前缀,而不是3个;在ILP32的mov指令之前生成5字节nop,而不是4字节nop。加载模块的TLS块的地址,其中包含变量x,到%rax没有PLT:
Intel MPX(内存保护扩展)提供4个128位宽边界寄存器(%bnd0 - %bnd3)。为了传递参数和返回函数,%bndN的下64位指定相应参数的下界,而上64位指定参数的上界。上界用补码的形式表示。
增加了一个POINTER类用于传递和返回指针类型,并对INTEGER类进行了如下更新:
POINTER 该类由指针类型组成。
INTEGER 该类由适合于通用寄存器之一的整型(指针类型除外)组成。
指针和整数分为:
指针在POINTER类中。
类型(有符号和无符号)_Bool、char、short、int、long和long long的参数在INTEGER类中。
ILP32不支持MPX,因为MPX需要64位指针。
聚合(结构和数组)和联合类型的分类更新如下:
1. 当c++对象通过不可见引用传递时,形参列表中的对象将被具有pointer类的指针替换。
2. 如果其中一个类是POINTER,结果就是POINTER。
对于参数传递,如果类是INTEGER或POINTER,则使用序列%rdi、%rsi、%rdx、%rcx、%r8和%r9的下一个可用寄存器。
边界传递
Intel MPX提供了ISA扩展,允许为指针参数传递边界,指定可以通过解引用指针合法访问的内存区域。这一段描述如何将边界传递给被调用方。
下面描述中使用的几个函数定义如下:
BOUND_MAP_STORE(bnd, addr, ptr) 该函数执行Intel MPX bndstx指令。PTR参数用于初始化BNDSTX指令的内存操作数的索引字段,addr编码在内存操作数的基址和/或位移字段中,BND编码在寄存器操作数中。
BOUND_MAP_LOAD(addr, ptr) 该函数执行Intel MPX bndldx指令。PTR参数用于初始化BNDLDX指令的内存操作数的索引字段,addr编码在内存操作数的基字段和/或位移字段中。
下面的算法用来决定如何为每个8字节传递边界:
1. 如果类是INTEGER, 8字节将在通用寄存器中传递,并且被调用的函数使用可变参数或标准参数,那么类将转换为POINTER。为包含在8字节中的指针创建了允许访问所有内存的人工边界。
2. 如果类是POINTER,并且这八个字节是在通用寄存器中传递的,那么这八个字节中包含的指针关联的边界将被传递到下一个可用寄存器%bnd0, %bnd1, %bnd2和%bnd3中。如果参数的边界没有可用的绑定寄存器,那么边界将通过执行BOUND_MAP_STORE(bnd, addr, ptr)函数以CPU定义的方式传递,其中bnd是参数的当前边界,addr是被调用方返回地址位置之外的堆栈位置地址(将由相应的调用指令放在堆栈上),ptr是指针参数的实际值。对于每个调用,最多可以有两个这样的指针参数,第一个有它的边界与(<返回地址堆栈位置> - 8)地址相关联,第二个-与(<返回地址堆栈位置> - 16)相关联。
3. 如果类是POINTER并且在栈上传递八字节,或者类是MEMORY并且参数包含指针成员,那么八字节中包含的每个指针关联的边界将通过执行BOUND_MAP_STORE(bnd, addr, ptr)函数以CPU定义的方式传递,其中bnd是指针参数的当前边界,addr是指针参数堆栈位置的地址,ptr是指针参数的实际值。如果8字节可能包含部分重叠指针的部分,那么与指针相关的边界将被忽略,并为此类指针传递允许访问所有内存的特殊边界。
被调用方使用相同的算法对传入参数进行分类。如果使用BOUND_MAP_STORE将参数传递给被调用方,则被调用方使用BOUND_MAP_LOAD(addr, ptr)获取传递的边界,其中addr是传递给调用方中相应的BOUND_MAP_STORE的相同地址,ptr是被调用方从通用寄存器或堆栈位置获取的指针参数的实际值。
当向函数传递带边界的参数时,必须提供函数原型。否则,运行时行为是未定义的。
返回值已更新:
如果类是INTEGER或POINTER,则使用序列%rax, %rdx的下一个可用寄存器。
边界返回
返回边界的算法如下:
1. 使用分类算法对返回类型进行分类。
2. 如果类型有MEMORY类,返回时%bnd0必须包含调用者在%rdi中传入的“隐藏”第一个参数的边界。
3. 如果类是POINTER,序列%bnd0, %bnd1, %bnd2, %bnd3的下一个可用寄存器用于返回包含在8字节中的指针的边界。
作为绑定传递约定的一个例子,考虑图12.2中所示的声明和函数调用。图12.3给出了相应的绑定寄存器分配,给出的堆栈帧偏移显示了调用函数前的帧。
传递变量参数的方式更新如下:
寄存器保存区
如果带变量参数列表的函数是为Intel MPX编译的,那么通过执行BOUND_MAP_STORE(bnd, addr, ptr)函数(12.1.2),将保存到寄存器保存区的参数寄存器传递的边界保存到prolog中的每个参数寄存器,其中bnd是指针参数的当前边界,addr是参数寄存器在寄存器保存区的位置的地址,ptr是参数寄存器的实际值。
va_arg宏
va_arg(l, type)的实现更新如下:
1. 从l->reg_save_area中获取类型,偏移量为l->gp_offset和/或l->fp_offset。如果参数在不同的寄存器类中传递,这可能需要将其复制到一个临时位置,或者需要对一般用途寄存器的比对大于8,对XMM寄存器的比对大于16。如果type指定了一个指针,那么被获取的参数的边界通过执行BOUND_MAP_LOAD(l->reg_save_area + l->gp_offset, ptr)(12.1.2)来加载,其中ptr是从l->reg_save_area中获取的实际值,偏移量为l->gp_offset。
为了在带有BND (0xf2)前缀的分支指令中保留用于符号查找的绑定寄存器,链接器应该生成BND过程链接表(见图12.4)和一个附加的过程链接表(见图12.5)。
在调用func()之前,调用的返回地址还没有放在堆栈上,因此偏移-16帐户将由调用指令进行返回地址的推送。
为了支持具有BND (0xf2)前缀的间接分支(见图12.6),所有BND过程联动表项中的分支都必须具有BND (0xf2)前缀。
当使用BND过程联动表时,外部函数的全局偏移表项的初始值为附加过程联动表对应表项的地址。
Linux约定
本章描述了一些只与GNU/Linux系统和Linux内核相关的细节。
AMD64处理器能够执行64位AMD64和32位ia32程序。符合Intel386 ABI的库将位于/lib、/usr/lib和/usr/bin等正常位置。AMD64之后的库将使用lib64子目录作为库,例如/lib64和/usr/lib64。符合Intel386 ABI和AMD64 ABI的程序将共享像/usr/bin这样的目录。特别是,没有/bin64目录。
本节仅供参考。
Linux AMD64内核内部使用与用户级应用程序相同的调用约定(详见3.2.3节)。喜欢调用系统调用的用户级应用程序应该使用C库中的函数。C库与Linux内核的接口与用户级应用的接口相同,不同之处在于:
1. 用户级应用程序使用整数寄存器来传递序列%rdi、%rsi、%rdx、%rcx、%r8和%r9。内核接口使用%rdi、%rsi、%rdx、%r10、%r8和%r9。
2. 系统调用是通过系统调用指令完成的。内核销毁寄存器%rcx和%r11。
3. 系统调用的编号必须在寄存器%rax中传递。
4. 系统调用限制为6个参数,没有参数直接传递到堆栈上。
5. 从系统调用返回,寄存器%rax包含系统调用的结果。-4095 ~ -1表示错误,取值为-errno。
6. 只有类INTEGER或类MEMORY的值被传递给内核。
Linux内核可能会将输入参数区域的末尾对齐到8字节边界,而不是16字节边界。它不属于红色区域(参见3.2.2节),因此内核代码不允许使用这个区域。内核代码应该由GCC使用-mno-red-zone选项进行编译。
Linux内核代码不允许更改x87和SSE单元。如果它们被内核代码更改了,那么必须在休眠或离开内核之前正确地恢复它们。在先发制人的内核上也可能需要更多的预防措施。
链接器优化
本章描述了可由链接器执行的优化。
在中小型模型中,当对同一个函数符号同时有PLT和GOT引用时,通常连接器会为PLT条目创建一个GOTPLT槽,为GOT引用创建一个GOT槽。创建一个运行时JUMP_SLOT重定位来更新GOTPLT槽,并创建一个运行时GLOB_DAT重定位来更新GOT槽。在运行时,JUMP_SLOT和GLOB_DAT重定位分别对GOTPLT和GOT槽位应用相同的符号值。
作为一种优化,链接器可以将GOTPLT和GOT槽合并到单个GOT槽中,并删除运行时的JUMP_SLOT重定位。它取代了常规的PLT条目:
通过GOT插槽间接跳跃的GOT PLT条目:
并将PLT引用解析为GOT PLT条目。间接jmp是一个5字节指令。nop可以被编码为3字节指令或11字节指令,用于8字节或16字节的PLT插槽。一个带有8字节槽的单独PLT可用于此优化。
这种优化不适用于STT_GNU_IFUNC符号,因为它们的GOTPLT槽被解析为所选的实现,它们的GOT槽被解析为它们的PLT条目。
如果需要指针相等,则必须避免这种优化,因为在这种情况下,符号值不会被清除,动态连接器也不会更新GOT槽。否则,生成的二进制文件将在运行时进入无限循环。
AMD64指令编码支持使用R_X86_64_GOTPCRELX或R_X86_64_REX_GOTPCRELX对符号foo的重定位将内存操作数上的某些指令转换为另一种形式的即时操作数(如果foo是在本地定义的)。
Convert call and jmp 将call和jmp的内存操作数转换为即时操作数。
Convert mov 将mov的内存操作数转换为即时操作数。当位置无关代码被禁用,foo在本地低32位地址空间中定义时,mov中的内存操作数可以转换为即时操作数。否则,必须将mov改为lea。
Convert Test and Binop 将test和binop的内存操作数转换为即时操作数,其中binop是adc, add, and, cmp, or, sbb, sub, xor指令中的一个,当位置无关代码被禁用时。
<
<