很早之前就知道栈缠绕的原理,很早之前就想要个万能函数,可以调用任意的函数,感觉很酷。在研究模拟java的动态load的时候,曾经做了个类似的,结果要求限制的挺多,最后自己只记得原理,代码早不知飞哪儿了。
前几天突然想,是否通过栈缠绕可以实现?然后做下实验,虽然碰了些疙瘩,但还算顺利,贴出成果:
#include "stdio.h"
#include "stdlib.h"
#include "stdarg.h"
void func_1(int a, int b)
{
printf("call func_1, a=%d,b=%d\n", a, b);
}
int func_2()
{
printf("call func_2\n");
return 1;
}
void func_3(const char* fmt, ...)
{
char temp[255] = {0};
va_list args;
va_start(args, fmt);
_vsnprintf(temp, sizeof(temp) - 1, fmt, args);
va_end(args);
printf(temp);
}
typedef int (*func_ptr)(...);
int call_fun(func_ptr func, ...);
#define PUSH __asm push 0
int call_fun(func_ptr func, ...)
{
*((int*)(&func)-1) = (*((int*)(&func)-1))^(*((int*)(&func)));
*((int*)(&func)) = (*((int*)(&func)-1))^(*((int*)(&func)));
*((int*)(&func)-1) = (*((int*)(&func)-1))^(*((int*)(&func)));
return (int)func;
}
#define START_CALL PUSH
#define END_CALL
int main (int argc, char * argv[])
{
START_CALL;
call_fun((func_ptr)func_1, 1, 2);
END_CALL;
START_CALL;
printf("func_2 return=%d\n", call_fun((func_ptr)func_2));
END_CALL;
START_CALL;
call_fun((func_ptr)func_3, "printf this:%d, %s\n", 1, "ok");
END_CALL;
return 0;
}
基本的内容就是上面的内容。下面是原理:
任意一个函数都可以通过call_fun来进行调用,只要知道函数指针,便可以进行调用。可以看到call_fun的声明,是一个不定参数的形式。
对于函数指针,也声明为不定参数,方便理解和转换。
最核心的内容分为两部分,一部分是实现,一部分是平衡。实现就是call_fun的函数实现。
对于小段对齐的C调用来讲,在内存中,call_fun函数的调用栈的内存图如下:
param2 | 参数2 |
param1 | 参数1 | func | 被调用的函数地址,参数0 |
ret | 函数的返回地址 |
对于call_func函数的内部实现来讲,其实现的机制就是将ret和func在内存地址中的内容交换了一下,这一交换让程序表现的类似下面的关系:
call_func(fun_ptr func,...)
{
func(__VA_ARGS);
}
与上述不同的是,当func函数返回的时候,返回的地址是在调用call_func之后的,就像call_func调用返回一样,因此,call_func返回值是无任何作用的,只是用来欺骗编译器而已。
但这样一来,相当于在call_func结尾的时候自动执行了一次call调用,在函数返回的时候,栈指针POP了两次,而call_func函数的调用者并不知道这一情况,因此栈出现了不平衡现象。如下代码:
push param2
push param1
push func
call call_func
add esp, 3*4
在调用完call_func之后,add esp,2*4就可以了,但编译器自动生成的代码是add esp, 3*4,这样栈就不平衡了,我们需要手动平衡这一内容。这一部分由START_CALL宏负责。
START_CALL宏很简单,最终的结果是一条PUSH语句。由于我们手动的添加了一次PUSH,而编译器自动生成的代码对于我们的操作又多了一次POP,因此天下太平,栈平衡了。