虽然编了几年程序,但是对于程序到底是什么规则变成汇编代码的,在这里搞了一个小程序。用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
关于这个程序的堆栈使用情况也做了一下分析,如图: