内容简介:4. ABI标准ABI表示应用程序二进制接口(Application Binary Interface)。ABI是函数如何调用,参数与返回值如何传递,允许函数改变哪些寄存器的标准。在合并汇编与高级语言时,遵守适合的ABI标准是重要的。调用惯例等的细节涵盖在手册5《不同C++编译器与操作系统的调用惯例》中。这里,为了您的方便。汇总了最重要的规则。4.1. 寄存器的使用
4. ABI标准
ABI表示应用程序二进制接口(Application Binary Interface)。ABI是函数如何调用,参数与返回值如何传递,允许函数改变哪些寄存器的标准。在合并汇编与高级语言时,遵守适合的ABI标准是重要的。调用惯例等的细节涵盖在手册5《不同C++编译器与操作系统的调用惯例》中。这里,为了您的方便。汇总了最重要的规则。
4.1. 寄存器的使用
16 位 DOS , Windows |
32 位 Windows , Unix |
64 位 Windows |
64 位 Unix |
|
可以自由使用的寄存器 |
AX , BX , CX , DX , ES , ST(0) - ST(7) |
EAX , ECX , EDX , ST(0) - ST(7) , XMM0-XMM7, YMM0-YMM7 |
RAX , RCX , RDX , R8 - R11 , ST(0) - ST(7) , XMM0-XMM5, YMM0-YMM5, YMM6H-YMM15H |
RAX , RCX , RDX , RSI , RDI , R8 - R11 , ST(0)-ST(7) , XMM0-XMM15, YMM0-YMM15 |
必须保存及恢复的寄存器 |
SI, DI, BP, DS |
EBX, ESI, EDI, EBP |
RBX, RSI, RDI, RBP, R12-R15, XMM6-XMM15 |
RBX, RBP, R12-R15 |
不能改变的寄存器 |
DS, ES, FS, GS, SS |
|||
用于参数传递的寄存器 |
(ECX) |
RCX, RDX, R8,R9, XMM0-XMM3, YMM0-YMM3 |
RDI, RSI, RDX, RCX, R8, R9, XMM0-XMM7, YMM0-YMM7 |
|
用于返回值的寄存器 |
AX, DX, ST(0) |
EAX, EDX, ST(0) |
RAX, XMM0, YMM0 |
RAX , RDX , XMM0 , XMM1 , YMM0 ST(0) , ST(1) |
表14.1. 寄存器的使用
在进行任何调用或返回前,浮点寄存器ST(0) ~ ST(7)必须是空的,除了用于函数返回值。在任何调用或返回前,必须通过EMMS清零MMX寄存器。在任何调用或返回到非VEX代码前,必须通过VZEROUPPER清零YMM寄存器。
算术标记可以自由改变。在32位与64位系统中,方向标记可以临时设置,但在任何调用或返回前,必须清除。在受保护的操作系统中,中断标记不能被清除。浮点控制字与MXCR寄存器的比特6 ~ 15,在修改它们的函数中必须被保存与恢复。
寄存器FS与GS用于线程环境块等,不应该被改变。其他段寄存器,除了在分段16位模型里,也不应该被改变。
-
- 数据存储
在C或C++中的一个函数里声明的变量与对象,保存在栈上,并相对于栈指针或栈框取址。出于两个原因,这是保存数据最高效的方式。首先,在该函数返回时,用于局部储存的栈空间被释放,可被下一个调用的函数重用。反复使用相同的内存区域改进了数据缓存。第二个原因是,保存在栈上的数据,通常可以通过相对于一个指针的8比特偏移来访问,而不是数据段中访问数据要求的32位。这使得代码更紧凑,因此需要更少的代码缓存或追踪缓存空间。
C++中的全局与静态数据保存在数据段中,在32位系统中以32位绝对地址,在64位系统中以32位RIP相对地址来访问。在C++中保存数据的第三种方式是使用new或malloc分配空间。如果速度是关键,应该避免这个方法。
4.2. 函数调用惯例
16 位模式DOS 与Windows 3.x 中的调用惯例
函数参数在栈上传递,第一个参数在最低地址处。
这对应首先压入最后的参数。栈由调用者清理。8或16比特大小的参数使用一个字的栈空间。超过16比特的参数以little-endian的形式保存,即最低位的字在最低地址。所有栈参数对齐到2。
在大多数情形里,函数返回值在寄存器中传递。8位整数在AL中返回,16位整数与近指针在AX,32位整数与远指针在DX:AX,布尔在AX,浮点值在ST(0)。
32 位Windows ,Linux ,BSD ,Mac OS X 中的调用惯例
根据以下调用惯例,函数参数在栈上传递:
调用惯例 |
栈上的参数序 |
参数的删除者 |
__cdecl |
第一个参数在最低地址 |
调用者 |
__stdcall |
第一个参数在最低地址 |
例程 |
__fastcall Microsoft与Gnu |
前两个参数在ecx,edx。余下同__stdcall |
例程 |
__fastcall Borland |
前三个参数在eax,edx,ecx。余下同__stdcall |
例程 |
_pascal |
第一个参数在最高地址 |
例程 |
__thiscall Microsoft |
This在ecx。余下同__stdcall |
例程 |
表4.2. 32位模式中的调用惯例
在 Linux 中,__cdecl调用惯例是默认的。在Windows中,__cdecl调用惯例也是缺省的,除了对成员函数,系统函数与DLL函数。在.obj与.lib文件中静态链接的模块最好使用__cdecl,而在.dll文件里的动态链接库应该使用__stdcall。对Windows下的成员函数,Microsoft、Intel、Digital Mars与Codeplay编译器缺省使用__thiscall,Borland编译器使用__cdecl,this作为第一个参数。
对具有整数参数的函数,最快的调用惯例是__fastcall,但这个调用惯例不是标准的。
记住在栈上压入一个值时,栈指针是递减的。这意味着第一个压入的参数在最高地址,符合_pascal惯例。你必须反序压入参数以满足__cdecl与__stdcall惯例。
32位或更小的参数使用4字节栈空间。超过32位的参数以little-endian的形式保存,即最低位的字在最低地址,并对齐到4。
Mac OS X与Gnu编译器3及更新版本在每条调用指令前对齐栈到16,尽管这个行为不一致。声明其他对齐是可能的,这会导致不兼容。参考手册5《不同C++编译器与操作系统的调用惯例》。
在大多数情形里,函数返回值在寄存器中传递。8位整数在AL中返回,16位整数在AX,32位整数、指针、引用与布尔在EAX,64位整数在EDX:EAX,浮点值在ST(0)。
关于复合类型(struct、class、union)及向量类型(__m64、__m128、__m256)参数的细节参考手册5《不同C++编译器与操作系统的调用惯例》。
64 位Windows 中的调用惯例
如果是整数,第一个参数在RCX中传递,如果是float或double,在XMM0中。第二个参数在RDX或XMM1中传递。第三个参数在R8或XMM2中传递。第四个参数在R9或XMM3中传递。注意,如果使用了XMM0,RCX就不用于参数传递。不超过4个参数可以在寄存器中传递,无论类型。更多的参数在栈上传递,第一个参数在最低地址处且对齐到8。成员函数有作为第一个参数的this。
除了在栈上传递的参数,调用者还必须在栈上分配32字节的空闲空间。这是一个影子空间(shadow space),如果需要,被调用函数可以在其中保存四个参数寄存器。影子空间是保存前四个参数的地方,如果它们根据__cdecl规则在栈上传递。影子空间属于被调用函数,它被允许在影子空间里保存参数(或别的任何东西)。调用者必须保留32字节的影子空间,即使函数没有参数。调用者必须清理栈,包括影子空间。返回值在RAX或XMM0中。
在任何CALL指令前,栈指针必须对齐到16,因此RSP的值,在函数入口,是8模16。在将XMM寄存器保存到栈时,函数可以依赖这个对齐。
关于复合类型(struct、class、union)及向量类型(__m64、__m128、__m256)参数的细节参考手册5《不同C++编译器与操作系统的调用惯例》。
64 位Linux 、BDS 与Mac OS X 中的调用惯例
前六个参数分别在RDI,RSI,RDX,RCX,R8,R9中传递。前八个浮点参数在XMM0 ~ XMM7中传递。所有这些寄存器都可以使用,因此最多14个参数可以在寄存器中传递。更多的参数在栈上传递,第一个参数在最低地址处且对齐到8。如果有任何参数在栈上,栈由调用者清理。没有影子空间。成员函数有作为第一个参数的this。返回值在RAX或XMM0中。
在任何CALL指令前,栈指针必须对齐到16,因此RSP的值,在函数入口,是8模16。在将XMM寄存器保存到栈时,函数可以依赖这个对齐。
范围在[RSP-1]到[RSP-128]的地址称为红区。函数可以安全地将数据保存到红区,只要它不会被任何PUSH或CALL指令改写。
关于复合类型(struct、class、union)及向量类型(__m64、__m128、__m256)参数的细节参考手册5《不同C++编译器与操作系统的调用惯例》。
4.3. 名字重整(mangling)与名字修饰
C++中对函数重载的支持,使向链接器提供关于函数参数的信息成为必须。这通过对函数名添加参数类型代码来完成。这称为名字重整。名字重整代码传统上是编译器特定的。幸好,一个不断增长的趋势是标准化这个区域,以改进不同编译器间的兼容性。不同编译器的名字重整代码的细节在手册5《不同C++编译器与操作系统的调用惯例》中描述。
不兼容的名字重整代码问题,通过使用extern “C”声明,最容易解决。使用extern “C”声明的函数没有名字重整。仅有的修饰是16与32位Windows以及32与64位Mac OS中的一个下划线。对带有__stdcall与__fastcall声明的函数名,有某个额外的修饰。
extern “C”声明不能用于成员函数、重载函数、操作符及其他 C语言 不正常的构造。在这些情形中,你可以通过定义一个调用非重整函数的重整函数,来避免名字重整。如果重整函数被声明为内联,那么编译器将把对重整函数的调用替换为对非重整函数的调用。例如,在汇编中不使用名字重整定义一个重载C++操作符:
class C1; // unmangled assembly function;
extern "C" C1 cplus (C1 const & a, C1 const & b);
// mangled C++ operator
inline C1 operator + (C1 const & a, C1 const & b) {
// operator + replaced inline by function cplus
return cplus(a, b);
}
重载函数可以相同的方式内联。类成员函数可以被翻译为友元函数,如第42页指令7.1b所示。
4.5. 函数例子
以下例子展示了如何以汇编编写遵循调用惯例的函数。首先是C++代码:
// Example 4.1a
extern "C" double sinxpnx (double x, int n) {
return sin(x) + n * x;
}
相同的函数可以汇编编写。下面的例子展示了对不同平台编写的相同函数。
; Example 4.1b. 16-bit DOS and Windows 3.x
ALIGN 4
_sinxpnx PROC NEAR
; parameter x = [SP+2]
; parameter n = [SP+10]
; return value = ST(0)
push bp ; bp must be saved
mov bp, sp ; stack frame
fild word ptr [bp+12] ; n
fld qword ptr [bp+4] ; x
fmul st(1), st(0) ; n*x
fsin ; sin(x)
fadd ; sin(x) + n*x
pop bp ; restore bp
ret ; return value is in st(0)
_sinxpnx ENDP
在16位模式中,我们需要BP作为栈框,因为SP不能用作基址指针。整数n只是16位。对sin函数我使用了硬件指令FSIN。
; Example 4.1c. 32-bit Windows
EXTRN _sin:near
ALIGN 4
_sinxpnx PROC near
; parameter x = [ESP+4]
; parameter n = [ESP+12]
; return value = ST(0)
fld qword ptr [esp+4] ; x
sub esp, 8 ; make space for parameter x
fstp qword ptr [esp] ; store parameter for sin; clear st(0)
call _sin ; library function for sin()
add esp, 8 ; clean up stack after call
fild dword ptr [esp+12] ; n
fmul qword ptr [esp+4] ; n*x
fadd ; sin(x) + n*x
ret ; return value is in st(0)
_sinxpnx ENDP
这里,我选择使用库函数__sin代替FSIN。在某些情形里,这可能会更快,因为FSIN提供了比所需更高的精度。_sin的参数在栈上作为8字节传递。
; Example 4.1d. 32-bit Linux
EXTRN sin:near
ALIGN 4
sinxpnx PROC near
; parameter x = [ESP+4]
; parameter n = [ESP+12]
; return value = ST(0)
fld qword ptr [esp+4] ; x
sub esp, 12 ; Keep stack aligned by 16 before call
fstp qword ptr [esp] ; Store parameter for sin; clear st(0)
call sin ; Library proc. may be faster than fsin
add esp, 12 ; Clean up stack after call
fild dword ptr [esp+12] ; n
fmul qword ptr [esp+4] ; n*x
fadd ; sin(x) + n*x
ret ; Return value is in st(0)
sinxpnx ENDP
在32位Linux中,函数名上没有下划线。在Linux中,栈必须保存16字节对齐(GCC 3或更新版本)。对sinxpnx的调用从ESP减去4。我们从ESP再减去12,使减去总数为16。我们可以减去更多,只要总数是16的倍数。在例子4.1c中,我们仅从ESP减去8,因为在32位Windows中栈仅对齐到4。
; Example 4.1e. 64-bit Windows
EXTRN sin:near
ALIGN 4
sinxpnx PROC
; parameter x = xmm0
; parameter n = edx
; return value = xmm0
push rbx ; rbx must be saved
movaps [rsp+16], xmm6 ; save xmm6 in my shadow space
sub rsp, 32 ; shadow space for call to sin
mov ebx, edx ; save n
movsd xmm6, xmm0 ; save x
call sin ; xmm0 = sin(xmm0)
cvtsi2sd xmm1, ebx ; convert n to double
mulsd xmm1, xmm6 ; n * x
addsd xmm0, xmm1 ; sin(x) + n * x
add rsp, 32 ; restore stack pointer
movaps xmm6, [rsp+16] ; restore xmm6
pop rbx ; restore rbx
ret ; return value is in xmm0
sinxpnx ENDP
在64位Windows中,函数参数在寄存器中传递。ECX没有用于参数传递,因为第一个参数不是整数。我们使用RBX与XMM6在sin的调用过程中保存n与x。对此,我们必须使用具有被调用者保存状态的寄存器,且我们在使用它们之前,必须在栈上保存这些寄存器。在调用sin前,栈必须对齐到16,我们可以依赖在调用sinxpnx前栈必须对齐到16这个条件。对sinxpnx的调用从RSP减去8;PUSH RBX指令减去8;SUB指令减去32。减去的总数是8+8+32 = 48,它是16的倍数,因此保证了正确的对齐。栈上额外的32字节是用于sin调用的影子空间。注意例子4.1e不包括对异常处理的支持。如果程序依赖于捕捉在函数sin里产生的异常,添加带有栈回滚信息的表是必要的。
; Example 4.1f. 64-bit Linux
EXTRN sin:near
ALIGN 4
sinxpnx PROC
PUBLIC sinxpnx
; parameter x = xmm0
; parameter n = edi
; return value = xmm0
push rbx ; rbx must be saved
sub rsp, 16 ; make local space and align stack by 16
movaps [rsp], xmm0 ; save x
mov ebx, edi ; save n
call sin ; xmm0 = sin(xmm0)
cvtsi2sd xmm1, ebx ; convert n to double
mulsd xmm1, [rsp] ; n * x
addsd xmm0, xmm1 ; sin(x) + n * x
add rsp, 16 ; restore stack pointer
pop rbx ; restore rbx
ret ; return value is in xmm0
sinxpnx ENDP
64位Linux不使用与64位Windows一样的寄存器进行参数传递。没有具有被调用者保存状态的XMM寄存器,因此在sin调用期间,我们必须把x保存在栈上,尽管把它保存在寄存器可能更快(把x保存在一个64位整数寄存器是可能的,但慢)。n仍然可以保存在一个具有被调用者保存状态的通用寄存器中。栈对齐到16。栈上不需要影子空间。红区不能使用,因为它将被对sin的调用改写。注意例子4.1f不包括对异常处理的支持。如果程序依赖于捕捉在函数sin里产生的异常,添加带有栈回滚信息的表是必要的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS汇编入门教程(一)ARM64汇编基础
- iOS 汇编入门教程(一):ARM64 汇编基础
- iOS汇编入门教程(三)汇编中的 Section 与数据存取
- iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
- 汇编语言8086笔记
- python编程(反汇编)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Art of Computer Programming, Volume 2
Knuth, Donald E. / Addison-Wesley Professional / 1997-11-04 / USD 79.99
Finally, after a wait of more than thirty-five years, the first part of Volume 4 is at last ready for publication. Check out the boxed set that brings together Volumes 1 - 4A in one elegant case, and ......一起来看看 《The Art of Computer Programming, Volume 2》 这本书的介绍吧!