这几天突然很想弄明白c函数调用时栈的使用情况,上网查了一下资料,自已也研究了一下,这篇blog就把我的所得记录下来吧。
转自:http://hubeihuyanwei.blog.163.com/blog/static/28205284200821873911607/
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
下面来解释一下
开始进入Main函数 esp=0x12FF84 ebp=0x12FFC0
完成椭圆形框起来的部分
00401070 push ebp ebp的值入栈,保存现场(调用现场,从test函数看,如红线所示,即保存的0x12FF80用于从test函数堆栈返回到main函数)
00401071 mov ebp,esp 此时ebp=0x12FF80 此时ebp就是“当前函数堆栈”的基址 以便访问堆栈中的信息;还有就是从当前函数栈顶返回到栈底
00401073 sub esp,40h
函数使用的堆栈,默认64个字节,堆栈上就是16个横条(密集线部分)此时esp=0x12FF40
在上图中,上面密集线是test函数堆栈空间,下面是Main的堆栈空间 (补充,其实这个就叫做 Stack Frame)
00401076 push ebx
00401077 push esi
00401078 push edi 入栈
00401079 lea edi,[ebp-40h]
0040107C mov ecx,10h
00401081 mov eax,0CCCCCCCCh
00401086 rep stos dword ptr [edi]
初始化用于该函数的栈空间为0XCCCCCCCC 即从0x12FF40~0x12FF80所有的值均为0xCCCCCCCC
18: printf("%d",test(10,90));
00401088 push 5Ah 参数入栈 从右至左 先90 后10
0040108A push 0Ah
0040108C call @ILT+0(test) (00401005)
函数调用,转向eip 00401005
注意,此时仍入栈,入栈的是call test 指令下一条指令的地址00401091 下一条指令是add esp,8
@ILT+0(?test@@YAJHH@Z):
00401005 jmp test (00401020)
即转向被调函数test
因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax
注意,从被调函数返回时,是弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。
从test函数返回,执行
00401091 add esp,8
清栈,清除两个压栈的参数10 90 调用者main负责
(所谓__cdecl调用由调用者负责恢复栈,调用者负责清理的只是入栈的参数,test函数自己的堆栈空间自己返回时自己已经清除,靠!一直理解错)
00401094 push eax 入栈,计算结果108入栈,即printf函数的参数之一入栈
00401095 push offset string "%d" (0042201c) 入栈,参数 "%d" 当然其实是%d的地址
0040109A call printf (004010d0) 函数调用 printf("%d",108) 因为printf函数时
0040109F add esp,8 清栈,清除参数 ("%d", 108)
19: return 0;
004010A2 xor eax,eax eax清零
20: }
main函数执行完毕 此时esp=0x12FF34 ebp=0x12FF80
004010A4 pop edi
004010A5 pop esi
004010A6 pop ebx
004010A7 add esp,40h //为啥不用mov esp, ebp? 是为了下面的比较
004010AA cmp ebp,esp //比较,若不同则调用chkesp抛出异常
004010AC call __chkesp (00401150)
004010B1 mov esp,ebp
004010B3 pop ebp //ESP=0X12FF84 EBP=0x12FFC0 尘归尘 土归土 一切都恢复最初的平静了 :)
004010B4 ret
另
1. 如果函数调用方式是__stdcall 不同之处在于
main函数call 后面没有了 add esp, 8
test函数最后一句 是 ret 8 (由test函数清栈, ret 8意思是执行ret后,esp+8)
2. 运行过程中0x12FF28 保存了指令地址 00401091是怎么保存的?
栈每个空间保存4个字节(粒度4字节) 例如下一个栈空间0x12FF2C保存参数10
因此
0x12FF28 0x12FF29 0x12FF2A 0x12FF2B
91 10 40 00
little-endian 认为其读的第一个字节为最小的那位上的数
3. char a[] = "abcde"
对局部字符数组变量(栈变量)赋值,是利用寄存器从全局数据内存区把字符串“abcde”拷贝到栈内存中的
4. int szNum[5] = { 1, 2, 3, 4, 5 }; 栈中是如何分布的?
00401798 mov dword ptr [ebp-14h],1
0040179F mov dword ptr [ebp-10h],2
004017A6 mov dword ptr [ebp-0Ch],3
004017AD mov dword ptr [ebp-8],4
004017B4 mov dword ptr [ebp-4],5
可以看出来 是从右边开始入栈,所以是 5 4 3 2 1 入栈
int *ptrA = (int*)(&szNum+1);
int *ptrB = (int*)((int)szNum + 1);
std::cout<< ptrA[-1] << *ptrB << std::endl;
结果如何?
28: int *ptrA = (int*)(&szNum+1);
004017BB lea eax,[ebp]
004017BE mov dword ptr [ebp-18h],eax
&szNum是指向数组指针;加1是加一个数组宽度;&szNum+1指向移动5个int单位之后的那个地方, 就是把EBP的地址赋给指针
ptrA[-1]是回退一个int*宽度,即ebp-4
29: int *ptrB = (int*)((int)szNum + 1);
004017C1 lea ecx,[ebp-13h]
004017C4 mov dword ptr [ebp-1Ch],ecx
如果上面是指针算术,那这里就是地址算术,只是首地址+1个字节的offset,即ebp-13h给指针
实际保存是这样的
01 00 00 00 02 00 00 00
ebp-14h ebp-13h ebp-10h
注意是int*类型的,最后获得的是 00 00 00 02
由于Little-endian, 实际上逻辑数是02000000 转换为十进制数就为33554432
最后输出533554432
堆栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层函数,这是怎么做的呢?
首先,caller和callee在这个问题上要有一个“约定”,由于caller是不知道callee内部是如何执行的,因此caller需要从callee的函数声明就可以知道应该从什么地方取得返回值。同样的,callee不能随便把返回值放在某个寄存器或者内存中而指望Caller能够正确地获得的,它应该根据函数的声明,按照“约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”:
1)首先,如果返回值等于4字节,函数将把返回值赋予EAX寄存器,通过EAX寄存器返回。例如返回值是字节、字、双字、布尔型、指针等类型,都通过EAX寄存器返回。
2)如果返回值等于8字节,函数将把返回值赋予EAX和EDX寄存器,通过EAX和EDX寄存器返回,EDX存储高位4字节,EAX存储低位4字节。例如返回值类型为__int64或者8字节的结构体通过EAX和EDX返回。
3) 如果返回值为double或float型,函数将把返回值赋予浮点寄存器,通过浮点寄存器返回。
4)如果返回值是一个大于8字节的数据,将如何传递返回值呢?这是一个比较麻烦的问题,我们将详细讲解:
我们修改foo函数的定义如下并将它的代码做适当的修改:
MyStruct foo(int a, int b) { ... }MyStruct定义为:
struct MyStruct { int value1; __int64 value2; bool value3; };
这时,在调用foo函数时参数的入栈过程会有所不同,如下图所示:
图10
caller会在压入最左边的参数后,再压入一个指针,我们姑且叫它ReturnValuePointer,ReturnValuePointer指向caller局部变量区的一块未命名的地址,这块地址将用来存储callee的返回值。函数返回时,callee把返回值拷贝到ReturnValuePointer指向的地址中,然后把ReturnValuePointer的地址赋予EAX寄存器。函数返回后,caller通过EAX寄存器找到ReturnValuePointer,然后通过ReturnValuePointer找到返回值,最后,caller把返回值拷贝到负责接收的局部变量上(如果接收返回值的话)。
你或许会有这样的疑问,函数返回后,对应的堆栈帧已经被销毁,而ReturnValuePointer是在该堆栈帧中,不也应该被销毁了吗?对的,堆栈帧是被销毁了,但是程序不会自动清理其中的值,因此ReturnValuePointer中的值还是有效的。