Chinaunix

标题: 运行时动态函数调用(Runtime Dynamic Invoke) [打印本页]

作者: nicolas.shen    时间: 2009-09-06 22:09
标题: 运行时动态函数调用(Runtime Dynamic Invoke)
window版没人气,发到这里吧,思想都是一样的,不过没有linux下的实现,有兴趣的试试看吧。
这阵子对动态解析比较感兴趣,对于动态解释型语言来说,这毫无研究的必要,但对于编译型的语言(主要讨论C/C++),凭着本人目前的水平,还无法想出办法。想法是很简单的,例如,类似这样的win32函数 BOOL Beep(int dwFreq,int dwDuration),静态调用就不说了,动态调用的话,先是加载函数库,然后取得Beep函数的地址,然后去invoke,但是,在编译期,必须显式地给出Beep的函数指针原型才能通过编译,因此,如果想偷懒, 不想给出原型以期达到广泛应用的话,至少现在微软提供的工具是没办法让你编译过的,那么怎么办呢?  比如我就想通过输入 函数名,参数1,参数1类型,参数2,参数2类型...这样的字符串,然后我就优哉游哉的让程序自己去调用我想调用的函数,那怎么办呢?
我们可以思考下微软自己是怎么实现动态链接和加载的呢,首先是加载库,然后取得函数地址,既然我们能得到函数的地址了,那我们就直接call呗,嗯还得传参,这够麻烦的了,如果不传参的话,就没这么难了。没关系,我们看看函数的调用规则吧,压栈基址指针,基址指向栈顶,参数进栈,调用,保存返回值,栈顶指向基址,出栈基址指针。那么,我们自己不也可以模拟下这个过程么?嗯,那就试试看吧
源码如下(win32下的console程序,vs2008下,vc++9.0 编译运行)
#include <stdio.h>
#include <Windows.h>
int main()
{
//加载函数库
HMODULE hMod = LoadLibraryA("Kernel32.dll");
//取得函数地址
FARPROC pfAddr = GetProcAddress(hMod,"Beep");
DWORD nRet ;//返回值
DWORD dwFreq = 750; //频率
DWORD dwDuration = 300;//持续时间
__asm
{
  //注意调用方式,这是stdcall
  push dwDuration//参数压栈,持续时间300毫秒
  push dwFreq //继续压,频率
  //...直到参数全部入栈
  call pfAddr //开始调用   
  mov nRet,eax //取得回值  
}
printf("%d",nRet);//输出返回值
//释放函数库
FreeLibrary(hMod);
return 0;
}
凭着"半拉可及"(俺们东北话)的知识,这个程序居然跑起来了,还不错,嗯,不过还得继续,因为我们忽略了我们想达到的目的中的一些细节,例如传参也要动态的,不光是函数名,还有类型,不能光是int吧?不过这是个好的开端,因为内嵌的汇编可以做循环嘛,可以实现循环的压栈(根据参数个数), 也可以根据参数类型选择相应的操作指令
作者: langue    时间: 2009-09-06 22:11
能直接描述你的问题么
作者: maxxfire    时间: 2009-09-06 23:04
有了函数指针,还怕不能动态调用么?
作者: syncpk99    时间: 2009-09-07 09:37
可以看看这个网站,有完整的解决方案。
http://www.dyncall.org/
作者: nicolas.shen    时间: 2009-09-07 11:02
原帖由 syncpk99 于 2009-9-7 09:37 发表
可以看看这个网站,有完整的解决方案。
http://www.dyncall.org/

嗯,多谢各位,我看了一下,很好,的确是我想要的
作者: blairzhong    时间: 2009-09-07 14:46
原帖由 syncpk99 于 2009-9-7 09:37 发表
可以看看这个网站,有完整的解决方案。
http://www.dyncall.org/



谢谢,这个网站不错,比起我们必须事先获得参数好多了
作者: OwnWaterloo    时间: 2009-09-08 01:48
标题: 回复 #1 nicolas.shen 的帖子;回复 #4 syncpk99 的帖子
这库够牛叉的……


要动态(编译后)解析一个运行时得到(比如字符串格式)的函数调用,如:
f(a1,a2, ... an);

比较难处理的是如何传递"数量未知"的参数

