【问题标题】:Calling Convention Discrepancy in x86_64 Assemblyx86_64 程序集中的调用约定差异
【发布时间】:2016-04-05 02:55:07
【问题描述】:

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

当我从 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

发生了什么事?

【问题讨论】:

  • 我在ASM_Method(void*, void*, int) 中只看到3 个参数你应该显示ASM_Method 的汇编代码
  • 我在这里有点停顿,因为我看不出 RCX 会有 1 的原因。您能否向我们展示您的整个汇编程序文件和演示该问题的整个 C++ 程序?这必须比所显示的更多。我仍然对你说ASM_Method(void*, void*, int) 有 5 个参数这一事实感到困惑。你是说 3 吗?
  • 在 ASM_Method 中,您将 r8 [has 64] 添加到 rax [has &dest]。因此,rax 是&dest[16]。您将其加载到xmm1 并在最后写回。所以,您正在保存/恢复dest[16..19]。但是,dest 未初始化。那是你要的吗?否则,ASM_Method 似乎是使用 16 字节内存操作的 memcpy 的快速复制版本。
  • 你还是没有按我说的做。我说过要发布您的完整 C++ 和汇编程序文件,并将其转换为Minimal Complete Verifiable Example。我什至在init_method 中提到这些问题的原因是因为我什至不知道它是如何到达您的ASM_method 的,因为我希望init_method 在尝试访问虚假内存地址之前很久就会出现段错误ASM_Method
  • 从案例 1/案例 2 asm 块:您的第三个参数是 64 但 asm inst 是 mov r8d,100h,这是 256 十进制 - 不是您指定的。这应该是mov r8d,40hboth 情况下。对于案例 2,我们需要从上面进行更多反汇编,因为我们没有设置 rbxrdi 的代码。特别是,由于 rcx 在 ASM_Method 中出现错误,它是从 rdi 设置的,因此值 [及其历史] 特别重要

标签: assembly x86-64 masm calling-convention


【解决方案1】:

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

(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 值,但实际上 whateverinit_method 将其设置为(即垃圾值,相对而言)


更新:

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

这是应该突出显示大多数 ABI 的 wiki 链接:https://en.wikipedia.org/wiki/X86_calling_conventions


更新 #2:

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

因为编译器优化。为了解释,我们将“关闭”优化一下。

所有函数作用域变量在函数的堆栈帧中有一个“保留槽”。所有这些“槽”在堆栈帧中都有一个固定的偏移量,编译器知道[由][计算]。如果函数一个堆栈帧[一些叶函数可以忽略它],那么所有变量都有它们的槽,无论是否使用优化。保持这个想法......

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

标量变量也有槽。这包括诸如“指向数组的指针”之类的东西,这就是我们在案例 2 中所拥有的。

[记住,优化暂时关闭]案例 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

注意,这里,我们不想“推”指针变量的地址,我们想“推”指针变量的内容

现在,让我们重新打开优化器。仅仅因为函数变量在堆栈帧上的一个槽并不意味着生成的代码有义务使用它。对于案例 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,但还有其他可供选择。

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

好的,还记得“保持那个想法”吗?是时候呼气了。通常,一旦为变量分配了寄存器分配,编译器就会尝试不理会它,直到不再需要它为止。但是,有时,没有足够的寄存器来同时保存所有活动变量。

例如,如果一个函数有 [比如说] 四个嵌套的 for 循环并使用 20 个不同的变量,那么就没有足够的寄存器可以使用。因此,编译器可能必须生成代码,将寄存器中的值“转储”回相应变量的堆栈帧槽。这是“寄存器溢出”。

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

此外,我们还讨论了 callee 保存的寄存器。但是,caller 保存的寄存器呢?虽然大多数函数在进入时推送非易失性并在退出时弹出它们(即它们为他们的调用者保留非易失性)。

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

所以,如果A 希望保留r10 中的值across 调用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 访问/使用频率太低,不值得进行非易失性寄存器分配

【讨论】:

  • 我之前打算将此link 作为评论发布。我认为 RDI 被丢弃是他看到这种特殊行为的原因。 RSI 将遭受同样的命运。
  • @MichaelPetch 我花了一段时间,因为我已经习惯了非 MS ABI [当没有其他任何意义时我不得不查找它]。好吧,问题解决了[终于!]。而且,我认为你在其他地方需要你,因为我发现了一个 MikeOS 问题:-)
  • 是的,这就是问题所在。但是为什么在调用之前一个引用堆栈(即float src[64])而另一个引用寄存器(new float[64])?
猜你喜欢
  • 2014-05-02
  • 1970-01-01
  • 2017-06-18
  • 2015-03-23
  • 1970-01-01
  • 1970-01-01
  • 2012-01-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多