windows内核开发学习笔记五十:x64调用协定

        本篇文章描述x64代码中,一个函数(调用方)用来调用另一个函数(被调用方)的标准流程和约定。

调用约定违约

        x64应用程序二进制接口(ABI)默认使用四个寄存器的快速调用的约定。在调用堆栈上分配空间使用一个影子存储,供调用者保存这些寄存器里的数据。

        在函数调用的参数和用于这些参数的寄存器之间有严格的一对一对应关系(默认指定的寄存器)。任何不适合8字节的参数,或者不是1、2、4或8字节的参数,必须通过引用传递。一个参数永远不会分布在多个寄存器上。

        x87寄存器堆栈未被使用。被调用方可能会使用它,但是在函数调用中它是volatile的。所有浮点运算都是使用16个XMM寄存器完成的。

        整数参数在寄存器RCX、RDX、R8和R9中传递。浮点参数在XMM0L、XMM1L、XMM2L和XMM3L中传递。16字节的参数通过引用传递。对于参数传递在规定中有详细描述。这些寄存器以及RAX、R10、R11、XMM4和XMM5都是易失性的。注册使用的详细记录在注册使用和调用者/被调用者保存的注册表中。

        对于原型函数,在传递之前,所有参数都被转换为预期的被调用方类型。调用方负责为被调用方的参数分配空间。调用者必须总是分配足够的空间来存储四个寄存器参数,即使被调用者不接受那么多参数。这种约定简化了对非原型C语言函数和C/ c++变参数函数的支持。对于可变参数或非原型函数,任何浮点值都必须在相应的通用寄存器中重复。超过前四个参数的任何参数都必须在调用之前存储在影子存储之后的堆栈上。Vararg函数的详细信息可以在Varargs中找到。非原型函数信息在非原型函数中有详细介绍。

对齐

        大多数结构都是按照自然的方向排列的。主要的异常是堆栈指针和malloc或alloca内存,它们是16字节对齐的,以提高性能。大于16字节的对齐必须手动完成。因为16字节是XMM操作常用的对齐大小,这个值应该适用于大多数代码。有关结构布局和对齐的更多信息,请参见类型和存储。有关堆栈布局的信息,请参见x64堆栈用法。

Unwindability

        叶函数是不改变任何非易失性寄存器的函数。例如,非叶函数可以通过调用函数来更改非易失性RSP。或者,它可以通过为局部变量分配额外的堆栈空间来更改RSP。为了在处理异常时恢复非易失性寄存器,非叶函数使用静态数据注释。该数据描述了如何在任意指令下正确地展开函数。该数据存储为pdata或过程数据,后者又引用xdata,即异常处理数据。xdata包含展开信息,并可以指向额外的pdata或异常处理函数。

        prolog和epilog是高度限制的,因此可以在xdata中适当地描述它们。堆栈指针在任何不属于epilog或prolog的代码区域中必须保持16字节对齐,除非在叶函数中。叶函数可以简单地通过模拟返回来展开,因此不需要pdata和xdata。函数prolog和epilog的结构请参见x64 prolog和epilog。有关异常处理以及pdata和xdata的异常处理和展开的更多信息,请参阅x64异常处理。

参数传递

        默认情况下,x64调用约定将前四个参数传递给寄存器中的函数。这些实参使用的寄存器取决于实参的位置和类型。其余参数按从右到左的顺序被压入堆栈。

        在RCX、RDX、R8和R9中,最左边四个位置的整数值参数分别以从左到右的顺序传递。如前所述,第5个和更高的参数在堆栈上传递。寄存器中的所有整数参数都是右对齐的,因此被调用方可以忽略寄存器的上位,只访问寄存器的必要部分。

        前四个形参中的任何浮点和双精度参数都在XMM0 - XMM3中传递,具体取决于位置。当有可变参数时,浮点值只放在整数寄存器RCX、RDX、R8和R9中。详细信息请参见Varargs。类似地,当相应的参数是整数或指针类型时,XMM0 - XMM3寄存器将被忽略。

        __m128类型、数组和字符串不会直接传递值。相反,指针被传递到调用者分配的内存中。传递大小为8、16、32或64位的结构体和联合以及__m64类型,就好像它们是具有相同大小的整数一样。其他大小的结构体或联合体作为指向调用者分配的内存的指针传递。对于这些作为指针传递的聚合类型(包括__m128),调用者分配的临时内存必须是16字节对齐的。

        内部函数不分配堆栈空间,也不调用其他函数,有时使用其他volatile寄存器传递额外的寄存器参数。编译器和内在函数实现之间的紧密绑定使得这种优化成为可能。

        如果需要,被调用方负责将寄存器参数转储到它们的阴影空间中。

        下表总结了如何通过类型和位置从左到右传递参数:

Parameter type fifth and higher fourth third second leftmost
floating-point stack XMM3 XMM2 XMM1 XMM0
integer stack R9 R8 RDX RCX
Aggregates (8, 16, 32, or 64 bits) and __m64 stack R9 R8 RDX RCX
Other aggregates, as pointers stack R9 R8 RDX RCX
__m128, as a pointer stack R9 R8 RDX RCX

参数传递1的例子-所有整数

windows内核开发学习笔记五十:x64调用协定_第1张图片

参数传递2 -所有浮点数的例子

windows内核开发学习笔记五十:x64调用协定_第2张图片

参数传递3的例子-混合的整数和浮点数

windows内核开发学习笔记五十:x64调用协定_第3张图片

参数传递4的例子- __m64, __m128,和聚合

windows内核开发学习笔记五十:x64调用协定_第4张图片

可变参数

