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

时间:2016-06-05 08:42:01

标签: c assembly x86 inline-assembly function-calls

我没有集会经验,但这正是我一直在努力的方向。如果我错过了传递参数和通过汇编中的指针调用函数的任何基本方面,我想输入。

例如,我想知道我是否应该恢复ecxedxesiedi。我读到它们是通用寄存器,但我无法找到它们是否需要恢复?打电话后我应该做什么样的清理工作?

这是我现在的代码,它确实有效:

#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)
          );

}

2 个答案:

答案 0 :(得分:6)

最初的问题是Is this assembly function call safe/complete?。答案是:不。虽然它似乎可以在这个简单的示例中工作(特别是如果禁用了优化),但是您违反了最终会导致失败的规则(真的难以追踪的规则)。

我想解决关于如何使其安全的(明显的)后续问题,但如果没有OP对实际意图的反馈,我不能真的这样做。

所以,我会用我们所拥有的东西尽力而为,并尝试描述使其不安全的事情以及你可以采取的一些措施。

让我们从简化asm开始:

 __asm__(
          "mov %0, %%edi;"
          :
          : "g"(a)
          );

即使使用此单一语句,此代码也已不安全。为什么?因为我们在不让编译器知道的情况下改变寄存器(edi)的值。

编译器怎么不知道你问?毕竟,它就在asm中!答案来自gcc docs

中的这一行
  

GCC不解析汇编程序指令本身而不解析汇编程序指令   知道它们的含义,甚至知道它们是否是有效的汇编输入。

在那种情况下,你如何让gcc知道发生了什么?答案在于使用约束(冒号后的东西)来描述asm的影响。

修复此代码的最简单方法可能就是这样:

  __asm__(
          "mov %0, %%edi;"
          :
          : "g"(a)
          : edi
          );

这会将edi添加到clobber list。简而言之,这告诉gcc edi的值将由代码更改,并且当asm退出时,gcc不应假设任何特定值将在其中。

现在,虽然这是最简单的,但它不一定是最好的方式。请考虑以下代码:

  __asm__(
          ""
          :
          : "D"(a)
          );

这使用machine constraint告诉gcc将变量a的值放入edi寄存器中。这样做,gcc将在“方便”的时间为您加载注册表,可能始终将a保留在edi中。

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

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

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

这告诉gcc在启动asm时,它应该将变量a的值放在与输出约束#0(即edi)相同的位置。它还说在输出时,edi不再是a,它将包含变量junk

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

该行另外一点:你用一个分号结束它。这是合法的,并将按预期工作。但是,如果您想使用-S命令行选项来确切地查看生成的代码(如果您希望通过内联asm获得好处,那么您将会发现)会产生难以阅读的内容码。我建议使用\n\t而不是分号。

所有这一切,我们仍然在第一线......

显然,同样适用于其他两个mov语句。

这将我们带到call声明。

迈克尔和我都列出了很多理由在内联电话中进行调用很困难。

  • 处理可能被函数调用ABI破坏的所有寄存器。
  • 处理红区。
  • 处理对齐。
  • Memory clobber。

如果这里的目标是'学习',那么随意尝试。但我不知道在生产代码中这样做会让我感到很自在。即使看起来它有效,我也不会有信心没有一些我错过的奇怪案例。这与我对using inline asm at all的正常关注不同。

我知道,这是很多信息。可能比你正在寻找gcc asm命令的介绍更多,但你选择了一个具有挑战性的起点。

如果您还没有这样做,请花时间查看gcc Assembly Language interface中的所有文档。这里有很多好的信息以及试图解释它是如何工作的例子。

答案 1 :(得分:1)

  

我读到它们是通用寄存器,但我找不到它们   需要恢复吗?

我不是该领域的专家,但是通过阅读x86-64 ABI(图3.4)以下寄存器:%rdi%rsi%rdx和<{1}}不会在函数调用之间保留,因此显然不需要恢复。

正如David Wohlferd评论的那样,你应该小心,因为无论哪种方式,编译器都不会意识到“自定义”函数调用,结果你可能会遇到它,特别是因为它可能不知道寄存器修改