Assembly x86_64程序集中的调用约定不一致

Assembly x86_64程序集中的调用约定不一致,assembly,x86-64,masm,calling-convention,Assembly,X86 64,Masm,Calling Convention,所以我有一个汇编例程,有3个参数ASM_Methodvoid*,void*,int和init_methodfloat,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,[r

所以我有一个汇编例程,有3个参数ASM_Methodvoid*,void*,int和init_methodfloat,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 
mov         r8d,100h  
mov         rdx,rbx  
mov         rcx,rdi  
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  
lea         rdx,[rbp+0A0h]  
lea         rcx,[rbp-60h]  
call        ASM_Method 
mov         r8d,100h  
mov         rdx,rbx  
mov         rcx,rdi  
call        ASM_Method
除非我将src初始化为某些值,否则在本例中,RCX将更改为无效地址,调用时为1

ASM_方法的汇编代码:

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_方法的汇编代码:

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的地址[可能来自新的和/或malloc]

在MS ABI中,rdi被认为是非易失性的。它必须由被调用方保留

2案例2然后调用init_方法。但是,init_方法并没有像必须的那样保留rdi。它将其用于自己的目的,例如edi。因此,在返回时,rdi已被丢弃

3当程序从init_方法返回时,编译器希望rdi具有与步骤1后相同的值。i、 编译器不知道init_方法损坏了rdi,因此它使用其值设置rcx[ASM_方法的第一个参数]。这应该是src值,但实际上是init_方法将其设置为的任何值,也就是垃圾值,相对而言

更新:

ABI对于不同的平台是不同的[通常,只是编译器]。gcc和clang与MS有不同的呼叫约定,即MS是古怪的鸭子或通常的嫌疑犯。例如,对于gcc/clang,rdi持有第一个参数,并且是可变的

以下是wiki链接,该链接应突出显示大多数ABI:

更新2:

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

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

所有函数作用域变量在函数的堆栈框架中都有一个保留插槽。所有这些插槽在堆栈帧内都有一个编译器已知的固定偏移量。如果函数有一个堆栈框架[某些叶函数可以省略它],那么所有变量都有它们的槽,不管是否使用优化。保持这种想法

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

标量变量也有插槽。这包括指向数组的指针,这就是案例2中的情况

[记住,优化暂时停止]案例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
注意,在这里,我们不想推送指针变量的地址,而是要推送指针变量的内容

现在,让我们重新打开优化器。函数变量在堆栈框架上有一个槽并不意味着生成的代码必须使用它。对于案例2中的简单函数,优化器认识到它可以使用非易失性寄存器来存储src和dest值,并且可以消除它们的堆栈访问/存储

因此,通过优化,案例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
编译器选择的特定非挥发物是任意的。在这种情况下,它们只是碰巧是rsi和rdi,但还有其他选择

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

好的,还记得那个想法吗?呼气的时间到了。通常,一旦一个变量被赋予寄存器赋值,编译器就会尝试将其单独处理,直到不再需要它为止。但是,有时,没有足够的寄存器一次保存所有活动变量

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

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

此外,我们还讨论了被叫人保存的寄存器。但是,调用方保存的寄存器呢。而大多数功能在进入时推动非挥发物,并在退出时弹出它们,即 为调用者保留非挥发物

给定函数(如A)可使用易失性寄存器来保存变量(如污泥)的某些内容(如r10)。如果它调用另一个函数,例如B,B可能会丢弃A的值

因此,如果A希望在对B的调用中保留r10中的值,A必须保存该值,调用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访问/使用太少,不需要非易失性寄存器分配

,所以我[仍然!]喜欢案例2的完全分解。但是,我猜一下

1编译器用一个值[正确的值]填充rdi。它是src的地址[可能来自新的和/或malloc]

在MS ABI中,rdi被认为是非易失性的。它必须由被调用方保留

2案例2然后调用init_方法。但是,init_方法并没有像必须的那样保留rdi。它将其用于自己的目的,例如edi。因此,在返回时,rdi已被丢弃

3当程序从init_方法返回时,编译器希望rdi具有与步骤1后相同的值。i、 编译器不知道init_方法损坏了rdi,因此它使用其值设置rcx[ASM_方法的第一个参数]。这应该是src值,但实际上是init_方法将其设置为的任何值,也就是垃圾值,相对而言

更新:

ABI对于不同的平台是不同的[通常,只是编译器]。gcc和clang与MS有不同的呼叫约定,即MS是古怪的鸭子或通常的嫌疑犯。例如,对于gcc/clang,rdi持有第一个参数,并且是可变的

以下是wiki链接,该链接应突出显示大多数ABI:

更新2:

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

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

所有函数作用域变量在函数的堆栈框架中都有一个保留插槽。所有这些插槽在堆栈帧内都有一个编译器已知的固定偏移量。如果函数有一个堆栈框架[某些叶函数可以省略它],那么所有变量都有它们的槽,不管是否使用优化。保持这种想法

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

标量变量也有插槽。这包括指向数组的指针,这就是案例2中的情况

[记住,优化暂时停止]案例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
注意,在这里,我们不想推送指针变量的地址,而是要推送指针变量的内容

现在,让我们重新打开优化器。函数变量在堆栈框架上有一个槽并不意味着生成的代码必须使用它。对于案例2中的简单函数,优化器认识到它可以使用非易失性寄存器来存储src和dest值,并且可以消除它们的堆栈访问/存储

因此,通过优化,案例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
编译器选择的特定非挥发物是任意的。在这种情况下,它们只是碰巧是rsi和rdi,但还有其他选择

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

好的,还记得那个想法吗?呼气的时间到了。通常,一旦一个变量被赋予寄存器赋值,编译器就会尝试将其单独处理,直到不再需要它为止。但是,有时,没有足够的寄存器一次保存所有活动变量

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

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

此外,我们还讨论了被叫人保存的寄存器。但是,调用方保存的寄存器呢。而大多数函数在进入时推送非挥发性物质,并在退出时弹出它们,即它们为调用方保留非挥发性物质

给定函数(如A)可使用易失性寄存器来保存变量(如污泥)的某些内容(如r10)。我 如果它调用另一个函数,例如B,B可能会丢弃A的值

因此,如果A希望在对B的调用中保留r10中的值,A必须保存该值,调用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访问/使用频率太低,不值得使用非易失性寄存器分配

我在ASM_Methodvoid*,void*,int中只看到3个参数,您应该显示ASM_method的汇编程序代码。我在这里稍作停顿,因为我看不出RCX中有1个参数的原因。您能向我们展示整个汇编文件和整个C++程序来演示这个问题吗?这其中必须有比所展示的更多的东西。我仍然对你所说的ASM_Methodvoid*,void*,int有5个参数感到困惑。你是说3吗?在ASM_方法中,你将r8[has64]添加到rax[has&dest]。因此,rax是&dest[16]。您正在将其加载到xmm1中,并在末尾将其写回。因此,您正在保存/恢复dest[16..19]。但是,dest未初始化。这就是你想要的吗?否则,ASM_方法看起来像是使用16字节内存操作的memcpy的快速复制版本。你仍然没有按照我说的做。我说要发布完整的C++和汇编文件,并将其转换为A。我甚至提到init_方法中的问题的原因是因为我甚至不知道它是如何进入ASM_方法的,因为我希望init_方法在从案例1/案例2 ASM块进入ASM_方法之前很长一段时间都会尝试访问假内存地址:您的第三个参数是64,但ASM inst是mov r8d,100h,这不是您指定的256位小数。这两种情况下都应为mov r8d,40小时。对于案例2,我们需要从上面进行更多的反汇编,因为我们没有设置rbx和rdi的代码。特别是,由于rcx在ASM_方法中出现问题,它是从rdi设置的,因此值[及其历史记录]特别重要。我在ASM_方法void*,void*,int中只看到3个参数。您应该显示ASM_方法的汇编代码。我在这里稍作停顿,因为我看不出rcx中包含1的原因。您能向我们展示整个汇编文件和整个C++程序来演示这个问题吗?这其中必须有比所展示的更多的东西。我仍然对你所说的ASM_Methodvoid*,void*,int有5个参数感到困惑。你是说3吗?在ASM_方法中,你将r8[has64]添加到rax[has&dest]。因此,rax是&dest[16]。您正在将其加载到xmm1中,并在末尾将其写回。因此,您正在保存/恢复dest[16..19]。但是,dest未初始化。这就是你想要的吗?否则,ASM_方法看起来像是使用16字节内存操作的memcpy的快速复制版本。你仍然没有按照我说的做。我说要发布完整的C++和汇编文件,并将其转换为A。我甚至提到init_方法中的问题的原因是因为我甚至不知道它是如何进入ASM_方法的,因为我希望init_方法在从案例1/案例2 ASM块进入ASM_方法之前很长一段时间都会尝试访问假内存地址:您的第三个参数是64,但ASM inst是mov r8d,100h,这不是您指定的256位小数。这两种情况下都应为mov r8d,40小时。对于案例2,我们需要从上面进行更多的反汇编,因为我们没有设置rbx和rdi的代码。特别是,由于rcx在ASM_方法中出现问题,它是从rdi设置的,因此该值[及其历史记录]特别重要。我将在前面将其作为注释发布。我认为RDI被抛弃是他看到一种特殊行为的原因。RSI也会遭受同样的命运。@MichaelPetch这花了我一段时间,因为我太习惯了非MS ABI[我不得不在没有其他意义的情况下查找它]。嗯,问题终于解决了。而且,我想你在其他地方是需要的,因为我发现了一个MikeOS的问题:-是的,这就是问题所在。但是为什么一个引用堆栈,即float src[64],而另一个在调用之前引用registers new float[64]呢?我之前打算将此作为注释发布。我认为RDI被抛弃是他看到一种特殊行为的原因。RSI也会遭受同样的命运。@MichaelPetch这花了我一段时间,因为我太习惯了非MS ABI[我不得不在没有其他意义的情况下查找它]。嗯,问题终于解决了。而且,我想你在其他地方是需要的,因为我发现了一个MikeOS的问题:-是的,这就是问题所在。但是为什么一个引用堆栈,即float src[64],而另一个在调用之前引用寄存器new float[64]?