使用GCC的内联汇编进行直接C函数调用

16

如果你想从内嵌汇编中调用一个C/C++函数,可以这样做:

void callee() {}
void caller()
{
    asm("call *%0" : : "r"(callee));
}

接下来,GCC将会生成类似这样的代码:

movl $callee, %eax
call *%eax

这可能会有问题,因为对于旧的CPU来说,间接调用会破坏流水线。

由于的地址最终是一个常量,我们可以想象可以使用约束。引用自GCC在线文档:

`i'

允许立即整数操作数(具有固定值)。这包括在汇编时或之后才会知道其值的符号常量。

如果我尝试像这样使用它:

asm("call %0" : : "i"(callee));

我从汇编器中得到以下错误:

错误:对于 `call',后缀或操作数无效

这是因为GCC生成的代码如下:

call $callee

与其使用

call callee

我的问题是是否有可能使GCC输出正确的call指令。


1
你确定间接调用会破坏流水线吗?你做过基准测试吗?我的理解是,在旧的x86架构(i686之前),间接调用非常糟糕(我记得在我的K6上它们慢了10-100倍),但现在的CPU更加智能,可以很好地处理它们。所以在得出结论之前,请进行一些测试! - R.. GitHub STOP HELPING ICE
@R..:你说得对:如果我在真正的CPU上进行基准测试,它不会有任何区别。然而,我正在qemu中运行我的代码,似乎在那里会有所不同(每个调用大约多20%的周期)。 - mtvec
那么我会建议你继续使用间接调用的方式。这样可以让gcc为PIC/PIE库/可执行文件生成正确的代码,而无需插入特殊的hack来处理这些事情。 - R.. GitHub STOP HELPING ICE
@R..:是的,那可能是最好的主意。虽然我不必担心PIC/PIE(这是内核代码),但我仍然非常有兴趣找到一个好的解决方案来解决这个问题。 - mtvec
如果是内核代码,只需硬编码调用,并在函数上加上 __attribute__((used)),这样它就不会被优化掉。 如果您只有一个目标操作系统和CPU架构,则无需担心可移植性。 顺便问一下,您真的在内核代码中使用C++吗? - R.. GitHub STOP HELPING ICE
@R..:是的,那可能是最好的选择,尽管我不喜欢硬编码混淆名称... 是的,我真的在使用C++ :-) 不过这只是一个爱好内核。 - mtvec
4个回答

18

我从GCC邮件列表中获得了答案

asm("call %P0" : : "i"(callee));  // FIXME: missing clobbers
现在我只需要找出%P0到底是什么意思,因为它似乎是一个未记录的特性...编辑:查看了GCC源代码后,不清楚约束前面的代码P具体代表什么。但是,在其他事情中,它防止GCC在常量值前加上$,这正是我在这种情况下所需要的。为了安全起见,您需要告诉编译器函数调用可能修改的所有寄存器,例如: "eax", "ecx", "edx", "xmm0", "xmm1", ..., "st(0)", "st(1)", ...。请参阅从扩展内联汇编调用printf,获取正确且安全地从内联汇编中进行函数调用的完整x86-64示例。

