欢迎有条件的同学访问墙外的地址:http://lzsblog.appspot.com/%3Fp%3D280001
前些天搞dr.com的破解时有很多收获,一直想总结一下,以后也许有用,今天开个头,以后有时间再慢慢整理。
写过dll的同学都知道stdcall 函数和一般的C函数是不一样的。最直观的不同就是在声明时stdcall函数要加上宏WINAPI。研究过导出表的同学可能会对这个问题有更深入的理解,因为当你输出一个C函数时,通常会加上特定的名字修饰。比如_foo@4(这个修饰方式和编译器实现相关)。你必须强制使用正确的函数名以防止LoadLibrary函数无法访问你的C函数。
那么究竟什么是stdcall,为什么有stdcall和cdecl的区别,为什么cdecl(也就是一般的C函数)要加上如此奇怪的名字装饰呢。本文将从编译和执行的角度解释以上问题。阅读本文你需要一定的汇编基础。不太明白的同学请参看有关资料。
一下叙述默认使用IA32结构。汇编代码会使用AT&T风格描述。如果你熟悉Intel风格,其实你要做的只是去掉每个指令的修饰后缀(如movl 中的l),它代表操作数的大小,l表示long,也就是4Byte。另外你需要忽略寄存器前的%和立即数前的$。
1、过程(函数)调用的一般步骤
如果你不是使用不靠谱的国内汇编教材,你都会在学习汇编语言或计算机组成原理时了解过程调用的一般步骤。如果你已经熟悉过程调用的步骤,你可以直接跳到下一节。
就像没有灯光的时候,你女伴的长相不是需要关注的第一问题一样。在较底层的空间里徘徊时,我们通常不需要区别函数和过程。因为函数只不过是有返回值的过程,而在机器层面,返回不过是把需要返回的值赋给指定的寄存器而已。
典型的过程调用有以下几个步骤。
1、将参数放到指定的区域,对于我们今天讨论的问题,这一步骤相当于以指定方式将参数压栈。而对于fastcall,参数是通过寄存器传递的。这也解释了它名字的来历。
2、将记录当前程序运行位置的寄存器值(IA32中为EIP)压栈。跳转到被调用过程所在的内存区域。IA32提供call指令完成上述过程。虽说call看起来只有一个指令,可是它处理的问题有很多。首先,它会将%EIP压栈,这是用来返回的,因为当被调过程完成时需要知道如何回到主调过程。然后他会执行一个类似于jmp的指令跳转到指定程序的位置。
现在出现的一个问题是,当被调用过程开始执行时,栈顶指针和栈基址貌似与正常不符,一般情况下,一个过程应该能确定启动时自己的栈基址和栈顶指针相同。而对于被调用过程,这一假设通常由于局部变量的使用(他们位于栈中)而不成立。因此,一个过程开始时,会把栈基址压栈,然后将栈基址设为当前的栈顶指针。这就是为什么每个过程前面有类似pushl %ebp和mov %esp, %ebp
3、在返回之前,你需要处理一些善后的事宜,包括恢复系统栈的栈基址和栈顶指针。你可以使用leave指令来完成恢复栈基址的工作。
4、当过程完成时,从栈里弹出主调用过程停止处的地址。并跳转过去,使得主调用过程继续进行。IA32提供ret完成上述过程。有时你可以使用retn n来执行上述步骤,后面跟的n是在返回之后esp将会增加的值。
2、关键问题:谁来善后栈顶指针
刚才说过leave指针可以用来恢复栈基址。它的工作方式是先将栈顶指针设为栈基址,这里如前面所说,保存着主调进程的栈基址。然后将这个栈基址弹出,这时栈顶指针会指向返回地址,而栈基址会恢复到主调进