我如何在 (GNU) C 中编写一个代理函数来连接两个不同的调用约定?

How can I write in (GNU) C a proxy function to interface two different calling conventions?

我正在编写一个 interpreter/compiler 混合体,其中调用约定在 CPU 堆栈上传递参数。函数只是指向可能在运行时生成的机器代码(如 C 函数指针)的指针。我需要一个代理函数来与自定义调用约定交互。 我想尽可能多地用 C 编写这个函数,尽管有些部分必须用汇编编写。我将此代理函数称为 apply.

我没有完全理解 GCC 内联汇编的语义,我想知道下面的暂定实现是否1元apply函数是对的,还是错的地方。特别是,我想知道许多 __asm__ 块之间堆栈的完整性:编译器(在我的例子中是 GCC 和 clang)如何解释被破坏的堆栈指针寄存器,以及它在生成代码?编译器是否理解我想要 "own" 堆栈? memory clobber 是必要的吗?

通过实验,我发现带有 -fomit-frame-pointer 的 clang 在看到 clobber 列表中的 rsp 寄存器时正确地禁用了函数的优化,因为 rsp 显然不再是一个在堆栈上寻址局部变量的可靠方法。这在 GCC 中不是真的,因此它会生成错误代码(这似乎是 GCC 中的错误)。所以我想这回答了我的一些问题。我可以接受 -fno-omit-frame-pointer,但 GCC 似乎没有考虑 rsp 被破坏的各种影响。

这是为 x86-64 编写的,尽管我有兴趣最终将它移植到其他体系结构。我们假设所有寄存器都在自定义调用约定中跨调用保留。

#define push(x) \
    __asm__ volatile ("pushq %0;" : : "g" (x) : "rsp", "memory")

#define pop(n) \
    __asm__ volatile ("addq %0, %%rsp;" : : "g" (n * 8) : "rsp", "memory")

#define call(f) \
    __asm__ volatile ("callq *%0;" : : "g" (f) : "cc", "memory")

void apply(void* f, void* x) {
  push(x);
  call(f);
  pop(1);
}

我认为 -mno-red-zone 标志在技术上是按我想要的方式使用堆栈所必需的。这是正确的吗?


前面的代码假设所有寄存器都在调用中保留。但是,如果有一组 没有 保留的寄存器,我应该如何在代码中反映这一点?我感觉将它们添加到 call 破坏列表不会产生正确的结果,因为寄存器可能会被推到堆栈的顶部,遮蔽被推入的 x。相反,如果它们保存在调用帧的先前保留区域中,它可能会起作用。是这样吗?我可以依赖这种行为吗? (我这样希望是不是很傻?)

另一种选择是手动保存和恢复这些寄存器,但我有一种强烈的感觉,这只会给安全带来错觉,并在某些时候中断。

I need a proxy function to interface with the custom calling convention. I want to write as much as possible of this function in C, although necessarily some parts will have to be written in assembly.

对不起,这根本行不通。您必须用汇编语言编写整个代理函数。

更具体地说——我不知道 clang,但 GCC 在非常基本的层面上假设没有人触及内联汇编中的堆栈指针,永远不会。这并不意味着它会出错——这意味着它会在你没有这样做的假设下愉快地错误优化,即使你告诉它你做了。这不太可能改变。它被烘焙到寄存器分配器和所有 umpteen CPU 后端。

现在,好消息是,您 可能 能够说服 libffi 做您想做的事。它有其他人用汇编语言为您编写的代理函数;如果它适合您的用例,它将为您省去很多麻烦。