C/C++拾遗录--关于一个C语言小程序的分析

本文详细介绍了在Windows环境下,函数调用时堆栈的变化过程及管理方式,特别是对于__cdecl和__stdcall两种调用约定的具体实现差异,并通过一个C++示例程序的源代码与对应的汇编代码进行了深入分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虽然编了几年程序,但是对于程序到底是什么规则变成汇编代码的,在这里搞了一个小程序。用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指令来完成的。

下边就是针对这些知识进行的部分实践:

  1. #include<stdio.h>
  2. #include<windows.h>
  3. #include<stdlib.h>
  4. intfun(char*szIn,intnTest)
  5. {
  6. charszBuf[9];
  7. printf("%d\n",nTest);
  8. strcpy(szBuf,szIn);
  9. return0;
  10. }
  11. intmain(intargc,char*argv[])
  12. {
  13. charsz_In[]="1234567";
  14. fun(sz_In,888);
  15. return0;
  16. }

汇编代码

  1. 00401003int3
  2. 00401004int3
  3. @ILT+0(?fun@@YAHPADH@Z):
  4. 00401005jmpfun(00401020)//进入fun函数
  5. @ILT+5(_main):
  6. 0040100Ajmpmain(00401080)//进入main函数,该位置是整段代码的入口
  7. 0040100Fint3
  8. 00401010int3
  9. 00401011int3
  10. 00401012int3
  11. 00401013int3
  12. 00401014int3
  13. 00401015int3
  14. 00401016int3
  15. 00401017int3
  16. 00401018int3
  17. 00401019int3
  18. 0040101Aint3
  19. 0040101Bint3
  20. 0040101Cint3
  21. 0040101Dint3
  22. 0040101Eint3
  23. 0040101Fint3
  24. ---c:\project\heap1\heap1.cpp--------------------------------------------------------------------------------------------------------------------------------------
  25. 1:#include<stdio.h>
  26. 2:#include<windows.h>
  27. 3:#include<stdlib.h>
  28. 4:intfun(char*szIn,intnTest)
  29. 5:{
  30. 00401020pushebp
  31. 00401021movebp,esp//保存基址指针,并将现在的栈顶保存为基址指针。
  32. 00401023subesp,4Ch//腾出一部分堆栈区用于存放局部变量。
  33. 00401026pushebx
  34. 00401027pushesi
  35. 00401028pushedi//保存三个寄存器的值。
  36. 00401029leaedi,[ebp-4Ch]
  37. 0040102Cmovecx,13h
  38. 00401031moveax,0CCCCCCCCh
  39. 00401036repstosdwordptr[edi]//将腾出的4Ch的空间初始化值为0xCC。
  40. 6:charszBuf[9];
  41. 7:printf("%d\n",nTest);
  42. 00401038moveax,dwordptr[ebp+0Ch]
  43. 0040103Bpusheax
  44. 0040103Cpushoffsetstring"%d\n"(0042201c)//先后压入栈中两个地址,nTest,一个是一个字符串指针。
  45. 00401041callprintf(004011d0)//调用printf函数时,它会自动做到堆栈平衡。
  46. 00401046addesp,8//由于刚才压入和两个参数,所以在这里手动将两个参数弹出堆栈
  47. 8:strcpy(szBuf,szIn);
  48. 00401049movecx,dwordptr[ebp+8]
  49. 0040104Cpushecx//压入szIn的指针。这个参数在高出基址的8位处,也就是调用该函数前压入栈中的。
  50. 0040104Dleaedx,[ebp-0Ch]
  51. 00401050pushedx//压入szBuf的指针,这个函数在低于基址的OCh位处,这是调用函数后分配的。局部变量的分配大
  52. 00401051callstrcpy(004010e0)//小都是按4的倍数分配的,所以尽管szBuf[9]但是也分配在了0Ch处。
  53. 00401056addesp,8
  54. 9:return0;
  55. 00401059xoreax,eax//返回值在EAX中。
  56. 10:}
  57. 0040105Bpopedi
  58. 0040105Cpopesi
  59. 0040105Dpopebx//弹出保存的数据。
  60. 0040105Eaddesp,4Ch//消除为局部变量腾出的空间。
  61. 00401061cmpebp,esp
  62. 00401063call__chkesp(00401250)//检验是否在用户自定义汇编代码中修改了ebp和esp的相对关系。一般情况下EBP>ESP
  63. 00401068movesp,ebp//将原基址恢复给栈顶寄存器。
  64. 0040106Apopebp//弹出原调用函数的堆栈基址。
  65. 0040106Bret//函数返回。
  66. ---Nosourcefile--------------------------------------------------------------------------------------------------------------------------------------------------
  67. 0040106Cint3
  68. 0040106Dint3
  69. 0040106Eint3
  70. 0040106Fint3
  71. 00401070int3
  72. 00401071int3
  73. 00401072int3
  74. 00401073int3
  75. 00401074int3
  76. 00401075int3
  77. 00401076int3
  78. 00401077int3
  79. 00401078int3
  80. 00401079int3
  81. 0040107Aint3
  82. 0040107Bint3
  83. 0040107Cint3
  84. 0040107Dint3
  85. 0040107Eint3
  86. 0040107Fint3
  87. ---c:\project\heap1\heap1.cpp--------------------------------------------------------------------------------------------------------------------------------------
  88. 11:intmain(intargc,char*argv[])
  89. 12:{
  90. 00401080pushebp
  91. 00401081movebp,esp//保存堆栈基址
  92. 00401083subesp,48h//腾出局部变量空间
  93. 00401086pushebx
  94. 00401087pushesi
  95. 00401088pushedi//保存3个寄存器
  96. 00401089leaedi,[ebp-48h]
  97. 0040108Cmovecx,12h
  98. 00401091moveax,0CCCCCCCCh//初始化局部变量空间
  99. 00401096repstosdwordptr[edi]
  100. 13:charsz_In[]="1234567";
  101. 00401098moveax,[string"1234567"(00422020)]
  102. 0040109Dmovdwordptr[ebp-8],eax
  103. 004010A0movecx,dwordptr[string"1234567"+4(00422024)]
  104. 004010A6movdwordptr[ebp-4],ecx//将字符串通过寄存器将字符拷贝到分配的空间中。
  105. 14:fun(sz_In,888);
  106. 004010A9push378h
  107. 004010AEleaedx,[ebp-8]
  108. 004010B1pushedx//从右至左将参数压入堆栈中,数字直接压入数值,字符串则压入字符串指针
  109. 004010B2call@ILT+0(fun)(00401005)
  110. 004010B7addesp,8//恢复堆栈
  111. 15:return0;
  112. 004010BAxoreax,eax//返回值在EAX中
  113. 16:}
  114. 004010BCpopedi
  115. 004010BDpopesi
  116. 004010BEpopebx//恢复3个寄存器
  117. 004010BFaddesp,48h//清除局部变量空间
  118. 004010C2cmpebp,esp
  119. 004010C4call__chkesp(00401250)//检测堆栈指针与堆栈基址
  120. 004010C9movesp,ebp//恢复调用函数的栈顶
  121. 004010CBpopebp//恢复调用函数的堆栈基址
  122. 004010CCret//函数返回
  123. ---Nosourcefile--------------------------------------------------------------------------------------------------------------------------------------------------
  124. 004010CDint3
  125. 004010CEint3

关于这个程序的堆栈使用情况也做了一下分析,如图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值