编写一个thunk以验证SysV ABI合规性

时间:2017-10-24 08:04:21

标签: c linux x86 function-pointers interceptor

SysV ABI定义了Linux的C级和汇编调用约定。

我想编写一个通用thunk来验证函数是否满足对被调用者保留寄存器的ABI限制,并且(可能)试图返回一个值。

所以给定像int foo(int, int)这样的目标函数,在汇编中编写这样的thunk非常容易,比如 1

foo_thunk:
push rbp
push rbx
push r12
push r13
push r14
push r15
call foo
cmp rbp, [rsp + 40]
jne bad_rbp
cmp rbx, [rsp + 32]
jne bad_rbx
cmp r12, [rsp + 24]
jne bad_r12
cmp r13, [rsp + 16]
jne bad_r13
cmp r14, [rsp + 8]
jne bad_r14
cmp r15, [rsp]
jne bad_r15
ret

现在我当然不想为每个电话编写一个单独的foo_thunk方法,我只想要一个通用方法。这个应该采用指向基础函数的指针(让我们在rax中说),并且使用间接调用call [rax]而不是call foo,否则将是相同的。< / p>

我能弄清楚的是如何在C级实现thunk的透明使用(或者在C ++中,似乎有更多的元编程选项 - 但让我们坚持下去到这里C)。我想采取类似的方式:

foo(1, 2);

并将其转换为对thunk的调用,但仍然在相同的位置传递相同的参数(thunk工作所需的那些)。

我希望我可以使用宏或模板魔法来修改源代码,因此上面的调用可以更改为:

CHECK_THUNK(foo, (1, 2));

为宏提供底层函数的名称。原则上它可以将其转换为 2

check_thunk(&foo, 1, 2);

我怎样才能声明check_thunk?第一个参数是&#34;某种类型&#34;函数指针。我们可以试试:

check_thunk(void (*ptr)(void), ...);

所以&#34;泛型&#34;函数指针(所有指针都可以有效地转换为此指针,并且我们实际上只在语言标准的爪子之外调用它的程序集)加上varargs。

