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

[url=http://rednaxelafx.iteye.com/blog/428721]前一篇[/url]演示了在堆上分配空间,生成出可执行代码,并执行之。一般来说可执行代码都是在代码段里的,在堆上和在数据段里都的不是“正常编译”得到的代码。这次就来演示一下把代码放到数据段的例子吧~

代码如下。跟前一篇一样,把对应的机器码和汇编都写在注释里,方便理解。
#include <stdio.h>

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

/*
void foo(puts_ptr p) {
p("greetings from generated code!");
}
*/
/* code as string in data section:
offset | bytes (in hex) | mnemonics
00 | 55 | push EBP
01 | 8BEC | mov EBP, ESP
03 | E8 00000000 | call next instruction
08 | 58 | pop EAX
09 | 83C0 0D | add EAX, 0D
12 | 50 | push EAX
13 | FF55 08 | call dword ptr [EBP + 8]
16 | 83C4 04 | add ESP, 04
19 | 5D | pop EBP
20 | C3 | ret
*/

int main() {
myfunc_ptr pMyfunc;
puts_ptr pPuts = &puts;
const char* code = "\x55\x8B\xEC\xE8\x00\x00\x00\x00"
"\x58\x83\xC0\x0D\x50\xFF\x55\x08"
"\x83\xC4\x04\x5D\xC3"
"greetings from generated code!";

pMyfunc = (myfunc_ptr)code;
pMyfunc(pPuts);

return 0;
}

可以看到,这个例子的关键就是源码里的字符串字面量——那堆奇怪的\xNN和后面接着的问候语。不要忘记C/C++里相邻的字符串字面量会被编译器合并为一个字符串常量。Windows上的[url=http://en.wikipedia.org/wiki/Portable_Executable]PE文件[/url]区分代码段(一般在.text section里)与数据段(一般在.data section和.rdata section;后者是只读数据段)里,而这个字符串字面量就会被安置在数据段中。

这个例子里放在数据段的代码的结构与前一篇放在堆上的代码基本一样,也是把代码与其需要的数据混在一起,前面是代码,字符串数据紧跟在代码后。
不同的是:前一篇生成的代码中第3条指令是一个push imm32,压入栈的是直接量;这个直接量是在生成代码时通过相对偏移量计算出来的。这个例子则使用了一个小trick来获取基地址,直接在代码中计算push的值,使用压入栈指令是push EAX。这个trick是以下的指令序列:
offset | bytes (in hex) | mnemonics
03 | E8 00000000 | call next instruction
08 | 58 | pop EAX
09 | 83C0 0D | add EAX, 0D

首先是一条call imm32指令,参数是0x00000000。这条指令的语义是:把下一条指令的地址作为返回地址压入栈,并且跳转到下一条指令的地址+0x00000000的位置。注意这条指令的32位直接量是2的补码,是带符号的。执行这条指令之后,我们就让CPU把call指令的下一条指令的地址压到了栈上,这样就可以知道“基地址”是多少了。
接下来是pop EAX,把刚才压到栈上的“返回地址”弹出来,赋值给EAX。
然后是add EAX, imm8。数一下指令的长度,可以知道要显示的字符串距离pop EAX指令的偏移量是13,也就是0x0D;所以把前面得到的“基地址”加上这个偏移量,就得到了要显示的字符串的起始地址。
后面的指令序列基本上就跟前一篇的一样了,不用多解释。

这种获取地址的trick在可重定位的代码(relocatable code)中很常见。例如编译器生成一个DLL的时候,它不知道实际运行的时候装载器会把DLL image装载到什么地址,所以里面跟地址相关的代码都得编译为基地址+偏移量的形式。基地址的获取经常就是用类似的trick来完成的。

前一篇提到了Windows Vista和Windows 7上的DEP,它对数据段的可执行性也有限制:数据段内存被认为是[b]不可执行[/b]的。所以这个例子在上述两种操作系统(和对应的Server版)里直接编译运行会出错。这些系统认为可执行代码就应该保存到代码段,所以上例中放在字符串中的代码也应该移到代码段中。

在堆上和数据段里放代码有什么意思呢?就像有些时候C的switch...case语句会被编译为基于表的跳转,而这个跳转表就夹杂在指令序列中一样,代码和数据其实没有明显的界限——冯·诺依曼体系结构的机器上,存储器既可以用于保存代码,也可以用于保存数据;代码就是数据,数据可以看作代码来执行。这篇和前一篇的demo只是演示了这一点而已。引申开来,有时候我们会希望根据运行时的某些特定条件来动态生成些效率较高的、特化的代码,而不是使用通用但效率较低的代码,这个时候就需要用到动态代码生成技术。生成的代码放在内存里,如果不能执行那就不好玩了。这两个demo演示了动态生成代码是可执行的。

下次再写咯……喝茶去~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值