优化汇编例程(4)

4. ABI标准

ABI表示应用程序二进制接口(Application Binary Interface)。ABI是函数如何调用,参数与返回值如何传递,允许函数改变哪些寄存器的标准。在合并汇编与高级语言时,遵守适合的ABI标准是重要的。调用惯例等的细节涵盖在手册5《不同C++编译器与操作系统的调用惯例》中。这里,为了您的方便。汇总了最重要的规则。

4.1. 寄存器的使用

 

16DOSWindows

32WindowsUnix

64Windows

64Unix

可以自由使用的寄存器

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位模型里,也不应该被改变。

    1. 数据存储

在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里产生的异常,添加带有栈回滚信息的表是必要的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值