MSVC汇编函数参数C ++ vs _asm

时间:2015-07-20 22:58:55

标签: c++ visual-c++ assembly

我有一个函数,它接受3个参数,dest,src0,src1,每个参数指向大小为12的数据。我制作了两个版本。一个用C编写并由编译器优化,另一个用_asm完全编写。是的。 3个论点?我自然会做类似的事情:

mov ecx, [src0]
mov edx, [src1]
mov eax, [dest]

我对编译器感到有点困惑,因为它认为适合添加以下内容:

_src0$ = -8                     ; size = 4
_dest$ = -4                     ; size = 4
_src1$ = 8                      ; size = 4
?vm_vec_add_scalar_asm@@YAXPAUvec3d@@PBU1@1@Z PROC  ; vm_vec_add_scalar_asm
; _dest$ = ecx
; _src0$ = edx

; 20   : {

sub esp, 8
mov DWORD PTR _src0$[esp+8], edx
mov DWORD PTR _dest$[esp+8], ecx

; 21   :    _asm
; 22   :    {
; 23   :        mov ecx, [src0]

mov ecx, DWORD PTR _src0$[esp+8]

; 24   :            mov edx, [src1]

mov edx, DWORD PTR _src1$[esp+4]

; 25   :            mov eax, [dest]

mov eax, DWORD PTR _dest$[esp+8]

Function body etc.

add esp, 8
ret 0

_src0 $ [esp + 8]等甚至意味着什么?为什么它在我的代码之前完成所有这些工作?为什么它会[显然]试图堆积任何东西呢?

相比之下,C ++版本在其正文之前只有以下内容,这非常相似:

_src1$ = 8                      ; size = 4
?vm_vec_add@@YAXPAUvec3d@@PBU1@1@Z PROC         ; vm_vec_add
; _dest$ = ecx
; _src0$ = edx

mov eax, DWORD PTR _src1$[esp-4]

为什么这有点足够?

2 个答案:

答案 0 :(得分:3)

Mats Petersson的答案解释__fastcall。但我想这并不是你要问的......

实际上_src0$[esp+8]仅表示[_src0$ + esp + 8]_src0$定义如上:

_src0$ = -8                     ; size = 4

所以,整个表达式_src0$[esp+8]只不过是[esp] ...

要了解它为什么要做所有这些事情,你应该首先了解Mats Petersson在帖子中所说的__fastcall,或者更一般地说,什么是调用约定。有关详细信息,请参阅其帖子中的链接。

假设您已理解__fastcall,现在让我们看看您的代码会发生什么。编译器正在使用__fastcall。您的被调用函数为f(dst, src0, src1),需要3个参数,因此根据调用约定,当调用者调用f时,它会执行以下操作:

  1. dst移至ecx,将src0移至edx
  2. src1推入堆栈
  3. 将4字节的返回地址压入堆栈
  4. 转到函数f
  5. 的起始地址

    然后被叫方f在其代码开始时知道参数的位置:dstsrc0位于寄存器ecxedx中, 分别; esp指向4字节的返回地址,但它下面的4个字节(即DWORD PTR [esp + 4])正好是src1

    所以,在你的“C ++版本”中,函数f只是做它应该做的事情:

    mov eax, DWORD PTR _src1$[esp-4]
    

    此处_src1$ = 8_src1$[esp-4]正是[esp+4]。请参阅,它只检索参数src1并将其存储在eax

    但这里有一个棘手的问题。在f的代码中,如果要多次使用参数src1,您当然可以这样做,因为它总是存储在堆栈中,就在返回地址的正下方;但是如果您想多次使用dstsrc0怎么办?它们在寄存器中,可以随时销毁。

    因此,在这种情况下,编译器应该执行以下操作:在输入函数f后,它应该记住ecxedx的当前值(通过将它们推到堆)。这8个字节是所谓的“阴影空间”。它不是在你的“C ++版本”中完成的,可能是因为编译器确定这两个参数不会被多次使用,或者它可以通过其他方式正确处理它。

    现在,您的_asm版本会发生什么?这里的问题是您正在使用内联汇编。编译器然后失去对寄存器的控制,并且它不能假设寄存器ecxedx_asm块中是安全的(它们实际上不是,因为您在_asm阻止)。因此,它被迫在函数的开头保存它们。

    保存如下:首先将esp提高8个字节(sub esp, 8),然后将edxecx移至[esp]和{{ 1}}分别。

    然后它可以安全地进入您的[esp+4]区块。现在在它的脑海中(如果它有一个),图片是_asm[esp]src0[esp+4]dst是4字节的返回地址,[esp+8][esp+12]。它不再考虑src1ecx

    因此,edx区块_asm中的第一条指令应解释为mov ecx, [src0],与

    相同
    mov ecx, [esp]

    和其他两条指令相同。

    在这一点上,你可能会说,啊哈,它在做蠢事,我不想浪费时间和空间,有没有办法?

    嗯有一种方法 - 不要使用内联汇编......这很方便,但是有妥协。

    您可以在mov ecx, DWORD PTR _src0$[esp+8] 源文件和f中编写汇编函数.asm。在public代码中,将其声明为C/C++。然后,当您开始汇编功能extern 'C' f(...)时,您可以直接使用fecx

答案 1 :(得分:0)

编译器决定使用一个调用约定,它使用"传递寄存器中的参数"又名__fastcall。这允许编译器传递寄存器中的一些参数,而不是压入堆栈,这可以减少调用的开销,因为从变量移动到寄存器比推入堆栈更快,并且它可以“当我们到达被调用函数时,s现在已经在寄存器中,所以不需要从堆栈中读取它。

有很多关于调用约定如何在网络上运行的信息。关于x86 calling conventions的维基百科文章是一个很好的起点。