这个汇编函数调用安全/完整吗?

3
我没有汇编的经验,但这是我正在研究的内容。如果我在通过指针调用函数时遗漏了任何基本方面,我希望能够得到反馈。
例如,我想知道是否应该恢复 ecx, edx, esi, edi。我了解它们是通用寄存器,但我无法确定它们是否需要被恢复?在调用后是否需要进行任何清理工作?
以下是我目前的代码,它可以正常工作:
#include "stdio.h"

void foo(int a, int b, int c, int d)
{
  printf("values = %d and %d and %d and %d\r\n", a, b, c, d);
}

int main()
{

  int a=3,b=6,c=9,d=12;
  __asm__(
          "mov %3, %%ecx;"
          "mov %2, %%edx;"
          "mov %1, %%esi;"
          "mov %0, %%edi;"
          "call %4;"
          :
          : "g"(a), "g"(b), "g"(c), "g"(d), "a"(foo)
          );

}

1
你可以尝试访问:http://codereview.stackexchange.com/ - Support Ukraine
简而言之:这段代码是不安全的。您正在更改内联汇编中的寄存器,而没有通知编译器。有一些事情可以做得更好(使用x86机器约束,添加内存破坏,破坏ABI允许foo更改的寄存器(eax?)等),但也许如果您说出您想要实现什么,您会得到更好的答案。 - David Wohlferd
1
我假设这是64位的代码?(至少从使用的调用约定来看是这样的)。在64位代码中调用函数比你想象的要复杂得多。我最近写了一个答案,可能对你有一些价值(关于64位代码、内联汇编和调用函数)。在那种情况下,我调用了printf,但它几乎适用于从内联汇编调用64位代码中的任何函数:https://dev59.com/j5ffa4cB1Zd3GeqP84KT#37503773 - Michael Petch
1
如果您将函数指针传递给内联汇编模板(输入操作数),我相信GCC会被强制保留一个非内联版本,并使用在函数属性中指定的任何约定(或默认约定)。 - Michael Petch
显示剩余4条评论
2个回答

6
原始问题是“这个汇编函数调用是否安全/完整?”。答案是:不安全。尽管在这个简单的例子中它可能看起来能够工作(特别是如果禁用了优化),但你正在违反规则,最终会导致难以追踪的故障。我想回答一个(显而易见的)后续问题,即如何使其安全,但是如果没有原帖作者的实际意图反馈,我无法真正做到这一点。因此,我将尽力根据我们拥有的内容来描述使其不安全的事情以及你可以采取的一些措施。让我们从简化该汇编代码开始:
 __asm__(
          "mov %0, %%edi;"
          :
          : "g"(a)
          );

即使只有这一条语句,这段代码也是不安全的。为什么呢?因为我们改变了一个寄存器(edi)的值,但没有告诉编译器。
你会问编译器怎么可能不知道?毕竟它就在汇编代码里呀!答案来自于gcc文档中的这句话:
GCC不解析汇编指令本身,也不知道它们的意义,甚至不知道它们是否是有效的汇编输入。
那么,你如何让gcc知道发生了什么?答案在于使用约束条件(冒号后面的内容)来描述asm的影响。
也许修复这个代码最简单的方法是这样的:
  __asm__(
          "mov %0, %%edi;"
          :
          : "g"(a)
          : edi
          );

这将edi添加到clobber list中。简而言之,这告诉gcc代码将更改edi的值,并且gcc不应假定在asm退出时它将有任何特定的值。
现在,虽然这是最简单的方法,但并不一定是最好的方法。考虑以下代码:
  __asm__(
          ""
          :
          : "D"(a)
          );

这里使用了一个机器约束,告诉gcc将变量a的值放入edi寄存器中。通过这种方式,gcc会在“方便”的时候为您加载寄存器,可能是通过始终将a保留在edi中。

但是,这段代码有一个(重要的)警告:通过在第二个冒号后放置参数,我们声明它为输入。输入参数必须是只读的(即在退出asm时必须具有相同的值)。

在您的情况下,call语句意味着我们无法保证edi不会被更改,因此这不太可行。有几种方法可以解决这个问题。最简单的方法是将约束移到第一个冒号后面,使其成为输出,并指定"+D"以表示该值是读+写的。但是,在asm之后,a的内容将基本上是未定义的(printf可能将其设置为任何值)。如果破坏a是不可接受的,则总是可以使用以下方法:

int junk;
  __asm__ volatile (
          ""
          : "=D" (junk)
          : "0"(a)
          );

