【问题标题】:A way to wrap a C function call with no knowledge of the signature?一种在不知道签名的情况下包装 C 函数调用的方法?
【发布时间】:2012-11-09 18:04:28
【问题描述】:

给定一个带有 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 本身不必知道?

【问题讨论】:

    标签: c linux function gcc assembly


    【解决方案1】:

    如果你查看编译后的 C 函数生成的程序集,你会看到每个函数体都被

    pushq %rbp
    movq  %rsp, %rbp
    ; body
    leave
    ret
    

    http://en.wikipedia.org/wiki/X86_instruction_listingsleave 指令列为 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 的调用将创建他自己的堆栈空间并将他的结果保存到寄存器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 来限制应用程序的可移植性。

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 我对 libffi 进行了一些研究,既是在寻找解决我自己问题的方法时,也是在您发布此答案之后。这似乎是一个很酷的想法,也是一个有效的解决方案,可以将来自各种编译和非编译语言的位粘合在一起,只要至少有一方愿意针对这个接口实现......
      猜你喜欢
      • 1970-01-01
      • 2016-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多