C 编写thunk以验证SysV ABI合规性
定义了Linux的C级和程序集调用约定 我想编写一个通用thunk,验证函数是否满足被调用方保留寄存器上的ABI限制,并(可能)尝试返回一个值 因此,给定一个像C 编写thunk以验证SysV ABI合规性,c,linux,x86,function-pointers,interceptor,C,Linux,X86,Function Pointers,Interceptor,定义了Linux的C级和程序集调用约定 我想编写一个通用thunk,验证函数是否满足被调用方保留寄存器上的ABI限制,并(可能)尝试返回一个值 因此,给定一个像intfoo(int,int)这样的目标函数,在汇编中编写这样的thunk非常容易,比如1: 现在,我当然不想为每个调用编写单独的foo\u thunk方法,我只想要一个通用方法。这个函数应该使用一个指向底层函数的指针(比如在rax中),并使用一个间接调用call[rax],而不是call foo,但在其他方面是相同的 我无法理解的是如何
intfoo(int,int)
这样的目标函数,在汇编中编写这样的thunk非常容易,比如1:
现在,我当然不想为每个调用编写单独的foo\u thunk
方法,我只想要一个通用方法。这个函数应该使用一个指向底层函数的指针(比如在rax
中),并使用一个间接调用call[rax]
,而不是call foo
,但在其他方面是相同的
我无法理解的是如何在C级实现THONK的透明使用(或者在C++中,其中似乎有更多的元编程选项-但我们还是坚持C)。我想采取如下措施:
foo(1, 2);
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);
并将其转换为对thunk
的调用,但仍然在相同的位置传递相同的参数(这是thunk工作所必需的)
预计我会修改源代码,可能是使用宏或模板魔术,因此上面的调用可以更改为:
CHECK_THUNK(foo, (1, 2));
为宏指定基础函数的名称。原则上,这可以转化为2:
但是我怎样才能申报支票呢?第一个参数是函数指针的“某种类型”。我们可以尝试:
check_thunk(void (*ptr)(void), ...);
所以一个“泛型”函数指针(所有指针都可以有效地转换到这个,我们实际上只称它为汇编,在语言标准之外),加上varargs
但这不起作用:…
的升级规则与正确的原型函数完全不同。它适用于foo(1,2)
示例,但是如果您调用foo(1.0,2)
,varargs版本只会将1.0保留为double
,您将使用一个完全错误的值(一个double
值双关为整数)调用foo
上述方法还有一个缺点,就是将函数指针作为第一个参数传递,这意味着thunk不再按原样工作:它必须将函数指针保存在rdi
中的某个位置,然后将所有值移一位(即mov rdi,rsi
)。如果存在非寄存器参数,事情会变得非常混乱
有没有办法使这项工作顺利进行
注意:这种类型的thunk基本上与堆栈上的任何参数传递都不兼容,这是这种方法的一个可接受的限制(它不应该用于具有那么多参数或内存
类参数的函数)
1这是检查被调用方保留的寄存器,但其他检查同样简单 2事实上,你甚至不需要宏来实现这一点,但它也在那里,所以你可以在发布版本中关闭thunk,只需直接调用 3我想我所说的“容易”是指在所有情况下都不起作用的thunk。显示的thunk不能正确对齐堆栈(容易修复),如果
foo
有任何堆栈传递的参数(更难修复),则会出现中断。在C源代码级别(无需修改gcc或链接器为您插入thunk),您可以为每个thunk定义不同的原型,但仍然共享相同的实现
您可以在asm源代码中的定义上放置多个标签,因此check\u thunk\u foo
的地址与check\u thunk\u 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
两次,使用不同定义的包装宏。一次用于真实原型,另一次用于thunks
顺便说一句,将函数指针作为额外参数传递给通用thunk可能会有问题。我认为在x86-64 SysV ABI中不可能可靠地删除第一个参数并转发其余的参数。对于使用6个以上整数参数的函数,您不知道有多少堆栈参数。您也不知道是否存在FP堆栈参数在第一个整数堆栈arg之前 对于在寄存器中传递所有可能的寄存器参数的函数来说,这应该可以很好地工作。(例如,如果有任何堆栈参数,则它们是按值计算的大型结构或其他不能放入整数寄存器的东西。) 为了解决这个问题,thunk可以基于返回地址而不是一个额外的隐藏参数进行调度,如果你有类似调试信息的东西来将调用站点返回地址映射到调用目标。或者你可以让gcc在
rax
或r11
中传递一个隐藏参数。从内联asm运行call
非常糟糕,所以你可以可能需要定制gcc,支持在额外寄存器中传递函数指针的某些特殊属性
但是如果您调用
foo(1.0,2)
,varargs版本将把1.0
保留为double
,您将使用一个完全错误的值(一个double
值双关为整数)调用foo
这并不重要,但不重要,您将使用
xmm0=(double)1.0
调用foo(2,垃圾)
。可变函数仍然使用与非可变函数相同的寄存器arg(或者在寄存器用完之前在堆栈上传递FP args,并将al=
设置为小于8)
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)
int res = add3(a, b, c);
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);
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