这告诉gcc在开始汇编时,应将变量a的值放入与输出约束#0(即edi)相同的位置。 它还表示,在输出时,edi将不再是a,而是将包含变量junk

编辑:由于实际上不会使用“junk”变量,因此我们需要添加volatile限定符。 当没有任何输出参数时,Volatile是隐含的。

关于该行的另一个要点:您可以用分号结束它。 这是合法的,并且将按预期工作。 但是,如果您想使用 -S 命令行选项查看生成的代码(如果您想使用内联汇编),则会发现这会产生难以阅读的代码。 我建议使用\n\t代替分号。

所有这些都是关于第一行的内容...

显然,其他两个mov语句也适用于此。

这就带我们来到了call语句。
我和Michael列举了许多原因,说明在内联汇编中进行调用很困难。
- 处理函数调用ABI可能破坏的所有寄存器。 - 处理红区。 - 处理对齐。 - 内存破坏。
如果目标是“学习”,那么可以随意尝试。但我不知道是否会在生产代码中使用这种方法感到舒适。即使看起来像是可以工作的,我也永远不会确信是否有一些奇怪的情况被忽略了。这是我普遍担心使用内联汇编的问题之外。
我知道,这是很多信息。可能比你作为gcc asm命令介绍时所期望的更多,但你选择了一个具有挑战性的起点。
如果你还没有这样做,请花时间查看gcc Assembly Language interface中的所有文档。那里有很多好的信息以及示例,试图解释所有这些是如何工作的。

关于不使用内联汇编的建议:我在最近的一个回答中更详细地阐述了这一点。完全同意,对于实际应用来说这是一个可怕的想法,并且会破坏很多优化(如内联和常量传播),而且很难保证它是安全的。此外,感谢您提供的gcc wiki链接。 - Peter Cordes
1
@PeterCordes - 在花费了所有时间学习gcc的内联汇编是如何工作的,以便我最终可以给那些可怕的文档重写,他们非常需要时,我开始意识到虽然它很酷,但大多数情况下都是一个坏主意。这就是为什么我最终写了那个维基页面。作为一个加演,我目前正在尝试在函数内使用gcc的基本汇编(不带冒号的那种)被弃用。这解释了我的另一个维基条目。尽管使用扩展汇编是糟糕的,但基本汇编更糟糕。 - David Wohlferd
顺便提一下,使用 "+D" (tmp) 比使用单独的输入和输出参数要容易得多。它会覆盖 C 变量,因此如果您需要内联汇编后仍需要输入,则在 C 中使用单独的输入和输出参数样式可能更容易。 (我记得唯一不起作用的情况是 x87:我认为不允许使用 "+t"。) - Peter Cordes
更新:在扩展的内联汇编中调用printf显示了完整的clobber、红区跳过和堆栈对齐,这些都是可靠地(?)完成此操作所必需的。强烈建议不要这样做,正如您所说的那样。 - Peter Cordes

0
我了解它们是通用寄存器,但我不确定它们是否需要被恢复?
虽然我不是该领域的专家,但根据我阅读的x86-64 ABI(图3.4),以下寄存器:%rdi%rsi%rdx%rcx在函数调用之间不会被保留,因此显然不需要被恢复。
正如David Wohlferd所评论的那样,你应该小心,因为无论哪种方式,编译器都不会意识到“自定义”函数调用,因此你可能会遇到问题,特别是因为它可能不知道寄存器的修改。

1
虽然ABI表示它们不需要被恢复,但gcc并不知道函数调用正在发生。它根本不解析汇编代码。因此,它没有理由认为rdi、rsi、rdx或rcx的值已经改变。 - David Wohlferd
4
更好了。但还有其他考虑因素,比如64位代码的红色区域。这意味着推入/弹出(传统的“恢复”寄存器方法)比通常更加复杂。即使rax在此代码中没有明确提到,它也可能会被printf或任何其他相关函数更改,所以也必须进行“破坏”。除了r8、r10等之外,在内联汇编中(安全地)调用函数是很困难的,而且通常是一个不好的主意。 - David Wohlferd
1
我同意 @DavidWohlferd 的观点:从内联汇编调用函数需要相当多的知识。最近我写了一个不太简单的答案,涉及到64位代码/内联汇编/调用函数。除了David所说的之外,GCC本身要求在进行CALL时栈必须对齐到16字节边界。因此,在调用之前,你不仅需要处理红区和破坏寄存器的问题,还需要处理栈对齐的问题。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接