给定一个具有 C 绑定(bind)和任意签名的函数,创建指向该函数的指针、传递它、包装它并调用它是一件简单的事情。
int fun(int x, int y)
{
return x + y;
}
void* funptr()
{
return (void*)&fun;
}
int wrapfun(int x, int y)
{
// inject additional wrapper logic
return ((int (*)(int, int))funptr())(x, y);
}
只要调用者和被调用者遵循相同的调用约定并同意签名,一切正常。
现在假设我想包装一个包含数千个函数的库。我可以使用 nm
或 readelf
来获取所有要包装的函数的名称,但我宁愿不必关心签名,甚至不需要包括库的相关头文件。
在某些情况下,考虑到版本和平台之间发生的外观变化,完全包含 header 可能不是一种选择。例如:
// from openssl/ssl.h v0.9.8
SSL_CTX* SSL_CTX_new(SSL_METHOD* meth);
// from openssl/ssl.h v1.0.0
SSL_CTX* SSL_CTX_new(const SSL_METHOD* meth);
这是我的基本原理,您可以留下或接受。无论如何,我的问题是:
有没有办法写
// pseudocode
void wrapfun()
{
return ((void (*)())funptr())();
}
wrapfun
的调用者知道 fun
的签名,但 wrapfun
本身不必知道?
最佳答案
如果您查看编译后的 C 函数生成的程序集,您会看到每个函数体都由
pushq %rbp
movq %rsp, %rbp
; body
leave
ret
http://en.wikipedia.org/wiki/X86_instruction_listings将 leave
指令列为 80186 等价物(在 AT&T 语法中)
movq %rbp, %rsp
popq %rpb
所以 leave
只是前两行的逆过程:保存调用者的栈帧并创建我们自己的栈帧,然后在最后展开。
结束的 ret
是将我们带到这里的 call
的逆函数,并且 http://www.unixwiz.net/techtips/win32-callconv-asm.html显示了在这些成对指令期间发生的指令指针寄存器的隐藏压入和弹出。
void 函数指针调用本身不起作用的原因是编译器为函数 wrapfun
创建了这个程序集。我们需要做的是以这样一种方式创建包装器,它可以将调用者为它设置的堆栈帧直接传递给 fun
的调用,而不用它自己的堆栈帧妨碍.换句话说,遵守 C 调用约定,同时违反它。
考虑一个 C 原型(prototype)
int wrapfun(int x, int y);
与汇编实现配对(AT&T x86_64)
.file "wrapfun.s"
.globl wrapfun
.type wrapfun, @function
wrapfun:
call funptr
jmp *%rax
.size wrapfun, .-wrapfun
基本上,我们跳过了典型的堆栈指针和基指针操作,因为我们希望 fun
的堆栈看起来与我的堆栈完全一样。对 funptr
的调用将创建他自己的堆栈空间并将他的结果保存到寄存器 RAX
中。因为我们没有自己的栈空间,而且调用者的 IP
恰好位于栈顶,我们可以简单地无条件跳转到包装函数,并让他的 ret
一路往回跳。以这种方式,一旦函数指针被调用,他将看到调用者设置的堆栈。
如果我们需要使用局部变量,将参数传递给 funptr
等,我们总是可以设置我们的堆栈,然后在调用之前将其拆除:
wrapfun:
pushq %rbp
movl %rsp, %rbp ; set up my stack
call funptr
leave ; tear down my stack
jmp *%rax
或者,我们可以将此逻辑嵌入到内联汇编中,利用我们对编译器前后操作的了解:
void wrapfun()
{
void* p = funptr();
__asm__(
"movq -8(%rbp), %rax\n\t"
"leave\n\t"
"popq %rbx\n\t"
"call *%rax\n\t"
"pushq %rbx\n\t"
"pushq %ebp\n\t" // repeat initial function setup
"movq %rsp, %rbp" // so it can be torn down correctly
);
}
这种方法的优点是在魔术之前更容易声明 C 局部变量。最后声明的局部变量将在 RBP-sizeof(var) 中,我们在拆除堆栈之前将其保存在 RAX 中。另一个可能的好处是有机会使用 C 预处理程序来内联 32 位或 64 位汇编,而无需单独的源文件。
编辑:现在的缺点是要求将 IP 保存到寄存器中,因为要求调用者不使用 RBX
来限制应用程序的可移植性。
简而言之,答案是肯定的。如果您愿意亲自动手,完全可以在不知道其签名的情况下包装一个函数。没有关于便携性的 promise ;)。
关于c - 一种在不知道签名的情况下包装 C 函数调用的方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13313530/