虽然编了几年程序,但是对于程序到底是什么规则变成汇编代码的,在这里搞了一个小程序。用VC查看了一下汇编代码。在此之前先介绍一下关于函数运行是堆栈变化的细节。
在高级语言编写程序时,函数的调用是很常见的事情,但是在函数调用过程中堆栈的变化通常有几个细节:
1.父函数将函数的实参按照从右至左的顺序压入堆栈;
2.CPU将父函数中函数调用指令Call的下一条指令地址EIP压入堆栈;
3.父函数通过Push Ebp指令将基址指针EBP的值压入堆栈,并通过Mov Ebp,Esp指令将当前堆栈指针Esp值传给Ebp;
4.通过Sub Esp,m(m是字节数)指令可以为存放函数中的局部变量开辟内存。函数在执行的时候如果需要访问实参或局部变量,都可以通过EBP指针来指引完成。
windows系统下常用的函数调用通常有种,__cdecl和__stdCall。
1.在VC、.net等开发环境中,编写命令行程序时的Main或者_tmain函数,以及大家自己定义的很多函数都是默认采用__cdecl调用方式;
2.通过MFC编写图形界面程序的时候,其主函数声明为extern "C" int WINAPI tWinMain(参数),该函数的调用约定是__stdCall。WINAPI和PASCAL等都是__stdCall的宏定义,是一个意思,此外,大家平时调用的API函数,绝大多数都是采用__staCall的调用方式;
3.__cdecl调用方式的函数,父函数在调用子函数的时候,先将子函数的实参按照从右至左的顺序压入堆栈中,子函数返回后,父函数通过Sub Esp,n(n=函数实参个数*4)指令来恢复堆栈;
4.__stdCall调用约定函数,子函数调用时实参入栈顺序也是从左到右,但是堆栈恢复是子函数返回时自己通过Ret n指令来完成的。
下边就是针对这些知识进行的部分实践:
#include<stdio.h>
#include<windows.h>
#include<stdlib.h>
int fun(char *szIn, int nTest)
{
char szBuf[9];
printf("%d\n",nTest);
strcpy(szBuf,szIn);
return 0;
}
int main(int argc, char *argv[])
{
char sz_In[] = "1234567";
fun(sz_In,888);
return 0;
}
汇编代码
00401003 int 3
00401004 int 3
@ILT+0(?fun@@YAHPADH@Z):
00401005 jmp fun (00401020) //进入fun函数
@ILT+5(_main):
0040100A jmp main (00401080) //进入main函数,该位置是整段代码的入口
0040100F int 3
00401010 int 3
00401011 int 3
00401012 int 3
00401013 int 3
00401014 int 3
00401015 int 3
00401016 int 3
00401017 int 3
00401018 int 3
00401019 int 3
0040101A int 3
0040101B int 3
0040101C int 3
0040101D int 3
0040101E int 3
0040101F int 3
--- c:\project\heap1\heap1.cpp --------------------------------------------------------------------------------------------------------------------------------------
1: #include<stdio.h>
2: #include<windows.h>
3: #include<stdlib.h>
4: int fun(char *szIn, int nTest)
5: {
00401020 push ebp
00401021 mov ebp,esp //保存基址指针,并将现在的栈顶保存为基址指针。
00401023 sub esp,4Ch //腾出一部分堆栈区用于存放局部变量。
00401026 push ebx
00401027 push esi
00401028 push edi //保存三个寄存器的值。
00401029 lea edi,[ebp-4Ch]
0040102C mov ecx,13h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi] //将腾出的4Ch的空间初始化值为0xCC。
6: char szBuf[9];
7: printf("%d\n",nTest);
00401038 mov eax,dword ptr [ebp+0Ch]
0040103B push eax
0040103C push offset string "%d\n" (0042201c) //先后压入栈中两个地址,nTest,一个是一个字符串指针。
00401041 call printf (004011d0) //调用printf函数时,它会自动做到堆栈平衡。
00401046 add esp,8 //由于刚才压入和两个参数,所以在这里手动将两个参数弹出堆栈
8: strcpy(szBuf,szIn);
00401049 mov ecx,dword ptr [ebp+8]
0040104C push ecx //压入szIn的指针。这个参数在高出基址的8位处,也就是调用该函数前压入栈中的。
0040104D lea edx,[ebp-0Ch]
00401050 push edx //压入szBuf的指针,这个函数在低于基址的OCh位处,这是调用函数后分配的。局部变量的分配大
00401051 call strcpy (004010e0) //小都是按4的倍数分配的,所以尽管szBuf[9]但是也分配在了0Ch处。
00401056 add esp,8
9: return 0;
00401059 xor eax,eax //返回值在EAX中。
10: }
0040105B pop edi
0040105C pop esi
0040105D pop ebx //弹出保存的数据。
0040105E add esp,4Ch //消除为局部变量腾出的空间。
00401061 cmp ebp,esp
00401063 call __chkesp (00401250) //检验是否在用户自定义汇编代码中修改了ebp和esp的相对关系。一般情况下EBP>ESP
00401068 mov esp,ebp //将原基址恢复给栈顶寄存器。
0040106A pop ebp //弹出原调用函数的堆栈基址。
0040106B ret //函数返回。
--- No source file --------------------------------------------------------------------------------------------------------------------------------------------------
0040106C int 3
0040106D int 3
0040106E int 3
0040106F int 3
00401070 int 3
00401071 int 3
00401072 int 3
00401073 int 3
00401074 int 3
00401075 int 3
00401076 int 3
00401077 int 3
00401078 int 3
00401079 int 3
0040107A int 3
0040107B int 3
0040107C int 3
0040107D int 3
0040107E int 3
0040107F int 3
--- c:\project\heap1\heap1.cpp --------------------------------------------------------------------------------------------------------------------------------------
11: int main(int argc, char *argv[])
12: {
00401080 push ebp
00401081 mov ebp,esp //保存堆栈基址
00401083 sub esp,48h //腾出局部变量空间
00401086 push ebx
00401087 push esi
00401088 push edi //保存3个寄存器
00401089 lea edi,[ebp-48h]
0040108C mov ecx,12h
00401091 mov eax,0CCCCCCCCh //初始化局部变量空间
00401096 rep stos dword ptr [edi]
13: char sz_In[] = "1234567";
00401098 mov eax,[string "1234567" (00422020)]
0040109D mov dword ptr [ebp-8],eax
004010A0 mov ecx,dword ptr [string "1234567"+4 (00422024)]
004010A6 mov dword ptr [ebp-4],ecx //将字符串通过寄存器将字符拷贝到分配的空间中。
14: fun(sz_In,888);
004010A9 push 378h
004010AE lea edx,[ebp-8]
004010B1 push edx //从右至左将参数压入堆栈中,数字直接压入数值,字符串则压入字符串指针
004010B2 call @ILT+0(fun) (00401005)
004010B7 add esp,8 //恢复堆栈
15: return 0;
004010BA xor eax,eax //返回值在EAX中
16: }
004010BC pop edi
004010BD pop esi
004010BE pop ebx //恢复3个寄存器
004010BF add esp,48h //清除局部变量空间
004010C2 cmp ebp,esp
004010C4 call __chkesp (00401250) //检测堆栈指针与堆栈基址
004010C9 mov esp,ebp //恢复调用函数的栈顶
004010CB pop ebp //恢复调用函数的堆栈基址
004010CC ret //函数返回
--- No source file --------------------------------------------------------------------------------------------------------------------------------------------------
004010CD int 3
004010CE int 3
关于这个程序的堆栈使用情况也做了一下分析,如图: