一种在不知道签名的情况下包装C函数调用的方法?

Rya*_*oun 2 c linux assembly gcc function

给定具有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);
}
Run Code Online (Sandbox Code Playgroud)

只要呼叫者和被呼叫者遵循相同的呼叫约定并同意签名,一切都有效.

现在让我们说我想要包含一个包含数千个函数的库.我可以使用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);
Run Code Online (Sandbox Code Playgroud)

这是我的背景理由,您可以留下或采取.无论如何,我的问题是这样的:

有没有办法写

// pseudocode
void wrapfun()
{
    return ((void (*)())funptr())();
}
Run Code Online (Sandbox Code Playgroud)

这样的来电者wrapfun知道了签名fun,但wrapfun本身却没有?

Rya*_*oun 5

如果查看从编译的C函数生成的程序集,您将看到包装的每个函数体

pushq %rbp
movq  %rsp, %rbp
; body
leave
ret
Run Code Online (Sandbox Code Playgroud)

http://en.wikipedia.org/wiki/X86_instruction_listingsleave指令列为80186等效(在AT&T语法中)

movq  %rbp, %rsp
popq  %rpb
Run Code Online (Sandbox Code Playgroud)

所以leave只是前两行的反转:保存调用者的堆栈帧并创建我们自己的堆栈帧,然后在结束时展开.

结束retcall我们在这里得到的结果,并且http://www.unixwiz.net/techtips/win32-callconv-asm.html显示了在这些配对指令期间发生的指令指针寄存器的隐藏按下和弹出.

void函数指针调用的原因本身不起作用,因为wrapfun编译器为函数创建了这个程序集.我们需要做的是创建包装器,使得它可以将调用者fun为其设置的堆栈帧直接交给调用,而不会让自己的堆栈框架妨碍.换句话说,观察C调用约定并同时违反它.

考虑一个C原型

int wrapfun(int x, int y);
Run Code Online (Sandbox Code Playgroud)

与程序集实现配对(AT&T x86_64)

  .file "wrapfun.s"
  .globl wrapfun
  .type   wrapfun, @function
wrapfun:
  call    funptr
  jmp     *%rax
  .size   wrapfun, .-wrapfun
Run Code Online (Sandbox Code Playgroud)

基本上,我们跳过典型的堆栈指针和基指针操作,因为我们希望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
Run Code Online (Sandbox Code Playgroud)

或者,我们可以将此逻辑嵌入到内联汇编中,利用我们对编译器在之前和之后执行的操作的了解:

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
    );
}
Run Code Online (Sandbox Code Playgroud)

这种方法的优点是在魔术之前更容易声明C局部变量.声明的最后一个局部变量将是RBP-sizeof(var),我们在拆除堆栈之前将其保存在RAX中.另一个可能的好处是有机会使用C预处理器,例如,内联32位或64位汇编,而无需单独的源文件.

编辑:现在的缺点是将IP保存到寄存器中的要求通过要求RBX呼叫者不使用来限制应用程序的可移植性.

简而言之,答案是肯定的.如果你愿意让自己的手变得有点脏,那么绝对可以在不知道它的签名的情况下包装一个函数.没有关于便携性的承诺;).