x86_64 程序集中的调用约定差异

Calling Convention Discrepancy in x86_64 Assembly

所以我有一个带有 3 个参数 ASM_Method(void*, void*, int)init_method(float, int*) 的汇编例程。感兴趣的是前者的空指针。

当我从 C++ 文件调用方法时,参数为:

float src[64];
float dest[64];
int radius[3];

init_method(1.5, radius);
ASM_Method(src, dest, 64);

这个调用进程的反汇编:

mov         r8d,100h  
lea         rdx,[rbp+0A0h]  
lea         rcx,[rbp-60h]  
call        ASM_Method 

不管初始化与否,程序运行正常。但是,当我这样做时:

float* src = new float[64];
float* dest = new float[64];
int radius[3];

init_method(1.5, radius);
ASM_Method(src, dest, 64);

调用时,RCX 设置的值不是正确的地址,但 RDX 是正确的。程序因此崩溃。

这个调用进程的反汇编:

mov         r8d,100h  
mov         rdx,rbx  
mov         rcx,rdi  
call        ASM_Method

除非我将 src 初始化为某些值,否则调用时 RCX 会更改为无效地址(在本例中为 1)。

ASM_Method 的汇编代码:

mov rax, rdx
add rax, r8
shr r8, 4
inc r8
xor r9, r9
movdqu xmm1, [rax]

MainLoop:
movdqu xmm0, [rcx + r9]
movdqu [rdx + r9], xmm0
add r9, 16
dec r8
jnz MainLoop

movdqu [rax], xmm1

ret

init_method 的汇编代码:

mulss xmm0, xmm0
mov ecx, 4
cvtsi2ss xmm1, ecx
mulss xmm0, xmm1

shr ecx, 2
cvtsi2ss xmm2, ecx
addss xmm2, xmm0
sqrtss xmm2, xmm2

stmxcsr roundFlags
or roundFlags, 2000h
ldmxcsr roundFlags

cvtss2si ecx, xmm2

stmxcsr roundFlags
and roundFlags, 0DFFFh
ldmxcsr roundFlags

mov eax, ecx
dec eax
bt ecx, 0
cmovnc ecx, eax

mov eax, 3
cvtsi2ss xmm1, eax
mulss xmm0, xmm1

cvtsi2ss xmm3, ecx
movss xmm2, xmm3
movss xmm4, xmm3

mulss xmm2, xmm2
mulss xmm2, xmm1

mov eax, 12
cvtsi2ss xmm1, eax
mulss xmm3, xmm1

mov eax, -4
cvtsi2ss xmm1, eax
mulss xmm4, xmm1
addss xmm4, xmm1

mov eax, 9
cvtsi2ss xmm1, eax

subss xmm0, xmm2
addss xmm3, xmm1
subss xmm0, xmm3
divss xmm0, xmm4

cvtss2si eax, xmm0

mov esi, ecx
add esi, 2

mov edi, ecx
cmp eax, 0
cmovle edi, esi
shr edi, 1
mov dword ptr [edx], edi

mov edi, ecx
cmp eax, 1
cmovle edi, esi
shr edi, 1
mov dword ptr [edx + 4], edi

mov edi, ecx
cmp eax, 2
cmovle edi, esi
shr edi, 1
mov dword ptr [edx + 8], edi

ret

这是怎么回事?

我 [仍然!] 喜欢案例 2 的 完整 反汇编。但是,我会猜一猜。

(1) 编译器用[正确的]值填充rdi。是src的地址[大概来自newand/ormalloc].

在 MS ABI 中,rdi 被认为是 "non-volatile"。它必须由 被呼叫者

保存

(2) 案例 2 然后调用 init_method。但是,init_method 不会 保留 rdi [因为它 必须 ]。它出于自己的目的使用它(例如 edi)。因此,在 return 之后,rdi 已被 丢弃

(3) 当来自 init_method 的程序 return 时,编译器 期望 rdi 将具有与它相同的值在步骤 (1) 之后。 (即)编译器不知道 init_method 损坏了 rdi,因此它使用它的值来设置 rcx [ASM_Method 的第一个参数]。这个 应该 src 值但它实际上是 任何 init_method 将它设置为(即垃圾值,相对说话)


更新:

