如果您查看从编译的 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 原型
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
将创建他自己的堆栈空间并将他的结果保存到 register 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
调用者不使用来限制应用程序的可移植性。
简而言之,答案是肯定的。如果您愿意弄脏自己的手,绝对可以在不知道其签名的情况下包装函数。没有关于可移植性的承诺;)。