【发布时间】:2017-10-24 08:04:20
【问题描述】:
SysV ABI 定义了 Linux 的 C 级和程序集调用约定。
我想编写一个通用 thunk 来验证一个函数是否满足 ABI 对被调用者保留寄存器的限制,并且(可能)尝试返回一个值。
所以给定一个像 int foo(int, int) 这样的目标函数,在汇编中编写这样一个 thunk 非常容易3,比如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,但否则会相同。
我不知道如何在 C 级别(或在 C++ 中,似乎有更多元编程选项 - 但我们在这里坚持使用 C)实现 thunk 的透明使用。我想采取类似的东西:
foo(1, 2);
并将其转换为对thunk 的调用,但仍然在相同的位置传递相同的参数(这是thunk 工作所必需的)。
预计我会修改源代码,可能使用宏或模板魔术,因此上面的调用可以更改为:
CHECK_THUNK(foo, (1, 2));
为宏指定底层函数的名称。原则上它可以把它翻译成2:
check_thunk(&foo, 1, 2);
我怎样才能声明 check_thunk 呢?第一个参数是“某种类型的”函数指针。我们可以试试:
check_thunk(void (*ptr)(void), ...);
所以一个“通用”函数指针(所有指针都可以有效地转换为这个,我们只会在语言标准的爪子之外真正称它为汇编),加上可变参数。
但这不起作用:... 的提升规则与正确原型化的函数完全不同。它适用于foo(1, 2) 示例,但如果您改为调用foo(1.0, 2),则可变参数版本只会将1.0 保留为double,并且您将调用foo 并使用完全错误的值(@987654339 @ 值双关语为整数。
上面还有一个缺点是将函数指针作为第一个参数传递,这意味着 thunk 不再按原样工作:它必须将函数指针保存在 rdi 某处,然后将所有值移动一个(即mov rdi, rsi)。如果有非注册参数,事情就会变得非常混乱。
有什么办法可以让这项工作顺利进行吗?
注意:这种类型的 thunk 基本上与堆栈上的任何参数传递都不兼容,这是这种方法的可接受限制(它不应该用于具有那么多参数的函数或MEMORY 类参数)。
1 这是检查被调用者保留的寄存器,但其他检查同样简单。
2 实际上,您甚至不需要宏来实现这一点 - 但它也在那里,因此您可以在发布版本中关闭 thunk 并直接调用。
3 好吧,“简单”我想我的意思是不是在所有情况下都有效。显示的 thunk 没有正确对齐堆栈(易于修复),如果 foo 有任何堆栈传递的参数(明显更难修复),则会中断。
【问题讨论】:
-
我想知道您是否可以为此使用任何
plt基础架构。例如修改gcc以通过thunk 包装器而不是通过PLT 调用?或者修改动态链接器解析的东西来解析PLT调用以通过thunk?并使用-fPIC编译以强制所有(?)调用通过PLT。我猜你只希望它用于一些手写函数,而不是编译器输出,所以这将是矫枉过正,而且每个函数都可以。 -
@PeterCordes 即使使用
PIC,我也很确定只有调用其他对象(例如,另一个.so)才会通过plt。 -
我认为同一对象中的外部可见函数通过 PLT,以允许符号插入。见Sorry state of dynamic libraries on Linux
-
是的,我认为你是对的。我不确定它是否适用于带有 -fPIE 的可执行文件?我不是在写共享对象。
-
@PeterCordes - 可能有几十个函数。对于我当前的实现,我可以完全在 asm 中进行检查,例如,使用宏编译函数或检查代码(使用 thunk 或仅内联每个函数的代码。我也想要一个 C 解决方案虽然因为它似乎更普遍有用,但重新编译 asm 并不总是可行的(而且我用 C 编写的越多,asm 越少越好)。
标签: c linux x86 function-pointers interceptor