但这并不起作用:...具有完全不同的促销规则而不是正确的原型功能。它适用于foo(1, 2)示例,但如果您拨打foo(1.0, 2),则varargs版本只会将1.0保留为double,并且您将调用foo带有完全错误的值(double值被视为整数。

上面的缺点是将函数指针作为第一个参数传递,这意味着thunk不再按原样运行:它必须将函数指针保存在rdi某处,然后移动所有值一个(即mov rdi, rsi)。如果有非注册参数,事情就会变得非常混乱。

有没有办法让这项工作顺利进行?

注意:这种类型的thunk基本上与堆栈上的任何参数传递不兼容,这是这种方法的可接受限制(它不应该用于具有那么多参数的函数或者使用MEMORY类参数)。

1 这是检查被调用者保留的寄存器,但其他检查同样很简单。

2 事实上,你甚至不需要宏 - 但它也在那里,所以你可以关闭发布版本中的thunk只是做一个直接打电话。

3 以及#34; easy&#34;我想我的意思是指在所有情况下都不起作用的人。显示的thunk没有正确对齐堆栈(易于修复),如果foo有任何堆栈传递的参数(更难以修复),则会中断。

2 个答案:

答案 0 :(得分:2)

以gcc特定的方式执行此操作的一种方法是利用typeofnested functions创建一个嵌入调用的函数指针底层函数,但本身没有任何参数。

该指针可以传递给thunk方法,该方法调用它并验证ABI合规性。

以下是使用此方法将调用转换为int add3(int, int, int)的示例:

原始呼叫如下:

int res = add3(a, b, c);

然后将调用包装在宏中,如 2

CALL_THUNKED(int res, add3, (a,b,c));

......扩展为类似:

    typedef typeof(add3  (a,b,c)) ret_type; 

    ret_type closure() {              
        return add3  (a,b,c);         
    }                                 
    typedef ret_type (*typed_closure)(void);  
    typedef ret_type (*thunk_t)(typed_closure); 

    thunk_t thunk = (thunk_t)closure_thunk; 
    int res = thunk(&closure);

我们在堆栈上创建closure()函数,它使用原始参数直接调用add3。我们可以获取这个闭包的地址并毫无困难地传递给它一个asm函数:调用它将具有使用参数 1 调用add3的最终效果。

其余的typedef基本上是处理返回类型。我们只有一个closure_thunk方法,声明如此void* closure_thunk(void (*)(void));并在汇编中实现。它需要一个函数指针(任何函数指针都可以转换为任何其他函数),但返回类型是“错误的”。我们将其转换为thunk_t,这是一个动态生成的typedef,用于具有“正确”返回类型的函数。

当然,对于C函数来说当然不合法,但是我们正在asm中实现这个函数,所以我们回避这个问题(如果你想要更加一致,你可能会问asm代码正确类型的函数指针,每次都可以“生成”它,超出标准的范围:当然它每次只返回相同的指针。)

asm中的closure_thunk函数按以下方式实现:

GLOBAL closure_thunk:function

closure_thunk:

push rsi
push_callee_saved

call rdi

; set up the function name
mov rdi, [rsp + 48]

; now check whether any regs were clobbered
cmp rbp, [rsp + 40]
jne bad_rbp
cmp rbx, [rsp + 32]
jne bad_rbx
cmp r12, [rsp + 24]
jne bad_r12
cmp r13, [rsp + 16]
jne bad_r13
cmp r14, [rsp + 8]
jne bad_r14
cmp r15, [rsp]
jne bad_r15

add rsp, 7 * 8
ret

也就是说,推送我们要在堆栈上检查的所有寄存器(以及函数名称),调用rdi中的函数然后进行检查。 bad_*方法没有显示,但它们基本上吐出了一个错误信息,比如“函数add3覆盖了rbp ...顽皮!”和abort()过程。

如果在堆栈上传递任何参数,这会中断,但它确实适用于在堆栈上传递的返回值(因为该情况下的ABI将指针传递给`rax中返回值的位置)。

1 如何实现这一点有点神奇:gcc实际上将几个字节的可执行代码写入堆栈,closure函数指针指向那里。少数字节基本上加载一个寄存器,该寄存器带有指向包含捕获变量的区域的指针(在本例中为a, b, c),然后调用实际(只读)closure()代码,然后可以访问通过该指针捕获变量(并将它们传递给add3)。

2 事实证明,我们可能会use gcc's statement expression syntax将宏写入更常用的函数,如语法,如int res = CALL_THUNKED(add3, (a,b,c))

答案 1 :(得分:0)

在C源代码级别(无需修改gcc或链接器为您插入thunk),您可以为每个thunk定义不同的原型,但仍然共享相同的实现。

您可以在asm源中的定义上添加多个标签,因此check_thunk_foocheck_thunk_bar具有相同的地址,但您可以为每个使用不同的C原型。

或者你可以制作这样的弱别名:

int check_thunk_foo(void*, int, int) 
    __attribute__ ((weak, alias ("check_thunk_generic")));
// or maybe this should be ((weakref ("check_thunk_generic")))

#define foo(...) check_thunk_foo((void*)&foo, __VA_ARGS__)

// or to put the args in their original slots,
// but then you'd need different thunks for different numbers of integer args.
#define foo(x, y) check_thunk_foo((x), (y), (void*)&foo)

这方面的主要缺点是你需要为每个功能复制+修改原始原型。您可以使用CPP宏来解决这个问题,因此arg列表只有一个定义点,而真正的原型(以及thunk如果启用)都使用它。可能通过重新包含相同的.h两次,使用不同的定义包装宏。一次为真正的原型,再次为thunk。

BTW,将函数指针作为额外的arg传递给通用thunk可能会有问题。我认为可靠地移除第一个arg并转发x86-64 SysV ABI中的其余部分是不可能的。对于占用6个以上整数args的函数,您不知道有多少堆栈args。并且你不知道在第一个整数堆栈arg之前是否存在FP堆栈args。

这对于在寄存器中传递所有寄存器可能的args的函数应该可以正常工作。 (即,如果存在任何堆栈args,它们是按值或其他不能进入整数寄存器的大型结构。)

要解决这个问题,thunk可以根据返回地址而不是额外隐藏的arg进行调度,如果你有类似调试信息的东西来将呼叫站点返回地址映射到调用目标。或者你可以让gcc在raxr11中传递一个隐藏的arg。从内联asm运行call很糟糕,所以你可能需要自定义gcc,并支持一些在额外寄存器中传递函数指针的特殊属性。

  

但如果您拨打foo(1.0, 2),则varargs版本会将1.0作为double离开,您将以完全错误的值调用foodouble值被视为整数。

这不重要,但不是,你要用foo(2, garbage)致电xmm0=(double)1.0。变量函数仍然使用寄存器参数与非变量函数相同(或者在寄存器用完之前选择在堆栈上传递FP args,并将al=设置为小于8)。