如果参数是通过可变参数(例如,省略号参数)传递的,则应用常规寄存器参数传递约定。该约定包括向堆栈溢出第5个参数和之后的参数。被呼叫者有责任放弃他们的地址被占用的争论。仅对于浮点值,整数寄存器和浮点寄存器都必须包含该值,以防被调用方期望在整数寄存器中获得该值。

Unprototyped功能

对于未完全原型化的函数,调用者将整型值作为整数传递,将浮点值作为双精度传递。仅对于浮点值,整型寄存器和浮点寄存器都包含浮点值,以防被调用方期望整型寄存器中的值。

windows内核开发学习笔记五十:x64调用协定_第5张图片

返回值

        通过RAX返回一个可以容纳64位的标量返回值,包括__m64类型。在XMM0中返回非标量类型,包括float、double和vector类型,如__m128、__m128i、__m128d。RAX或XMM0返回值中未使用位的状态为未定义。

        用户定义类型可以由全局函数和静态成员函数的值返回。要根据RAX中的值返回用户定义的类型,它的长度必须为1、2、4、8、16、32或64位。它还必须没有用户定义的构造函数、析构函数或复制赋值操作符。它不能有私有或受保护的非静态数据成员,也不能有引用类型的非静态数据成员。它不能有基类或虚函数。而且,它只能有同样满足这些要求的数据成员。(这个定义本质上与c++ 03 POD类型相同。因为在c++ 11标准中定义已经改变,所以我们不建议使用std::is_pod进行测试。)否则,调用者必须为返回值分配内存,并将指向它的指针作为第一个参数传递。剩下的参数向右移动一个参数。在RAX中,被调用方必须返回相同的指针。

下面的例子展示了如何通过指定的声明传递函数的形参和返回值:

示例返回值1 - 64位结果

windows内核开发学习笔记五十:x64调用协定_第6张图片

返回值2的示例- 128位结果

windows内核开发学习笔记五十:x64调用协定_第7张图片

返回值3的例子-用户类型的结果由指针

windows内核开发学习笔记五十:x64调用协定_第8张图片

返回值4的示例-用户类型按值返回结果

windows内核开发学习笔记五十:x64调用协定_第9张图片

调用者/被保存寄存器

x64 ABI认为寄存器RAX、RCX、RDX、R8、R9、R10、R11和XMM0-XMM5是volatile的。当YMM0-YMM15和ZMM0-ZMM15存在时,其上部也是挥发性的。在AVX512VL上,ZMM、YMM和XMM寄存器16-31也是不稳定的。考虑在函数调用时销毁易失性寄存器,除非通过分析(如整个程序优化)可以证明安全性。

x64 ABI考虑寄存器RBX、RBP、RDI、RSI、RSP、R12、R13、R14、R15和XMM6-XMM15的非易失性。它们必须由使用它们的函数保存和恢复。

函数指针

函数指针只是指向各自函数标签的指针。函数指针没有目录(TOC)要求。

对旧代码的浮点支持

MMX和浮点堆栈寄存器(MM0-MM7/ST0-ST7)在上下文切换中被保留。这些寄存器没有明确的调用约定。在内核模式代码中严格禁止使用这些寄存器。

FPCSR

寄存器状态还包括x87 FPU控制字。调用约定规定这个寄存器是非易失的。

x87 FPU控制字寄存器在程序执行开始时使用下列标准值设置:

windows内核开发学习笔记五十:x64调用协定_第10张图片

修改FPCSR中的任何字段的被调用方必须在返回给调用方之前恢复它们。而且,修改了这些字段中的任何一个的调用者必须在调用被调用者之前将它们恢复到它们的标准值,除非通过协议被调用者期望修改的值。

关于控件标志的非易变性规则有两个例外:

  • 在函数中,给定函数的文档目的是修改非易失的FPCSR标志。
  • 例如,通过整个程序分析,可以证明违反这些规则会导致程序的行为与未违反规则的程序相同。

MXCSR

寄存器状态还包括MXCSR。调用约定将该寄存器划分为volatile部分和非volatile部分。在MXCSR[0:5]中,volatile部分由6个状态标志组成,而寄存器的其余部分MXCSR[6:15]则被认为是非volatile的。

在程序开始执行时,将非易失性部分设置为以下标准值:

windows内核开发学习笔记五十:x64调用协定_第11张图片

修改MXCSR中任何非易失性字段的被调用方必须在返回给其调用方之前恢复它们。而且,修改了这些字段中的任何一个的调用者必须在调用被调用者之前将它们恢复到它们的标准值,除非通过协议被调用者期望修改的值。

关于控件标志的非易变性规则有两个例外:

  • 在函数中,给定函数的文档记录的目的是修改非易失的MXCSR标志。
  • 例如,通过整个程序分析,可以证明违反这些规则会导致程序的行为与未违反规则的程序相同。

不要对跨越函数边界的MXCSR寄存器的易变部分状态做任何假设,除非函数文档明确地描述了它。

setjmp / longjmp

当包含setjmpx .h或setjmp.h时,所有对setjmp或longjmp的调用都会导致调用析构函数和__finally调用的展开。这种行为与x86不同,在x86中,包括setjmp.h会导致__finally子句和析构函数不会被调用。

调用setjmp将保留当前的堆栈指针、非易失性寄存器和MXCSR寄存器。对longjmp的调用返回到最近的setjmp调用站点,并将堆栈指针、非易失性寄存器和MXCSR寄存器重置为最近的setjmp调用保留的状态。

你可能感兴趣的:(系统内核,C/C++,windows内核,系统内核,操作系统,windows内核,C/C++,驱动开发)