2

给定一个带有 C 绑定和任意签名的函数,创建一个指向该函数的指针、传递它、包装它并调用它是一件简单的事情。

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);
}

只要调用者和被调用者遵循相同的调用约定并就签名达成一致,一切正常。

现在假设我想包装一个包含数千个函数的库。我可以使用nmreadelf获取要包装的所有函数的名称,但我宁愿不必关心签名,甚至不需要包含库的关联头文件。

在某些情况下,考虑到版本和平台之间发生的外观变化,干净地包含标题可能不是一种选择。例如:

// 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自己不必知道?

4

2 回答 2

5

如果您查看从编译的 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前两行也是如此:保存调用者的堆栈帧并创建我们自己的堆栈帧,然后在最后展开。

关闭retcall我们到这里的反面,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调用者不使用来限制应用程序的可移植性。

简而言之,答案是肯定的。如果您愿意弄脏自己的手,绝对可以在不知道其签名的情况下包装函数。没有关于可移植性的承诺;)。

于 2012-11-09T18:04:28.713 回答
2

除了Ryan 的回答之外,您还应该考虑使用libffi外部函数接口库,可能在您的 GCC 编译器中)。它适合您的目标,并“可移植地”抽象细节(对于它支持的许多架构、系统和 ABI)。

于 2012-11-09T18:21:06.033 回答