编写代码以验证SysV ABI的合规性 [英] Writing a thunk to verify SysV ABI compliance

查看:98
本文介绍了编写代码以验证SysV ABI的合规性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

SysV ABI 定义了Linux的C级和程序集调用约定.

The SysV ABI defines the C-level and assembly calling conventions for Linux.

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

I would like to write a generic thunk that verifies that a function satisfied the ABI restrictions on callee preserved registers and (perhaps) tried to return a value.

因此,给定目标函数(如int foo(int, int)),很容易3在汇编中编写这样的代码,例如 1 :

So given a target function like int foo(int, int) it's pretty easy3 to write such a thunk in assembly, something like1:

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,但在其他方面将是相同的.

Now of course I don't actually wan to write a separate foo_thunk method for each call, I just want one generic one. This one should take a pointer to the underlying function (let's say in rax), and would use an indirect call call [rax] than call foo but would otherwise be the same.

我不知道如何在C级别(或在C ++中,似乎有更多的元编程选项-但在这里继续使用C)实现对thunk的透明使用.我想像这样:

What I can't figure out is how to to implement the transparent use of the thunk at the C level (or in C++, where there seems to be more meta-programming options - but let's stick to C here). I want to take something like:

foo(1, 2);

并将其转换为对thunk的调用,但仍在相同的位置传递相同的参数(thunk必须工作).

and translate it to a call to the thunk, but still passing the same arguments in the same places (that's needed for the thunk to work).

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

It is expected that I modify the source, perhaps with macro or template magic, so the call above could be changed to:

CHECK_THUNK(foo, (1, 2));

为宏指定基础函数的名称.原则上,它可以将其转换为 2 :

Giving the macro the name of the underlying function. In principle it could translate this to2:

check_thunk(&foo, 1, 2);

我如何声明check_thunk?第一个参数是函数指针的某种类型".我们可以尝试:

How can I declare check_thunk though? The first argument is "some type" of function pointer. We could try:

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

因此是通用"函数指针(所有指针都可以有效地强制转换为此,并且我们仅在语言标准的爪子之外才将其实际称为汇编),外加varargs.

So a "generic" function pointer (all pointers can validly be cast to this, and we'll only actually call it assembly, outside the claws of the language standard), plus varargs.

但这不起作用:...具有与正确原型函数完全不同的升级规则.它适用于foo(1, 2)示例,但是如果您调用foo(1.0, 2),则varargs版本将仅将1.0保留为double,并且您将以完全错误的值(将值设为整数.

This doesn't work though: the ... has totally different promotion rules than a properly prototyped function. It will work for the foo(1, 2) example, but if you call foo(1.0, 2) instead, the varargs version will just leave the 1.0 as a double and you'll be calling foo with a totally wrong value (a double value punned as an integer.

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

The above also has the disadvantage of passing the function pointer as the first argument, which means the thunk no longer works as-is: it has to save the function pointer in rdi somewhere and then shift all the values over by one (i.e., mov rdi, rsi). If there are non-register args, things get really messy.

有什么办法可以使这项工作顺利进行?

Is there any way to make this work smoothly?

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

Note: this type of thunk is basically incompatible with any passing of parameters on the stack, which is an acceptable limitation of this approach (it should simply not be used for functions with that many arguments or with MEMORY class arguments).

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

1 This is checks the callee preserved registers, but the other checks are similarly straightforward.

2 实际上,您甚至根本不需要该宏-但它也在那里,因此您可以关闭发行版中的thunk,然后直接进行调用.

2 In fact, you don't even really need the macro for that - but it's also there so you can turn off the thunk in release builds and just do a direct call.

3 好吧,简单"我想我的意思是在所有情况下都不起作用.所示的重击无法正确对齐堆栈(易于修复),并且如果foo具有任何通过堆栈的参数(明显较难修复),则会中断.

3 Well by "easy" I guess I mean one that doesn't work in all cases. The shown thunk doesn't correctly align the stack (easy to fix), and breaks if foo has any stack-passed arguments (significantly harder to fix).

推荐答案

以gcc特定的方式实现此目的的一种方法是利用

One way to do this, in a gcc-specific way, is to take advantage of typeof and nested functions to create a function pointer that embeds the call to the underlying function, but itself doesn't have any arguments.

此指针可以传递给thunk方法,该方法将调用它并验证ABI遵从性.

This pointer can be passed to the thunk method, which calls it and verifies ABI compliance.

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

Here's an example of transforming a call to int add3(int, int, int) using this method:

原始呼叫如下:

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

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

Then you wrap the call in a macro, like this2:

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的最终效果.

We create the closure() function on the stack, which calls directly into add3 with the original arguments. We can take the address of this closure and pass it an asm function without difficulty: calling it will have the ultimate effect of calling add3 with the arguments1.

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

The rest of the typedefs is basically dealing with the return type. We have only a single closure_thunk method, declared like this void* closure_thunk(void (*)(void)); and implemented in assembly. It takes a function pointer (any function pointer is convertible to any other), but the return type is "wrong". We cast it to thunk_t which is a dynamically generated typedef for a function that has the "right" return type.

当然,这对于C函数当然不是合法的,但是我们正在asm中实现该函数,因此我们回避了这个问题(如果您想更加兼容,则可以向asm代码询问正确类型的函数指针,可以每次在标准的范围之外生成"它:当然,每次都只返回相同的指针).

Of course, that's certainly not legal for C functions, but we are implementing the function in asm, so we kind of sidestep the issue (if you wanted to be a bit more compliant, you could perhaps ask the asm code for a function pointer of the right type, which can "generate" it each time, outside of the reach of the standard: of course it's just returning the same pointer each time).

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

The closure_thunk function in asm is implemented along the lines of:

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_*方法未显示,但是它们基本上会显示一条错误消息,例如"Function add3 overwrote rbp ... naughty!".和abort()该过程.

That is, push all the registers we want to check on the stack (along with the function name), call the function in rdi and then do your checks. The bad_* methods aren't shown, but they basically spit out an error message like "Function add3 overwrote rbp... naughty!" and abort() the process.

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

This breaks if any arguments are passed on the stack, but it does work for return values passed on the stack (because the ABI for that case passes a pointer to the location for the return value in `rax).

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

1 How this is accomplished is kind of magic: gcc actually writes a few bytes of executable code onto the stack, and the closure function pointer points there. The few bytes basically loads a register with a pointer to the region that contains the captured variables (a, b, c in this case), and then calls the actual (read-only) closure() code which then can access the captured variables though that pointer (and pass them to add3).

2 事实证明,我们可能使用gcc的语句表达语法 以更常见的函数(如语法)(如int res = CALL_THUNKED(add3, (a,b,c)))编写宏.

2 As it turns out, we could probably use gcc's statement expression syntax to write the macro in a more usual function like syntax, something like int res = CALL_THUNKED(add3, (a,b,c)).

这篇关于编写代码以验证SysV ABI的合规性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