在上一篇文章的最后我提到尝试拷贝二进制代码然后修改函数跳转地址来包装函数,但是失败了。这种做法其实是在动态的创建出新的代码,每个 wrap 函数其实对应了一个新的函数。(用 nested function 来实现 closure 时,函数只有一个,只不过环境不同。)
如果可以在运行时编译代码,就可以通过动态生成代码的方式来包装函数了。
当我发现可以用 libtcc 动态生成代码来包装 C 函数的时候忍不住再次感慨,Fabrice Bellard (QEMU,FFMPEG 的创建者) 真是神一样的程序员!运行时编译代码听起来很唬人,但 libtcc 作为Tiny C Compiler (TCC) 的一个部分真的是非常简单易用!
TCC 是 Bellard 从 2001 年的 C 混淆代码比赛上写的一个 C 编译器 OTCC 发展而来的。(OTCC 只能编译 C 的一个子集。)TCC 本身非常小,x86 上的可执行文件才 100k,但是完整支持 C99。由于 TCC 的编译速度非常快,你可以在 C 文件的第一行加上 “#!/usr/local/bin/tcc -run”,然后把 C 程序当成脚本来执行。
本文的重点是用 libtcc 来动态生成代码,从而在 C 语言中包装函数。
如果要执行下面的例子代码,需要先下载 TCC 并编译安装。TCC 源码包里的 tests/libtcc_test.c 是一个使用 libtcc 动态编译代码的例子,非常简单。下面给出用 libtcc 来包装函数的例子,其实在 create_wrap_function
中我只是稍微修改了下 libtcc_test.c 里面的代码,保留了原先代码的注释。
使用命令 gcc libtcc-wrap.c -o libtcc-wrap -ldl -ltcc
编译后即可执行。
这个例子里面,我们把包装函数的代码保持在字符串里,用 libtcc 在运行时编译这段源代码。为了在动态编译的源代码中使用其他的函数,我们需要用 tcc_add_symbol
添加这些函数。被包装函数在源代码中的符号是 origin
,我们用实际的被包装函数的指针作为这个符号的 location。
create_wrap_function
中调用 libtcc 来编译代码的过程看起来还是很简单的。虽然这种方式对于每个被包装的函数都要生成新的代码,但它能工作在 libtcc 支持的所有平台上。
有了动态编译源代码的能力,我们是不是可以在 C 中创建出类似解释型语言中 REPL 的东西?不过其实已经有现成的 C 解释器产品了,有兴趣的同学可以尝试下 Ch。