四川省网站建设_网站建设公司_移动端适配_seo优化
2026/1/21 18:14:16 网站建设 项目流程

一、问题引入

在学完c语言后,你可能会有部分疑惑?
比如:
1.局部变量是怎么创建的?
2.为什么如果不初始化局部变量就会使随机值?
3.函数是怎么传参的?传参的顺序是怎么样的?
4.形参和实参的关系是怎么样的?
5.函数调用是怎么做的?
6.调用完后是怎么返回的?

看完本文相信你能对这些问题有更深入的理解

二、

1.基础知识储备:

我们知道main()函数是程序的入口,其他函数都是通过main()函数调用的。但是问题来了:main()又是由谁条用的呢?其实main()函数也是由其他函数调用的,不是由我们自已写的函数,而是编译器自带的运行时库函数。比如在VS2022下,main()先是由mainCRTStartup调用_tmainCRTStartup,再由_tmainCRTStartup调用main()函数的。
所以在执行main()函数之前就已经存在_tmainCRTStartup的栈帧

屏幕截图 2026-01-21 141843

2.实战操练

注:环境为debug x86
接下来我用简单的代码带你了解函数栈帧创建的过程:

点击查看代码
int Add(int x, int y) {int z = 0;z = x + y;return z;
}int main() {int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%d\n", c);
}

1.首先我们先转到汇编层面:

F10->右击鼠标->转到反汇编

2.我们可以看到以下的陌生代码:

屏幕截图 2026-01-21 141415
别着急,我们一步一步拆解:

  • push指令:压栈,即将ebp压入栈顶,同时esp往上挪动(减去地址)

屏幕截图 2026-01-21 142424

  • mov指令:寄存器之间的数据传送,即将esp的值赋给ebp

屏幕截图 2026-01-21 142638

  • sub指令:用于执行减法运算,即将esp减去部分值(具体的数由编译器决定)

屏幕截图 2026-01-21 142943
这时main()函数的栈帧已经初具雏形

  • 三个push:将ebx、esi、edi压入栈中,若想具体了解用处,请看寄存器的作用(来自网络)

屏幕截图 2026-01-21 144003

  • lea指令:用于加载有效地址,即它将内存地址加载到寄存器中。这里是将ebp-24h(局部变量开始的地址)的地址加载到edi中
  • mov指令:将9赋给ecx寄存器,存放重复指令的次数
  • mov指令:将0cccccccch赋给eax寄存器
  • rep stos指令:REP是一个前缀指令,表示重复执行其后的指令,而STOS指令的作用是将寄存器EAX中的值存储到ES:EDI指向的内存地址中,dword表示双字,一个双字代表4个字节。即重复ecx(9)次(初始化了32个字节),将eax(0cccccccch)的值存储到edi(ebp-24h)中,简单的来说就是将[ebp-24h,ebp-4h]初始化成0cccccccch。(解释了问题1随机值的来源)

屏幕截图 2026-01-21 160221

  • 那么为什么不是直接从ebp开始呢?还有为什么只初始化36个字节呢?而不是初始化E4h(228)个字节呢?
    • 为什么不从ebp开始:因为ebp此时存着的是原来栈帧的ebp,如果被初始化,原有栈帧值被覆盖,就回不到原有栈帧了
    • 为什么只初始化36个字节:因为编译器会自动识别代码长度,从而决定初始化数量,后续的字节是用来:栈对齐:x86 CPU 要求栈地址是 4/8 字节对齐,编译器会多分配空间保证对齐,提升执行效率安全缓冲区:Debug 模式下的 “栈保护” 机制,预留空间防止轻微的栈溢出直接崩溃

总结一下:

屏幕截图 2026-01-21 163455

  • mov指令:将0BBC008h(成员变量的地址)赋给ecx
  • call 00BB132F:调用地址 0x00BB132F 处的成员函数(配合 Visual Studio 的 “仅我的代码(Just My Code)” 调试功能,用来判断当前执行的代码是否属于你自己写的代码,还是系统或第三方库的代码
  • nop:无实际功能,仅用于让代码地址对齐,提升 CPU 执行效率

3.创建变量:

屏幕截图 2026-01-21 162403

屏幕截图 2026-01-21 163625
解释了问题1

4.传入形参:

屏幕截图 2026-01-21 163718

  • mov:将ebp-14h地址的值(b=20)赋给eax寄存器
  • push:将eax压入栈顶
  • mov:将ebp-8地址的值(a=10)赋给ecx寄存器
  • push:将ecx压入栈顶(解释了问题3(传参是从右往做传的))

屏幕截图 2026-01-21 164601

5.进入Add函数:

屏幕截图 2026-01-21 164708
步骤跟main函数一致,都是在创建栈帧然后初始化

屏幕截图 2026-01-21 165429

点击查看代码
int z = 0;
z = x + y;
  • 执行z = 0语句并调用add指令将eax寄存器中的值与ebp+0Ch中的值相加,然后重新将eax寄存器中的值返回z的地址
点击查看代码
return z;
  • 将z的值放入eax寄存器中

屏幕截图 2026-01-21 170324

  • pop指令:从栈顶部弹出数据到寄存器或内存单元,即将edi、esi、ebx弹出栈顶
  • add指令:将esp将回到ebp
  • cmp指令:检查esp和ebp之间的差值是否正确
  • call指令:如果这一步发现异常,call 00BB1253 会立刻终止程序并告诉你 “栈被破坏了”
  • mov指令:双重保险,哪怕栈已经乱了,也能强行把 esp 拉回正确位置
  • pop指令:弹出ebp,并将此时ebp1的值赋给ebp
    经过这一步,ebp又回到了开头的位置

屏幕截图 2026-01-21 171556

  • ret指令:ret 是最终跳转,能将栈顶的数据弹出,并赋值给指令指针寄存器(IP),从而使程序继续执行调用子程序之前的指令。

屏幕截图 2026-01-21 172538

  • add指令:清理形参a,b(解决了问题4(形参只是实参的拷贝,改变形参不影响实参))
  • mov指令:将之前存在eax寄存器中的值赋给c

最后打印……

三、总结

相信你看完本篇已经能回答前五个问题了
第6个问题:其实cpu在执行call指令的时候,会自动的将cpu下一条指令压入栈中,ret指令弹出栈顶数据弹出的就是call函数的下一条指令,并且赋值给了指令指针寄存器(IP),这样cpu就能按照原来的逻辑继续运行了

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询