这个汇编函数调用是safe/complete吗?

Is this assembly function call safe/complete?

我没有组装经验,但这是我一直在做的事情。如果我遗漏了在汇编中通过指针传递参数和调用函数的任何基本方面,我想要输入。

例如,我想知道是否应该恢复 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)
          );

}

I read they are general purpose registers, but I couldn't find if they need to be restored?

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

正如 David Wohlferd 所评论的那样,您应该小心,因为无论哪种方式,编译器都不会意识到 "custom" 函数调用,因此您可能会遇到它,特别是因为它可能不是知道寄存器修改。

原来的问题是Is this assembly function call safe/complete?。答案是:不。虽然它在这个简单的示例中似乎可行(尤其是在禁用优化的情况下),但您违反了最终会导致失败的规则(确实 难以追踪的规则)。

我想解决关于如何使其安全的(显而易见的)后续问题,但如果没有 OP 对实际意图的反馈,我真的无法做到这一点。

因此,我将尽我所能,并尝试描述使其不安全的因素以及您可以采取的一些措施。

让我们从简化 asm 开始:

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

即使只有这条语句,这段代码也已经不安全了。为什么?因为我们在不让编译器知道的情况下更改寄存器 (edi) 的值。

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

中的这一行

GCC does not parse the assembler instructions themselves and does not know what they mean or even whether they are valid assembler input.

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

也许修复此代码的最简单方法是这样的:

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

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

现在,虽然这是最简单的方法,但不一定是最好的方法。考虑这段代码:

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

这使用 machine constraint 告诉 gcc 为您将变量 a 的值放入 edi 寄存器。这样做,gcc 将在 'convenient' 时间为您加载寄存器,也许通过始终在 edi 中保持 a

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

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

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

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

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

该行的另一点:以分号结尾。这是合法的,并且会按预期工作。但是,如果您曾经想使用 -S 命令行选项来准确查看生成了哪些代码(并且如果您想精通内联汇编,您会的),您会发现生成的代码难以阅读代码。我建议使用 \n\t 而不是分号。

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

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

这将我们带到 call 声明。

Michael 和我都列出了在内联 asm 中调用困难的一些原因。

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

如果这里的目标是 'learning,' 那么请随意尝试。但我不知道在生产代码中这样做我是否会感到舒服。即使它看起来可行,我也永远不会确信我没有错过一些奇怪的案例。除了我对 using inline asm at all.

的正常担忧之外

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

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