MSVC 汇编函数参数 C++ 与 _asm
MSVC Assembly function arguments C++ vs _asm
我有一个带有 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]
为什么这点就够了?
编译器已决定使用使用 "pass arguments in registers" 又名 __fastcall
的调用约定。这允许编译器将一些参数传递到寄存器中,而不是压入堆栈,这可以减少调用中的开销,因为从变量移动到寄存器比压入堆栈更快,而且现在已经在当我们到达被调用函数时注册,所以不需要从堆栈中读取它。
关于调用约定如何在 Web 上工作的更多信息。 x86 calling conventions 上的维基百科文章是一个很好的起点。
Mats Petersson 的回答解释了 __fastcall
。但我想这不完全是你要问的......
其实_src0$[esp+8]
就是[_src0$ + esp + 8]
,而_src0$
就是上面定义的:
_src0$ = -8 ; size = 4
所以,整个表达式 _src0$[esp+8]
只不过是 [esp]
...
要了解它为什么会做所有这些事情,您可能首先应该了解 Mats Petersson 在他的 post 中所说的内容,__fastcall
,或者更笼统地说,什么是 调用约定。详见他post中的link
假设您已经理解__fastcall
,现在让我们看看您的代码发生了什么。编译器正在使用 __fastcall
。您的被调用函数是 f(dst, src0, src1)
,它需要 3
个参数,因此根据调用约定,当调用者调用 f
时,它会执行以下操作:
- 将
dst
移动到 ecx
并将 src0
移动到 edx
- 将
src1
压入堆栈
- 将 4 个字节 return 地址压入堆栈
- 转到函数的起始地址
f
而被调用者f
,当它的代码开始时,就知道参数在哪里:dst
和src0
在寄存器ecx
和[=28中=], 分别; esp
指向 4 个字节的 return 地址,但它下面的 4 个字节(即 DWORD PTR[esp+4])正好是 src1
.
因此,在您的 "C++ version" 中,函数 f
只是做了它应该做的事情:
mov eax, DWORD PTR _src1$[esp-4]
这里_src1$ = 8
,所以_src1$[esp-4]
正好是[esp+4]
。看,它只是检索参数 src1
并将其存储在 eax
.
中
然而这里有一个棘手的问题。在f
的代码中,如果你想多次使用参数src1
,你当然可以这样做,因为它总是存储在堆栈中,就在return地址的下面;但是如果你想多次使用 dst
和 src0
怎么办?它们在寄存器中,可以随时销毁。
所以在那种情况下,编译器应该做以下事情:在进入函数 f
之后,它应该记住 ecx
和 edx
的当前值(通过推送它们入栈)。这8个字节就是所谓的"shadow space"。它没有在你的 "C++ version" 中完成,可能是因为编译器肯定知道这两个参数不会被多次使用,或者它可以通过其他方式正确处理它。
现在,您的 _asm
版本会怎样?这里的问题是您使用的是内联汇编。然后编译器失去对寄存器的控制,它不能假设寄存器 ecx
和 edx
在你的 _asm
块中是安全的(它们实际上不是,因为你在_asm
块)。因此它被迫在函数的开头保存它们。
保存过程如下:它首先将 esp
增加 8 个字节(sub esp, 8
),然后将 edx
和 ecx
移动到 [esp]
并且[esp+4]
分别。
然后它可以安全地进入你的_asm
块。现在在它的脑海里(如果它有的话),图片是[esp]
是src0
,[esp+4]
是dst
,[esp+8]
是4字节return地址,[esp+12]
是src1
。它不再考虑 ecx
和 edx
.
因此,您在 _asm
块中的第一条指令 mov ecx, [src0]
应解释为 mov ecx, [esp]
,与
相同
mov ecx, DWORD PTR _src0$[esp+8]
其他两条指令也一样。
此时,您可能会说,啊哈,它在做愚蠢的事情,我不想让它浪费时间,space在这方面,有什么办法吗?
好吧,有一个方法 - 不要使用内联汇编...它很方便,但有一个折衷方案。
您可以在 .asm
源文件和 public
中编写汇编函数 f
。在 C/C++
代码中,声明为 extern 'C' f(...)
。然后,当您开始组装函数 f
时,您可以直接使用 ecx
和 edx
.
我有一个带有 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]
为什么这点就够了?
编译器已决定使用使用 "pass arguments in registers" 又名 __fastcall
的调用约定。这允许编译器将一些参数传递到寄存器中,而不是压入堆栈,这可以减少调用中的开销,因为从变量移动到寄存器比压入堆栈更快,而且现在已经在当我们到达被调用函数时注册,所以不需要从堆栈中读取它。
关于调用约定如何在 Web 上工作的更多信息。 x86 calling conventions 上的维基百科文章是一个很好的起点。
Mats Petersson 的回答解释了 __fastcall
。但我想这不完全是你要问的......
其实_src0$[esp+8]
就是[_src0$ + esp + 8]
,而_src0$
就是上面定义的:
_src0$ = -8 ; size = 4
所以,整个表达式 _src0$[esp+8]
只不过是 [esp]
...
要了解它为什么会做所有这些事情,您可能首先应该了解 Mats Petersson 在他的 post 中所说的内容,__fastcall
,或者更笼统地说,什么是 调用约定。详见他post中的link
假设您已经理解__fastcall
,现在让我们看看您的代码发生了什么。编译器正在使用 __fastcall
。您的被调用函数是 f(dst, src0, src1)
,它需要 3
个参数,因此根据调用约定,当调用者调用 f
时,它会执行以下操作:
- 将
dst
移动到ecx
并将src0
移动到edx
- 将
src1
压入堆栈 - 将 4 个字节 return 地址压入堆栈
- 转到函数的起始地址
f
而被调用者f
,当它的代码开始时,就知道参数在哪里:dst
和src0
在寄存器ecx
和[=28中=], 分别; esp
指向 4 个字节的 return 地址,但它下面的 4 个字节(即 DWORD PTR[esp+4])正好是 src1
.
因此,在您的 "C++ version" 中,函数 f
只是做了它应该做的事情:
mov eax, DWORD PTR _src1$[esp-4]
这里_src1$ = 8
,所以_src1$[esp-4]
正好是[esp+4]
。看,它只是检索参数 src1
并将其存储在 eax
.
然而这里有一个棘手的问题。在f
的代码中,如果你想多次使用参数src1
,你当然可以这样做,因为它总是存储在堆栈中,就在return地址的下面;但是如果你想多次使用 dst
和 src0
怎么办?它们在寄存器中,可以随时销毁。
所以在那种情况下,编译器应该做以下事情:在进入函数 f
之后,它应该记住 ecx
和 edx
的当前值(通过推送它们入栈)。这8个字节就是所谓的"shadow space"。它没有在你的 "C++ version" 中完成,可能是因为编译器肯定知道这两个参数不会被多次使用,或者它可以通过其他方式正确处理它。
现在,您的 _asm
版本会怎样?这里的问题是您使用的是内联汇编。然后编译器失去对寄存器的控制,它不能假设寄存器 ecx
和 edx
在你的 _asm
块中是安全的(它们实际上不是,因为你在_asm
块)。因此它被迫在函数的开头保存它们。
保存过程如下:它首先将 esp
增加 8 个字节(sub esp, 8
),然后将 edx
和 ecx
移动到 [esp]
并且[esp+4]
分别。
然后它可以安全地进入你的_asm
块。现在在它的脑海里(如果它有的话),图片是[esp]
是src0
,[esp+4]
是dst
,[esp+8]
是4字节return地址,[esp+12]
是src1
。它不再考虑 ecx
和 edx
.
因此,您在 _asm
块中的第一条指令 mov ecx, [src0]
应解释为 mov ecx, [esp]
,与
mov ecx, DWORD PTR _src0$[esp+8]
其他两条指令也一样。
此时,您可能会说,啊哈,它在做愚蠢的事情,我不想让它浪费时间,space在这方面,有什么办法吗?
好吧,有一个方法 - 不要使用内联汇编...它很方便,但有一个折衷方案。
您可以在 .asm
源文件和 public
中编写汇编函数 f
。在 C/C++
代码中,声明为 extern 'C' f(...)
。然后,当您开始组装函数 f
时,您可以直接使用 ecx
和 edx
.