1
请参阅 https://dev59.com/mZfga4cB1Zd3GeqPALtR 了解使用内联汇编中的 call 的危险性。对于 x86-64 System V(Linux),要可靠地执行此操作基本上是不可能的,因为您无法告诉编译器您想破坏 red-zone。这通常是一个坏主意。请参阅 https://dev59.com/XlzUa4cB1Zd3GeqP69Wo。 - Peter Cordes
1
@CiroSantilli新疆改造中心996ICU六四事件:也许在这个答案中加入一个大而明显的警告和更多详细信息的链接是适当的。并且要包括一个“memory”清除,除非你调用的函数已知不会对此函数可见的任何副作用产生影响。 - Peter Cordes
1
@PeterCordes 那会有所帮助。到目前为止,我最喜欢的答案是Michael的具体尝试:https://dev59.com/j5ffa4cB1Zd3GeqP84KT#37503773 - Ciro Santilli OurBigBook.com
1
顺便提一下,更新:%P现在已经有文档记录了,可以打印没有修饰的符号名称。或者在需要/适当的情况下打印foo@plt。https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#x86-Operand-Modifiers - Peter Cordes
1
@PSkocik - 可能是因为符号地址在位置无关/可重定位代码中不是绝对常量?不,它们可以使用-fpie,PIC和PIE之间唯一的区别是支持符号重定位。 (如果可见性不是“隐藏”,那么库应该使用主可执行文件的全局变量定义,而不是它们自己的定义,但这与此无关。我认为这基本上是符号重定位的特殊情况。)我不知道为什么它不只是引用PLT条目,因为您没有使用-fno-plt - Peter Cordes
显示剩余3条评论

2
也许我在这里漏掉了什么,但是
extern "C" void callee(void) 
{

}

void caller(void)
{
  asm("call callee\n");
}

应该可以正常工作。您需要使用extern "C",以便名称不会根据C ++命名重载规则进行装饰。


这是极其不安全的。不要在函数内使用GNU C基本汇编语句(除非它是__attribute__((naked)))。你的“caller”可以内联到其他函数中,但它的汇编语句没有声明任何clobber,尽管它破坏了所有调用破坏寄存器(包括ZMM0-31、k0-7、mm0-7、st0-7)和红区。从asm语句内部安全地进行函数调用是一个巨大的痛苦:在扩展内联ASM中调用printf - Peter Cordes

1
如果您正在生成32位代码(例如,使用-m32 gcc选项),以下汇编内联会发出直接调用:
asm ("call %0" :: "m" (callee));

所有被调用破坏的寄存器,如ZMM0-7、k0-7等,都缺少清除。 - Peter Cordes

-1

诀窍在于字符串字面值的连接。在GCC开始从您的代码中获取任何实际含义之前,它将连接相邻的字符串字面值,因此即使汇编字符串与您程序中使用的其他字符串不同,如果您这样做,它们也应该被连接:

#define ASM_CALL(X) asm("\t call  " X "\n")


int main(void) {
    ASM_CALL( "my_function" );
    return 0;
}

由于您正在使用GCC,您也可以执行以下操作

#define ASM_CALL(X) asm("\t call  " #X "\n")

int main(void) {
   ASM_CALL(my_function);
   return 0;
}

如果你还不知道的话,调用内联汇编是非常棘手的。当编译器生成自己对其他函数的调用时,它会包括在调用之前和之后设置和恢复的代码。但是它不知道它应该为你的调用做任何这样的事情。你要么自己包含这些代码(非常难以正确实现,并且可能会因编译器升级或编译标志而中断),要么确保你的函数以这样一种方式编写,即它不会改变任何寄存器或堆栈上的条件(或变量)。

编辑:这只适用于C函数名称——对于C++函数名称,它们是被编码的。


1
不,这完全行不通。首先,对于调用来说 '$' 是完全错误的,其次他不想在汇编字符串中包含 "my_function" ,而是C++生成的混淆名称 - Nordic Mainframe
我没有看到这也适用于C++。在这种情况下,如果函数名被重载,很可能需要大量的麻烦才能使其正常工作。我只是从另一篇帖子中复制了“$”,因为我没有记住x86汇编语法。修复它... - nategoose
这也是GNU C基本汇编,所有被调用破坏的寄存器都缺少清除声明。因此,您可以预期如果在除了不内联的微不足道的函数之外的任何地方使用它,它都会出现问题,即使在这种情况下仍然可能会出现问题。https://gcc.gnu.org/wiki/ConvertBasicAsmToExtended 只有一个__attribute__((naked))函数才是安全的(也来自clobbers,因为它们只能靠自己)。 - Peter Cordes

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