不同平台的 ABI 不同 [通常,只是编译器]。 gccclang 与 MS 有不同的调用约定(即 MS 是奇怪的鸭子或通常的嫌疑人)。例如,对于 gcc/clangrdi 保留第一个参数,而 volatile

这是应突出显示大部分 ABI 的 wiki link:https://en.wikipedia.org/wiki/X86_calling_conventions


更新#2:

But why does one refer to the stack (i.e float src[64]) yet the other refers to registers (new float[64])before calling?

因为编译器优化。为了解释,我们将 "turn off" 优化一下。

All 函数作用域变量在函数的堆栈帧中有一个 "reserved slot"。所有这些 "slots" 在堆栈帧中都有一个固定的偏移量,编译器 [由] 计算得出。如果函数一个堆栈帧[一些叶函数可以省略它],那么所有变量都有它们的槽,不管优化是否正在进行使用与否。坚持这个想法...

如果您有一个固定大小的数组,如案例 1,则该数组的整个 space(即数据)都在框架内。因此,给定数组的地址是帧指针 + 数组的偏移量。因此,lea rcx,[rbp + offset_of_src]

标量变量也有槽。这包括 "pointers to arrays" 之类的东西,这就是案例 2 中的内容。

[记住,目前优化是 off] 案例 2 中缺少的部分代码类似于 [simplified]:

// allocate src
call malloc
mov [ebp + offset_of_src],rax

// allocate dest
call malloc
mov [ebp + offset_of_dest],rax

// push arguments for init_method and call it
call init_method

// call ASM_Method
mov r8d,64
mov edx,[ebp + offset_of_dest]
mov ecx,[ebp + offset_of_src]
call ASM_Method

注意,这里,我们不想"push"指针变量的地址,我们要"push" ]指针变量的内容.

现在,让我们重新打开优化器。仅仅因为函数变量 在堆栈帧上有 一个槽并不意味着生成的代码必须使用它。对于情况 2 中的简单函数,优化器意识到它可以使用非易失性寄存器来存储 srcdest 值,并且可以为它们消除堆栈 access/storage。

因此,经过优化,情况 2 看起来像:

// allocate src
call malloc
mov rdi,rax

// allocate dest
call malloc
mov rsi,rax

// push arguments for init_method and call it
call init_method

// call ASM_Method
mov r8d,64
mov edx,rsi
mov ecx,rdi
call ASM_Method

编译器选择的特定非易失性是任意的。在本例中,它们恰好是 rsirdi,但还有其他选择。

compiler/optimizer 在选择这些寄存器和其他寄存器来保存数据值方面非常聪明。它可以看到给定函数何时不再需要寄存器中的 ,并且可以重新分配它以保存另一个 [无关] 值(如果它选择的话)。

好的,还记得"hold that thought"吗?是时候呼气了。通常,一旦变量被赋予寄存器赋值,编译器就会尝试不理会它,直到不再需要它为止。但是,有时,没有足够的 寄存器来同时保存所有活动变量。

例如,如果一个函数有 [比方说] 四个嵌套 for 循环并使用 20 个不同的变量,则没有足够的寄存器可供使用。因此,编译器可能必须生成代码 "dumps" 将寄存器中的值返回到相应变量的堆栈帧槽。这是一个 "register spill".

这就是为什么在标量的堆栈帧中总是一个槽,即使它从未使用过[由于优化寄存器的值]。它使编译过程更简单并且偏移量相同。

此外,我们正在谈论 被叫方 保存的寄存器。但是,caller 保存的寄存器呢?虽然大多数函数在进入时压入非易失性变量并在退出时弹出它们(即它们为 他们的 调用者保留非易失性变量)。

给定的函数(例如A)可以使用volatile寄存器来保存变量(例如r10)的东西(例如)sludge。如果它调用另一个函数(例如 B),B 可能会破坏 A 的值。

因此,如果 A 希望在 r10 中保留一个值 必须调用 BA保存,调用B,然后恢复:

mov [rbp + offset_of_sludge],r10
call B
mov r10,[rbp + offset_of_sludge]

因此,有可用的堆栈帧槽非常方便。

有时,函数有太多变量,以至于为其中一些变量生成的代码看起来像未优化的版本:

mov rax,[rbp + offset_of_foo]
add rax,rdx
sub rax,rdi
mov [rbp + offset_of_foo],rax

因为 foo access/usage 太少了,不值得进行非易失性寄存器分配