详细一点的说, 假设, 解释器实现了:

1. 从脚本中得到一个表示调用的字符串

  1. const char* call_string = script_get_a_call_at_runtime();
复制代码


2. 从该字符串中得到函数名、函数返回类型、各个参数、以及它们的类型:

  1. void* f           = script_parse_function_name(call_string);

  2. typedef struct value
  3. {
  4.     int t;
  5.     union {
  6.         char   c;
  7.         int    i;
  8.         double d;
  9.         /* more */
  10.     } v;
  11. } value;

  12. value return_value;

  13. return_value.t = script_parse_function_return_type(call_string);

  14. int count   = 0;
  15. value* args = script_parse_arguments(call_string,&count);
复制代码


如何将args[0], args[1], ... args[count-1], 传递给f?

stdarg.h 帮不上忙的。 它只能取出"数量未知"的参数, 而不是传入
传入参数的时候, 数量依然在编译时就被固定, 如:
printf("%s %s\n","hello","world"); /* 2个,编译后固定 */


dyncall的处理方式:
1. 抽象出一个参数列表模型, 它被dyncall称为DCCallVM。称为VM有点过头了……

  1. DCCallVM* vm = dcNewCallVM(4096);
复制代码


2. 每次只传入1个参数

  1. dcArgXXX(vm, arg); /* XXX代表传入参数类型 */
复制代码


这是一个重点。 每次只压入1个参数, 就可以组合出传递多个参数 —— 只要重复调用压入参数的函数即可, 如:

  1. for (int i = 0; i< count; ++i ) {
  2.     switch (args[ i ].t) {
  3.     case TYPE_DOUBLE: dcArgDouble(vm, args[ i ].v.d); break;
  4.     case TYPE_INT   : dcArgInt   (vm, args[ i ].v.i); break;
  5.     /* more */
  6.     }
  7. }
复制代码


3. 抽象出一个调用模型, 它在dyncall中是dcCallXXX(XXX代表返回类型)。
重点之一: dcCallXXX 现在是拥有固定数目参数的函数(VM、function)。 数目未知的参数已被VM代替


  1. switch( return_value.t. ) {
  2. case TYPE_INT   : return_value.v.i = dcCallInt(vm,(DCpointer)f); break;
  3. case TYPE_DOUBLE: return_value.v.d = dcCallDouble(vm,(DCpointer)f); break;
  4. /* more */
  5. }
复制代码


现在, 已经可以使用"编译时数目实际参数固定的函数调用", 表达数目未知的函数调用了。


重点之二: 也是该库牛逼的地方……
它直接使用了native assembly code来实现这个调用模型……
将vm中的参数们依次提取, 并合理(依平台、调用约定而定)的传递, 然后调用。

这样做比较麻烦的地方就是移植性。 该库好像也仅支持x86和ppc, 还要兼顾OS和编译器组合。
体力活啊……

这样做的优势在于:
只要写好解释器与dyncall交互的代码, 就可以"直接"调用任意函数(dyncall还提供了load模块,这不是重点,实现不难)了。
说任意函数, 依然有一些限制 —— 参数只能是buildin-type, 不能是aggregates (structures, unions and classes)。
dyncall是可以支持指针参数的。 指针参数的麻烦之处不在dyncall上, 而在解释器如何实现。


说它直接, 是相对于另外一种方式 —— 一种可移植的方式, 但被调用的函数需要包装一下。
比如lua, 就是采用的这种方式。




要在lua中调用一个C函数, 前面的步骤都是类似的:

