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

我没有assembly经验,但这是我一直在努力的。 如果我缺少传递参数和通过程序集中的指针调用函数的任何基本方面,我想输入。

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

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

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

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

让我们从简化asm开始:

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

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

怎么编译器不知道你问? 毕竟,它就在asm中! 答案来自gcc文档中的这一行:

GCC不会自己解析汇编程序指令,也不知道它们的含义,甚至不知道它们是否是有效的汇编程序输入。

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

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

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

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

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

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

这使用机器约束来告诉gcc将变量a的值放入edi寄存器中。 这样做,gcc将在“方便”的时候为你加载寄存器,也许总是保持a edi。

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

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

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

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

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

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

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

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

这将我们带到call声明。

迈克尔和我都列出了很多原因,在内联中调用asm很困难。

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

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

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

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

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

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

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