我目前正在深入研究汇编领域,主要从x86_64、C 和 System V AMD64 的角度,通常针对 Linux。
整数(以及暗示指针)值的调用约定非常简单,按顺序使用以下寄存器:
- RDI
- 相对强弱指数
- RDX
- RCX
- R8
- R9
- XMM0–7
较长的参数计数是通过将值推送到子例程的堆栈帧来处理的。我从维基百科页面上获得了这些寄存器名称x86_64调用约定。
对于较大的值(如结构和数组),约定似乎也是推送到被调用方的堆栈帧中。
但是,函数浮点参数的调用约定是什么?是否使用浮点寄存器?
另一个相关问题:如果我有混合的参数类型怎么办?
void mixed(int a, float b, mystruct c) { /* ... */ }
如果我的函数采用这样的参数列表,如何从程序集调用这样的函数?像这样的交错参数列表中使用了哪些寄存器?
参数传递的调用约定在第 3.2.3 节的 AMD 64 PDF 文档的 System V 应用程序二进制接口中指定。
我不确定文档是否可以在这里合法引用,但我至少可以解释一下。
分类类型
首先,文档为参数值定义了八种不同的分类:
- INTEGER:使用通用寄存器的整数类型和指针
- SSE:使用向量寄存器的类型。
- SSEUP:类似于SSE,但主要用于存储大(>=128 位)值的较高字节
- X87:浮点类型。
- X87UP:大浮点类型的上字节。
-
COMPLEX_X87:
complex
浮点类型的寄存器。 - NO_CLASS:填充区域以及空结构和联合,通常在堆栈的内存中。 内存
- :在主内存中的堆栈上独占传递的类型。
分类规则
接下来,它定义了 C 类型如何适应这些分类:
_Bool
、char
、short
、int
、long
、long long
和指针被归类为整数,并将使用这些寄存器。float
、double
、_Decimal32
、_Decimal64
和__m64
被归类为SSE,并将使用这些寄存器。__float128
、_Decimal128
和__m128
被分成两半,将最低有效字节/位存储在SSE中,将最高有效字节/位存储在SSEUP中。__m256
被拆分为四个 64 位(8 字节)值,其中最低有效字节存储为SSE,其余存储为SSEUP__m512
同样被分成 64 位(8 字节)块,其中最低有效字节存储为SSE,其他所有字节存储为SSEUPlong double
值将其 64 位尾数存储为 X87,16 位指数填充为 64 位(8 字节)并存储在X87UP中。-
__int128
基本上存储在INTEGER中的两个long
值,前半部分是低位/字节,后半部分是高位/字节。它们可以理解为好像它们被定义为结构体:typedef struct { long low_bits, high_bits; } __int128;
-
complex double
和complex float
类型被分成两半,前半部分是实部,后半部分是虚部,并存储在SSE中。它们可以理解为好像它们被定义为这样的结构:typedef struct { double real, imaginary; } complex_double;
complex long double
值分类为COMPLEX_X87。struct
s、union
s和数组的逻辑相当复杂,有关详细信息,请参阅上面链接的文档。简而言之,有一个递归算法定义了如何传递聚合类型,该算法决定如何传递值。
参数传递
现在我们有一个分类系统和一个递归算法来处理struct
s、union
s和数组,我们将这个系统和算法应用于函数的参数,该函数包括每个参数的以下步骤:
- 如果它是MEMORY对象,请将其写入堆栈。
- 如果是整数,请使用
%rdi
、%rsi
、%rdx
、%rcx
、%r8
和%r9
中的下一个可用寄存器。 - 如果是SSE,请使用
%xmm0
到%xmm7
范围内的下一个可用寄存器。 - 如果是SSEUP,请使用上次使用的
%xmm
的下一个可用 64 位区块来寄存SSE类型。
如果是 X87 - 、X87UP或COMPLEX_X87,则会在内存中传递。
冲洗并对所有参数值重复此操作。如果给定类型的寄存器用完,请写入堆栈。
TL;DRSystem V ABI 定义了一种非平凡但相当简单的算法,用于传递不同类型的数据。