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

时间:2016-04-05 02:55:08

标签: assembly x86-64 masm calling-convention

所以我有一个包含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

发生了什么事?

1 个答案:

答案 0 :(得分:5)

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

(1)编译器用{[1}}填充rdi。它是src的地址[可能来自new和/或malloc]。

在MS ABI中,rdi被视为"非易失性"。它必须由被调用者

保留

(2)案例2然后调用init_method。但是,init_method确实保留rdi [因为必须]。它将其用于自己的目的(例如edi)。因此,在返回时,rdi已被删除

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

<强>更新

ABI对于各种平台[通常只是编译器]是不同的。 gccclang具有与MS不同的呼叫约定(即MS是奇怪的鸭子或通常的嫌疑人)。例如,对于gcc/clangrdi包含第一个参数, volatile

这里应该突出显示大部分ABI的维基链接:https://en.wikipedia.org/wiki/X86_calling_conventions

更新#2:

  

但是为什么在调用之前会引用堆栈(即float src [64])而另一个引用寄存器(new float [64])?

由于编译器优化。为了解释,我们将关闭&#34;有点优化。

所有函数作用域变量都有一个&#34;保留的插槽&#34;在函数的堆栈框架中。所有这些&#34;插槽&#34;在堆栈帧中有一个固定的偏移量,由[由编译器计算]已知。如果函数完全具有堆栈帧[某些叶子函数可以忽略它],那么所有变量都有它们的插槽,无论是否使用了优化。坚持这个想法......

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

标量变量也有插槽。这包括&#34;指向数组&#34;的内容,这就是我们在案例2中所拥有的内容。

[请记住,暂时优化 off ]案例2中缺少的部分代码与[简化]类似:

// 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

注意,在这里,我们不想&#34;推动&#34;指针变量的地址,我们希望&#34; push&#34;指针变量的内容

现在,让我们重新开启优化器。仅仅因为函数变量具有堆栈帧上的插槽并不意味着生成的代码有义务使用它。对于像情况2那样的简单函数,优化器意识到它可以使用非易失性寄存器来存储srcdest值,并且可以消除它们的堆栈访问/存储。

因此,通过优化,案例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,但还有其他人可以选择。

编译器/优化器非常聪明地选择这些寄存器而其他寄存器保存数据值。它可以看到给定函数何时不再需要寄存器中的,并且如果它选择的话,可以重新分配它以保存另一个[不相关]值。

好的,记住&#34;坚持这个想法&#34;?是时候呼气了。通常,一旦变量被赋予寄存器赋值,编译器就会尝试不加考虑,直到不再需要它为止。但是,有时候,没有足够的寄存器来同时保存所有活动变量。

例如,如果一个函数有[比较]四个嵌套for循环并使用20个不同的变量,那么就没有足够的寄存器可供使用。因此,编译器可能必须生成&#34;转储&#34;寄存器中的值返回到相应变量的堆栈帧槽。这是&#34;注册溢出&#34;。

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

另外,我们讨论的是 callee 保存的寄存器。但是,调用者保存的寄存器呢?虽然大多数功能在进入时会推动非挥发性物质并在退出时弹出它们(即它们为他们的来电者保留非挥发性物质)。

给定函数(例如A)可以使用 volatile 寄存器来保存变量(例如r10)的某些内容(例如sludge)。如果它调用其他功能(例如B),则B可能会废弃A的值。

因此,如果A希望保留r10 中的值,则B的来电,A必须保存,致电{ {1}},然后将其恢复:

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 访问/使用过于频繁而无法进行非易失性寄存器分配