运行时对代码操纵的一个小demo

昨晚跟NS老兄聊的时候,聊到运行时生成代码的问题。单就“[i]在运行的时候制造出一块数据,让它被执行[/i]”而言,完全没难度可言——其实就是申请一块内存空间,往那里写入一些代表指令的数据,然后调用那块代码就行了。如果要生成的代码是固定的,或者是很有规律的(只是类似填空那样的),那很好办~
一般觉得动态生成代码难那主要是说按需要动态从某种形式的源代码生成出对应的可执行代码有难度。或者说编译器的后端不好写 T T

写了个极简陋的demo来演示怎么“在运行时生成代码”(其实没那么夸张……)
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef int (*puts_ptr)(const char*);
typedef void (*myfunc_ptr)(puts_ptr);

/*
void foo(puts_ptr p) {
p("greetings from generated code!");
}
*/
/* corresponding assembler for generated code is like:
offset | bytes (in hex) | mnemonics
00 | 55 | push EBP
01 | 8BEC | mov EBP, ESP
03 | 68 ???????? | push ???????? ; address of string not known until run-time
08 | FF55 08 | call dword ptr [EBP + 8]
11 | 83C4 04 | add ESP, 4
14 | 5D | pop EBP
15 | C3 | ret
*/

int main() {
myfunc_ptr pMyfunc; /* pointer to generated code */
puts_ptr pPuts = &puts;
char* pCode = (char*)malloc(sizeof(char) * 50); /* allocate memory for code gen */
char* pGen = pCode; /* address to generate code to */
const char* pStr = "greetings from generated code!";

/* do code generation */

/* function prologue */
*pGen++ = 0x55; /* push EBP */
*pGen++ = 0x8B; *pGen++ = 0xEC; /* mov EBP, ESP */
/* function body */
*pGen++ = 0x68; /* push */
*((int*)pGen) = (int)pCode + 16; /* (address of string) */
pGen += 4;
*pGen++ = 0xFF; *pGen++ = 0x55; *pGen++ = 0x08; /* call dword ptr [EBP + 8] */
*pGen++ = 0x83; *pGen++ = 0xC4; *pGen++ = 0x04; /* add ESP, 4 */
/* function epilogue */
*pGen++ = 0x5D; /* pop EBP */
*pGen++ = 0xC3; /* ret */
/* make a copy of the string, following the generated code */
strcpy(pGen, pStr);

/* end of code generation */

/* invoke generated code */
pMyfunc = (myfunc_ptr)pCode;
pMyfunc(pPuts);

free(pCode);
return 0;
}

上面的代码所做的事情就是:在堆上申请一块空间,向里面填充一组指令,然后把这块空间当成一个函数,通过函数指针去调用刚生成的代码。生成的代码与它要显示的字符串“打包”在了一起。
所生成的函数内容如代码中注释所示,用C写的话大致等同于被注释掉的那个foo()。唯一不同的是:如果解除foo()的注释并编译的话,其中的"greetings from generated code!"字符串是直接编译到可执行文件的数据段,跟可执行代码所在的代码段不在一起;而生成的代码里,我是直接把这个字符串放在紧跟生成的可执行代码的后面。所以在第38行有个magic number,16,这个数字是生成的可执行代码的大小;我需要这个数字作为偏移量来计算出字符串在实际运行时的地址。
开头的push EBP和mov EBP, ESP,与末尾的pop EBP,这几条指令是用来管理栈帧指针(frame pointer,简称FP)的。在这里其实没什么实际用处,因为我没用到动态大小的栈帧;但我生成的代码并不是一个叶函数(它还调用了puts;叶函数是不调用别的函数的函数),而puts里还是用到了栈帧指针的,如果我不照例设置栈帧指针的话,整个过程就不太顺了……所以虽然没啥用还是写了这部分。
因为在32位Windows上,MSVCRT里puts的calling convention是[url=http://msdn.microsoft.com/en-us/library/zkwh89ks.aspx]__cdecl[/url],所以在调用了puts之后得自己清理栈,所以要生成那个add ESP, 4的指令。
在生成的代码里,设置好栈帧指针后,dword ptr [EBP]指向是就是老的FP的值,dword ptr [EBP+4]是函数的返回地址(由call指令压进来的),dword ptr [EBP+8]就是第一个参数(也是这个函数接收的唯一一个参数)。这也是遵循__cdecl的。

我用函数指针来调用生成的代码,是因为执行完那块代码后我还想让控制流回到main()里。如果不用回来的话,直接JMP也可以的。要JMP就得内嵌汇编了,直接用goto不行。

这个demo在32位Windows XP上用VC9和GCC 4.3编译运行都没问题,执行结果就是把问候语输出出来。但这其实作 弊了:在Windows XP上,[url=http://en.wikipedia.org/wiki/Data_Execution_Prevention]DEP(Data Execution Prevention)[/url]默认是不对一般应用程序启动的。因此我可以malloc得到堆上一块空间,然后通过call指令跳转到那里把它当作可执行代码来执行。如果DEP打开了的话,分配的空间必须标有PAGE_EXECUTE、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE或者PAGE_EXECUTE_WRITECOPY属性才可以执行;通过C++的[color=blue]new[/color]、C的[color=blue]malloc[/color]或者Win32 API的[color=blue]HeapAlloc[/color]分配的内存空间都[b][color=red]不可执行[/color][/b]。所以这个demo要是在Windows Vista或者Windows 7上运行就会出错。在这些平台上,可以通过[color=blue]VirtualAlloc[/color]函数分配空间,只要设置要相应属性,还是可以申请到可执行的内存空间的。这种做法的好处是我自己调用VirtualAlloc的时候我肯定是准备要生成可信任的代码,那么执行不会有问题;如果别人想通过栈溢出之类的方法来hack我的程序,栈上的空间是不可执行的,他们就不能随意修改栈上内容当成代码来执行。
话说用[url=http://msdn.microsoft.com/en-us/library/aa366898(VS.85).aspx]VirtualProtect[/url]能改变已提交的page的权限……可写可执行权限可以想办法搞过来~

要是NS老兄有兴趣的话我可以演示一下生成jump table的代码……如果你要自己来的话那我就不spoil the game了。
Have fun ^ ^

P.S. 借地放图。这是Windows XP SP2之后开始Performance对话框里新增的tab,可以设置DEP的应用范围。
[img]/upload/attachment/128343/3f93511c-dfe7-3300-9669-b1e00377869a.jpg[/img]

P.S.S. (2009-09-07)刚才在Ubuntu 9.04下试一些代码的时候正好想起这个例子,也顺便试了下,果然能编译执行成功。我是不是应该换到有nx实现的CPU上再在Ubuntu里试试……?

[url=http://blogs.oracle.com/nike/]Nikolay Igotti[/url]写过一篇稍微实用一些的同类小例子:[url=http://blogs.oracle.com/nike/entry/simple_jit_compiler_for_your]Simple JIT compiler for your application[/url]
这里还有一篇用Rust写的例子:[url]http://www.hydrocodedesign.com/2014/01/17/jit-just-in-time-compiler-rust/[/url]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值