为啥这个 MSVC asm 块没有 ret,或者非 void 函数有返回?

Posted

技术标签:

【中文标题】为啥这个 MSVC asm 块没有 ret,或者非 void 函数有返回?【英文标题】:Why doesn't this MSVC asm block have a ret, or the non-void function have a return?为什么这个 MSVC asm 块没有 ret,或者非 void 函数有返回? 【发布时间】:2021-07-19 09:28:10 【问题描述】:

我正在学习如何在 C++ 代码中使用内联 assembly

Here 是一个非常简单的例子:

// Power2_inline_asm.c
// compile with: /EHsc
// processor: x86

#include <stdio.h>

int power2( int num, int power );

int main( void )

    printf_s( "3 times 2 to the power of 5 is %d\n", \
              power2( 3, 5) );

int power2( int num, int power )

   __asm
   
      mov eax, num    ; Get first argument
      mov ecx, power  ; Get second argument
      shl eax, cl     ; EAX = EAX * ( 2 to the power of CL )
   
   // Return with result in EAX

既然power2 函数返回结果,为什么ret 指令在asm 代码的末尾没有?

还是在 asm 块之外,函数结束前的 C++ return 关键字?

【问题讨论】:

C 编译器会自动添加ret 指令以及在此之前需要的任何其他指令。不要自己添加ret,因为这很可能会破坏代码。 @fuz 编译器应该从哪里取值返回? 显然您正在学习如何在 32 位 x86 MSVC 中使用内联汇编。 x64 MSVC 不支持__asm 语句,这与其他编译器支持内联汇编的方式不同。我的意思是你可能正在学习一些用途有限的东西。 __asm 是一个编译器扩展,用于特定的编译器和一组特定的平台。您可能对标准 C++ asm 关键字感兴趣,尽管它的尖锐警告使它与 __asm 没有太大区别。 返回值取自eax。甚至有一个很好的评论这么说。 【参考方案1】:

EAX隐含返回值,编译器生成ret(部分代码由编译器生成,如果__declspec(naked)未指定)。由于没有 C++ return 语句,从 C++ 的角度来看,行为是未定义的,未定义行为的表现是返回 EAX 包含的任何内容,即结果。

【讨论】:

返回float怎么样,还在eax中吗? 浮点数在ST0 @Alex Guteniev 好的。我已经用that:__declspec(naked) int my_mul2(int num, int power) __asm mov eax, num ; Get first argument mov ecx, power ; Get second argument mul ecx ; EAX = EAX * (2 to the power of CL) ret // final value is in eax 这样的裸露形式重写了这个例子,但它没有返回正确的结果.. @derek911 你不能像这样在裸函数中引用函数参数。除非您知道自己在做什么,否则不要使用内联汇编!这是学习汇编编程的一种非常糟糕的方式。 MSVC 特别支持在 asm 语句之后从非 void 函数的末尾脱落,将 EAX 或 ST0 视为返回值。或者至少这就是 MSVC 事实上的工作方式,无论是否有意支持,但它甚至支持内联此类函数,因此它不仅仅是对 UB 的调用约定滥用。 (clang -fasm-blocks 确实 not 那样工作;IDK 关于 clang-cl。但它没有定义从非 void 函数末尾脱落的行为,完全省略了 ret 因为该执行路径一定不能到达。)【参考方案2】:

您似乎不清楚ret 指令和返回值之间的关系。 没有

ret 指令的操作数不是返回值,它是被调用者处理参数清理的调用约定从堆栈中移除的字节数。

返回值以其他方式传递,由调用约定控制,并且必须在到达ret 指令之前存储。

【讨论】:

【参考方案3】:

asm 中没有ret 指令是完全正常的;您想让编译器生成函数序言/尾声,包括ret 指令,就像它们通常对到达函数中 的执行路径所做的那样。 (或者您可以使用 __declspec(naked) 在 asm 中编写 whole 函数,包括处理调用约定,这将允许您使用 fastcall 在寄存器中获取 args 而无需从堆栈内存)。

更有趣的事情是在没有return 的情况下从非void 函数的末尾脱落。(我也编辑了你的问题来询问这个问题)。

在 ISO C++ 中,这是未定义的行为。 (所以像clang -fasm-blocks 这样的编译器可以假定永远不会到达执行路径,甚至不会为它发出任何指令,甚至不会发出ret。)但MSVC 确实至少-facto 定义这样做的行为

MSVC 确实支持在 asm 语句之后从非 void 函数的末尾脱落,将 EAX 或 ST0 视为返回值。或者至少这就是 MSVC 事实上的工作方式,无论是否有意支持,但它甚至支持内联此类函数,因此它不仅仅是对 UB 的调用约定滥用。 (clang -fasm-blocks 确实那样工作;关于clang-cl 的IDK。但它没有定义从非void 函数末尾脱落的行为,完全省略了ret,因为该执行路径一定不能到达。)


不在asm 块中使用ret

asm 块执行时,ESP 没有指向返回地址;我认为 MSVC 总是强制使用 asm 的函数将 EBP 设置为帧指针。

如果不让编译器有机会恢复调用保留的寄存器并清理函数尾声中的堆栈,您绝对不能只在函数中间使用ret

另外,如果编译器将power2 内联到调用者中会怎样? 然后你会从那个调用者那里返回(如果你在 asm 块中做了leave / ret)。


查看编译器生成的 asm。

(TODO:我打算写更多内容并在 https://godbolt.org/ 上链接一些东西,但一直没有回复。)

【讨论】:

我在旧标签中找到了这个半写的答案;希望它比没有更有用,请随时编辑或让我知道是否有任何我在快速浏览时没有注意到的主要差距。

以上是关于为啥这个 MSVC asm 块没有 ret,或者非 void 函数有返回?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 MSVC 使用 SSE2 指令来处理这种琐碎的事情?

如果函数没有明确使用'ret',为啥没有返回值

MSVC:为啥包含基本 QString 操作的函数没有内联?

在C / C ++中了解Windows / MSVC的一些Antidebug内联asm

为啥 MSVC 在编译宏时大发雷霆,而 G++ 完全是关于禅的?

为啥这个 DynamicMethod (ldarg.1, newobj, ret) 会触发 VerificationException?