1. lua抽象出一个参数模型。(lua具体是怎么做的忘了, 所以下面伪得不能再伪的代码
lua的参数模型不仅仅包括参数、还包括返回值。被称为一个stack。

2. 向stack传入和取出参数的API, 都只有固定个参数, 这样在编译时才能写出这个API调用。

  1. LuaStack* stack = luaStack_new();
  2. for (int i = 0;i < count; ++i )
  3.     luaStack_set(stack, args[ i ] ); /* 并不是一个严格的栈 */
复制代码


3. lua的调用模型, 是使用C实现的。 所以它必须是一个参数数目固定的API —— 至少调用时,参数个数是固定的。

  1. luaCall( f, stack );  /* [b]可以编译[/b] */
  2. [code]

  3. btw: lua解释器中, 函数调用完毕时, 还可以从stack中取出(多个)返回值。

  4. 所以, 这个f绝对不能是Beep, 需要包装一次。
  5. 这一次包装, 就把[b]参数列表的解析的工作留给了用户[/b]。[b] lua就不用写不可移植的代码[/b]。

  6. 用户就必须为每一个需要在lua中调用的函数, 实现一个参数数目固定的包装函数。
  7. 这函数签名我还记得 —— typedef int (*lua_CFunction)(lua_State* );

  8. [code]
  9. int BeepWrapper(lua_State* lua) {
  10. /*
  11.     从lua_State中取得参数(上面说的stack包含在lua_State中)。
  12.     这样就将数目未知的参数,通过lua_State一个参数表达出来。 这里需要取2个。
  13.     然后, 调用真正的Beep。
  14.     以及, 将返回值放回stack。 返回返回值的个数。
  15. */
  16. }
复制代码


还需要向lua注册这个BeepWrapper, 使得解释器可以找到f, 也就是上面的伪代码:

  1. lua_CFunction f = script_parse_function_name(call_string);
复制代码



综上:
它们都是抽象出一个参数列表模型, 来将数目未知的参数转换为数目固定的参数。
实现调用时, dyncall直接使用native assembly code, 无须包装、注册; 但需要为不同平台编写不同代码。
而lua将实现调用的工作, 转交给用户。 每一个要在lua中调用的C函数都需要实现一个包装函数,并注册。
但lua、以及用户代码中就不容易出现平台相关代码。

[ 本帖最后由 OwnWaterloo 于 2009-9-8 01:52 编辑 ]
作者: syncpk99    时间: 2009-09-08 09:39
楼上也够牛X的,找到这个库N久了,就是一眼也没看过啊
作者: nicolas.shen    时间: 2009-09-08 11:01
嗯的确有不少体力活,真是佩服啊
作者: 群雄逐鹿    时间: 2009-09-08 12:11
好像想到个用纯C++实现的办法
验证一下再来回复
作者: egmkang    时间: 2009-09-08 12:48
__asm
...
你还真不怕麻烦...
为啥不用函数指针直接来呢
作者: drangon    时间: 2009-09-08 13:07
函数指针的问题主要是参数固定,不能可变。

但其实很多时候简化一下设计,用函数指针就够用了,非要实现强大的功能或者灵活性,很多时候没太大必要,反而是自找麻烦,维护不过来还沾沾自喜说曲高和寡。。。。
作者: OwnWaterloo    时间: 2009-09-08 13:10
标题: 回复 #8 syncpk99 的帖子
原帖由 syncpk99 于 2009-9-8 09:39 发表
找到这个库N久了,就是一眼也没看过啊


因为以前做过类似的工作, 所以对它的代码感到很亲切
作者: OwnWaterloo    时间: 2009-09-08 13:13
标题: 回复 #9 nicolas.shen 的帖子
原帖由 nicolas.shen 于 2009-9-8 11:01 发表
嗯的确有不少体力活,真是佩服啊


嗯, 敢直接写汇编那真是体力活了……
即使在x86, 还要为不同编译器写不同的代码 ……
dyncall_call_x86_nasm.asm
dyncall_call_x86_gas.S
dyncall_call_x86_nasm.asm
dyncall_call_x86_apple.s

好像不是程序生成, 4个文件都是手写的。  相当的麻烦……


我以前是直接写x86的machine code。 就不用考虑不同编译器的问题了。
但又遇到了DEP的问题。 也是体力活……  稍微轻松一些。
作者: OwnWaterloo    时间: 2009-09-08 13:16
标题: 回复 #10 群雄逐鹿 的帖子
期待
作者: OwnWaterloo    时间: 2009-09-08 13:16
标题: 回复 #11 egmkang 的帖子
原帖由 egmkang 于 2009-9-8 12:48 发表
__asm
...
你还真不怕麻烦...
为啥不用函数指针直接来呢


你在仔细想想需求, 就知道函数指针也是帮不上忙的。
使用函数指针进行调用时, 参数个数在运行前就固定了。
作者: OwnWaterloo    时间: 2009-09-08 13:20
标题: 回复 #12 drangon 的帖子
原帖由 drangon 于 2009-9-8 13:07 发表
但其实很多时候简化一下设计,用函数指针就够用了,非要实现强大的功能或者灵活性,很多时候没太大必要,反而是自找麻烦,维护不过来还沾沾自喜说曲高和寡。。。。


嗯, 很多时候。 但不是所有情况下。
在极少的情况下, 有需求就是真正的需要 —— 绕弯去实现比设计一种方法直接支持要更麻烦, 甚至不可能。
作者: nicolas.shen    时间: 2009-09-08 13:48
其实想这样做,就是想在脚本里调用api,实在是还没有想到别的办法,期待 群雄逐鹿 兄弟的 C++实现
作者: 群雄逐鹿    时间: 2009-09-08 14:48
实现的还是很不爽,
抛砖引玉,这个代码可以可以动态的调用printf,目前只支持%s和%d两种参数。

主要就是用detect_stack_pos检测stack位置,
然后手动填写stack.
特别要满足 detect_stack_pos 和 被调用的函数(这里是printf)处在一摸一样的stack位置,
这个可以通过 数组 any_fn funcs[ 3 ]; 保证。

VC2008和cygwin下测试通过,cygwin 结果

$ ./a.exe '---%s---%d--%d---%s' ffeww 333 22 abcd
---ffeww---333--22---abcd

$ ./a.exe '---bbs%d.chinaunix.%s--' 3 net
---bbs3.chinaunix.net--

但是不足:
1. 需要用动态数组预留stack空间,VC只能用静态的代替。
2. 需要一个全局变量记录 stack检测结果,开始认为c++可以用类成员代替全局变量,但实际上代替不了(this指针怎么都必须有)
    如果全局变量的问题能解决,就比较好包装一个通用的库了。

还有返回值可以不可以用,还没验证。




  1. #include <stdlib.h>
  2. #include <stdio.h>

  3. typedef void (*any_fn)( );
  4. typedef struct
  5. {
  6.         void *stack_pos0;
  7.         void *stack_pos1;
  8.         int argc;
  9.         char **argv;
  10. } stack_t;

  11. stack_t g_stack;

  12. void detect_stack_pos( void *arg0, void *arg1 )
  13. {
  14.         stack_t *stack = &g_stack;
  15.         stack->stack_pos0 = (void *)&arg0;
  16.         stack->stack_pos1 = (void *)&arg1;
  17.         //printf( "stack pos = %p %p\n", &arg0, &arg1 );
  18. }
  19.        
  20. void set_stack( )
  21. {
  22.         stack_t *stack = &g_stack;
  23.        
  24.         int direction = ( stack->stack_pos0 < stack->stack_pos1 ? 1 : -1 );

  25.         int i;               
  26.         void *stack_pos = stack->stack_pos0;
  27.         const char *p;
  28.         const char *fmt = stack->argv[ 1 ];
  29.                
  30.         *(const char **)stack_pos = fmt;
  31.         stack_pos = (void *)( (const char **)stack_pos + direction );
  32.                
  33.         i = 2;
  34.         for( p = fmt; *p != '\0'; ++p )
  35.         {
  36.                 if( *p != '%' )
  37.                         continue;
  38.                 ++p;
  39.                 if( *p == '\0' )
  40.                         break;
  41.                        
  42.                 switch( *p )
  43.                 {
  44.                 case 'd':
  45.                         *(int *)stack_pos = atoi( stack->argv[ i++ ] );
  46.                         stack_pos = (void *)( (int *)stack_pos + direction );
  47.                         break;
  48.                 case 's':
  49.                         *(const char **)stack_pos = stack->argv[ i++ ];
  50.                         printf( "%s\n", *(const char **)stack_pos);
  51.                         stack_pos = (void *)( (const char **)stack_pos + direction );
  52.                         break;
  53.                 }
  54.         }
  55. }


  56. size_t get_min_stack_size( const char *fmt )
  57. {
  58.         size_t param_num = 1;
  59.         while( *fmt != '\0' )
  60.         {
  61.                 if( *fmt == '%' )
  62.                         ++param_num;
  63.                 ++fmt;
  64.         }
  65.        
  66.         /* Only support %d %s now, so just use sizeof(void *) instead */
  67.         return param_num * sizeof(void *);
  68. }

  69.        
  70. void call_func( int argc, char **argv )
  71. {
  72.         int i;
  73.         size_t min_stack_size;
  74.         any_fn funcs[ 3 ];
  75.        
  76.         funcs[ 0 ] = (any_fn)&detect_stack_pos;
  77.         funcs[ 1 ] = (any_fn)&set_stack;
  78.         funcs[ 2 ] = (any_fn)&printf;
  79.        
  80.         g_stack.argc = argc;
  81.         g_stack.argv = argv;
  82.        
  83.        
  84.         min_stack_size = get_min_stack_size( argv[ 1 ] );
  85.        
  86.         for( i = 0; i < sizeof( funcs ) / sizeof( funcs[ 0 ] ); ++i )
  87.         {
  88.                 volatile any_fn func = funcs[ i ];
  89.                 {
  90.                         /* Allocate stack size */
  91. #ifdef __GNUC__
  92.                         volatile char buffer[ min_stack_size ];
  93. #else
  94.                         volatile char buffer[ 400 ];
  95. #endif
  96.                         /* Prevent optimization by assigning a value */
  97.                         if( i <= 1 )
  98.                         {
  99.                                 buffer[ 0 ] = 0;
  100.                                 buffer[ sizeof( buffer ) / sizeof( buffer[ 0 ] ) - 1 ] = 0;
  101.                         }
  102.                        
  103.                         (*func)( );
  104.                 }
  105.         }
  106. };

  107. int main( int argc, char **argv )
  108. {
  109.         call_func( argc, argv );
  110.         return 0;
  111. }


复制代码

[ 本帖最后由 群雄逐鹿 于 2009-9-8 14:50 编辑 ]
作者: OwnWaterloo    时间: 2009-09-08 14:59
标题: 回复 #19 群雄逐鹿 的帖子
确实是纯C/C++的,也实现得很巧妙。

但不是可移植的, 因为代码中已经假设机器具有栈结构, 参数通过栈传递, 所有函数指针都有相同表示。
这些都是C/C++没有规定的。
作者: 群雄逐鹿    时间: 2009-09-08 15:06
原帖由 OwnWaterloo 于 2009-9-8 14:59 发表
确实是纯C/C++的,也实现得很巧妙。

但不是可移植的, 因为代码中已经假设机器具有栈结构, 参数通过栈传递, 所有函数指针都有相同表示。
这些都是C/C++没有规定的。


栈结构不用担心

栈传值和函数的调用约定是个问题。
必须有各种调用约定的 detect_stack_pos__??? 版本,才能模拟被所有调用函数的情景,这个太不好办。
除非被调用函数限定在有限的约定。
作者: OwnWaterloo    时间: 2009-09-08 15:13
标题: 回复 #21 群雄逐鹿 的帖子
据 dyncall的文档说, 也只有x86上才有这么多乱七八糟的calling convention ……
其他平台都要干净不少。

忽然想到一个问题:
void __fastcall f(int a,int b) {
    void* pa = &a; /* 这…… */
    void* pb = &b; /* 编译器怎么做?临时复制一份? */
}
这就是你说的栈结构不用担心的意思?  还是主流架构都有栈结构?
作者: 群雄逐鹿    时间: 2009-09-08 15:26
标题: 回复 #22 OwnWaterloo 的帖子
void* pa = &a; 要看是否优化吧,何况后面还可能改pa的值,++pa等。

不用stack的话,
如果寄存器传值,因为能传值的寄存上数量有上限,
比如最多传8个int的话,
只需要将any_fn的定义改成最多的参数个数,在对应位置填上参数即可。
这个很直观的,比预留/检测stack位置,然后填stack更容易做到。

typedef void (*any_fn)( int a, int b, int c, int d, int e, int f, ing g, int h );

弄了半天,还是不可移植,呵呵
作者: OwnWaterloo    时间: 2009-09-08 15:33
标题: 回复 #23 群雄逐鹿 的帖子
虽然不可移植,  还是好处的吧?  比如不需要写多种不同格式的汇编代码了?




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2