分类: C/C++
2009-09-21 09:25:33
一个C语言函数原型 void GetParameterFromStack(void* p1, void* p2, void* p3),在Intel 32位X86结构运行,运行环境为VS2005,当刚执行到该函数,尚未执行任何函数代码,此时ESP为 0x0012FD78,且有ESP前后的一段内存,如下 :
0x0012FD28 cc cc cc cc e8 6c 3a 00 cc cc cc cc 6c fd 12 00
0x0012FD38 2c 1c 41 00 12 c8 02 78 68 ff 12 00 88 fd 12 00
0x0012FD48 00 80 fd 7f 01 00 00 00 64 00 00 00 70 fd 12 00
0x0012FD58 8f 90 26 10 a8 ff 12 00 a5 10 41 00 1e 48 51 78
0x0012FD68 fe ff ff ff 68 ff 12 00 a3 15 41 00 f4 6c 3a 00
0x0012FD78 e7 15 41 00 40 68 3a 00 44 ff 12 00 44 68 3a 00
0x0012FD88 00 00 00 00 f4 f9 33 0a 00 80 fd 7f 44 68 3a 00
0x0012FD98 cc cc cc cc 40 68 3a 00 cc cc cc cc cc cc cc cc
请问函数GetParameterFromStack(void* p1, void* p2, void* p3)中参数p1, p2, p3的值分别是多少?
如果你从上面轻松的可以得到参数p1=0x003a6840, p2=0x12ff44, p3= 0x003a6844,你就可以直接忽略该知识点。当然如果你能进一步得到这样的信息,p2对应传递的参数应该是一个局部变量,p1和p3对应传递的参数肯定不是分配在栈空间,也应该不是全局变量,分配在堆空间的,最后你也知道0x004115e7是函数返回后要执行的地址。如果你尚未能准确的得到每一个参数,请往下看。
这个题目涉及到两个知识点,其一就是函数调用方式,其二就是call指令的作用。函数调用约定请参看转载文章(http://blog.csdn.net/abortexit/archive/2009/06/19/4281988.aspx)。Call指令是CPU提供的,是用于完成函数调用,CPU执行Call指令时将下条指令偏移压入堆栈,然后设置eip寄存器为调用地址,从而完成函数调用。下条指令的地址存放在EIP中,所以Call指令相当于成两条指令:push eip;jmp subAddress。所以我们要知道call执行会影响栈空间。
在这里我将结合standard call 和C Declaration两种方式,以及call指令的作用演示栈空间的变化。下面看一个汇编和C语言混合编程的代码示例:
#include
void __cdecl fun1(int uPara1,int uPara2)
{
std::cout << "cdecl: uPara1 "<< uPara1 << std::endl;
std::cout << "cdecl: uPara2 "<< uPara2 << std::endl;
return;
}
void __stdcall fun2(int uPara1,int uPara2)
{
std::cout << "stdcall: uPara1 "<< uPara1 << std::endl;
std::cout << "stdcall: uPara2 "<< uPara2 << std::endl;
return;
}
int main(void)
{
//!! Fix me
__asm
{
push 2
push 1
push lab
jmp fun1
lab:
pop eax
pop eax
}
__asm
{
push 2
push 1
call fun2
}
return 0;
}
上面的示例代码可以直接运行,现在我们来看fun1的调用,首先push 2,然后push 1,接着push了一个lab,这个lab显然是一个地址值,后面使用指令jump跳转到fun1的入口开始执行。刚才我说了,一个call指令相当于一个push和一个jump指令,那么我们来看看这个push和jump能不能完成call的功能,push lab是把jmp指令后面的地址入栈,刚好满足call指令的第一个功能即把下一条指令的地址入栈,接着使用jmp跳转到地址fun1执行。
如果fun1是一个函数,即被编译后有ret指令,那么push和jmp就相当于一个call指令。如果fun1不是一个函数,即没有ret指令,那么push和jmp应该被分别理解。在这里显然fun1是一个函数,所以我使用了push和jmp完成了call指令的功能。Fun1有两个参数,push 2和push 1就是参数传递,fun1的调用约定是__cdecl所以参数从右至左依次传递,因此我们知道uPara2的值应该是2,uPrar1的值应该是1。对于函数fun2和此类似,uPara2的值应该是2,uPrar1的值应该是1。只不过不需要调用者清空参数。
我们继续看fun1的调用,我们知道__cdecl 需要调用者清空栈,所以在lab后我使用了两个pop指令完成两个push的逆操作。
到这里还没有结束,你直接运行这个程序可以得到正确的结果,可以我在程序中还放置了一个注释Fix me。你能看出哪里有问题?这段代码是为了演示,所以我使用了pop来完成push的逆操作,我把push中栈的数据pop到eax中,这是不合适的,原因很简单,我在使用eax之前没有保存,即破坏了eax原有的值,你可以在push 2之前加入push eax,然后在lab之后多执行一次pop eax就比较合理了。但是这在某些情况下还是有错误的,我们知道eax在很多情况下被用来保存返回值,所以上面的处理可能把返回值给破坏了,其实如果你完美理解了__cdecl的调用方式,你应该知道怎么处理的,使用汇编指令add esp, 8就可以了。
好了,背景知识已经介绍完了,那么对于这个题目,我想你也能轻松的得到结果吧。Void*在32bits机器中占用了4bytes,当程序执行到函数GetParameterFromStack,esp为0x0012FD78,根据调用知识,我们知道栈顶存放的应该是函数返回后要执行的地址。由于在VS中内存是little-endian方式,所以0x004115e7就是函数GetParameterFromStack返回后要执行的地址,根据参数入栈顺序得到p1=0x003a6840, p2=0x12ff44, p3= 0x003a6844。
最后再提一点,以后有时间还会介绍,就是p2=0x12ff44为何是栈变量?p1和p3为何不是全局变量和栈变量?或者是根据操作系统地址空间的分配,或者是根据程序已有的信息都差不多能得到同样的结论,ESP指向的地址为0x0012FD78,所以p2应该是在栈上,有根据0x004115e7为代码段的地址,数据段是和代码段相连,且p1和p3指向的地址不在栈上,所以最有可能存放在堆上。