我有一个函数,它接受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]
为什么这有点足够?
答案 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
时,它会执行以下操作:
dst
移至ecx
,将src0
移至edx
src1
推入堆栈f
然后被叫方f
在其代码开始时知道参数的位置:dst
和src0
位于寄存器ecx
和edx
中, 分别; 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
,您当然可以这样做,因为它总是存储在堆栈中,就在返回地址的正下方;但是如果您想多次使用dst
和src0
怎么办?它们在寄存器中,可以随时销毁。
因此,在这种情况下,编译器应该执行以下操作:在输入函数f
后,它应该记住ecx
和edx
的当前值(通过将它们推到堆)。这8个字节是所谓的“阴影空间”。它不是在你的“C ++版本”中完成的,可能是因为编译器确定这两个参数不会被多次使用,或者它可以通过其他方式正确处理它。
现在,您的_asm
版本会发生什么?这里的问题是您正在使用内联汇编。编译器然后失去对寄存器的控制,并且它不能假设寄存器ecx
和edx
在_asm
块中是安全的(它们实际上不是,因为您在_asm
阻止)。因此,它被迫在函数的开头保存它们。
保存如下:首先将esp
提高8个字节(sub esp, 8
),然后将edx
和ecx
移至[esp]
和{{ 1}}分别。
然后它可以安全地进入您的[esp+4]
区块。现在在它的脑海中(如果它有一个),图片是_asm
是[esp]
,src0
是[esp+4]
,dst
是4字节的返回地址,[esp+8]
是[esp+12]
。它不再考虑src1
和ecx
。
因此,edx
区块_asm
中的第一条指令应解释为mov ecx, [src0]
,与
mov ecx, [esp]
和其他两条指令相同。
在这一点上,你可能会说,啊哈,它在做蠢事,我不想浪费时间和空间,有没有办法?
嗯有一种方法 - 不要使用内联汇编......这很方便,但是有妥协。
您可以在mov ecx, DWORD PTR _src0$[esp+8]
源文件和f
中编写汇编函数.asm
。在public
代码中,将其声明为C/C++
。然后,当您开始汇编功能extern 'C' f(...)
时,您可以直接使用f
和ecx
。
答案 1 :(得分:0)
编译器决定使用一个调用约定,它使用"传递寄存器中的参数"又名__fastcall
。这允许编译器传递寄存器中的一些参数,而不是压入堆栈,这可以减少调用的开销,因为从变量移动到寄存器比推入堆栈更快,并且它可以“当我们到达被调用函数时,s现在已经在寄存器中,所以不需要从堆栈中读取它。
有很多关于调用约定如何在网络上运行的信息。关于x86 calling conventions的维基百科文章是一个很好的起点。