当前位置:首页 > 技术学院 > 技术前线

很多开发者天天写函数、调用函数,却从来没有真正看过函数在内存里的完整形态。你写的一行普通函数,编译之后根本不是一个完全独立的整体,它的代码、运行时数据、调用上下文,散落在内存里完全不同的区域,从你调用它的那一刻开始,到它执行结束返回,整个生命周期里的每一步内存变化都有严格的规则。彻底理清函数运行时的内存布局,你就能彻底看懂栈溢出、野指针篡改返回地址这类底层问题的根因,写出更安全、更可控的底层代码。

一、编译期的静态布局:函数代码在程序镜像里的初始形态

在程序还没有启动运行、只是躺在磁盘上的可执行文件里,函数的所有机器指令代码,会被编译器集中存放在一个叫做代码段的专属区域里。这里面存放的是函数被编译之后生成的完整机器指令序列,是纯只读的属性,运行时绝对不允许被任何代码修改,避免恶意篡改指令逻辑的安全问题。

这个阶段的函数,只是一段静态的二进制指令集合,完全没有任何运行时的动态数据。编译器会给这个函数的入口地址打上唯一的标记,程序加载进内存之后,这段指令序列会被直接映射到虚拟地址空间的代码段区域,函数的入口地址就是这段指令序列的起始虚拟地址。我们平时用到的函数指针,本质上存储的就是这个入口地址,拿到这个地址就可以直接跳转到这段指令的起始位置开始执行。

除了可执行的指令,函数里定义的所有字符串常量、const修饰的只读常量,也不会存放在函数的动态运行区域里,它们会被编译器统一放到只读数据段,和代码段一样拥有只读属性,所有调用这个函数的执行流,共享同一份常量副本,不会为每一次函数调用单独拷贝一份常量数据,最大程度节省内存空间。这个静态阶段的函数,没有任何动态的执行上下文,只是一段等待被CPU执行的纯指令序列。

二、调用瞬间的栈帧生成:动态运行数据的完整构建

当程序执行到调用这个函数的指令时,CPU首先会自动把当前函数的返回地址,也就是调用完成之后要回到原来代码位置继续执行的下一条指令的地址,自动压入当前线程的栈空间里。紧接着CPU跳转到目标函数的入口地址,开始执行函数的第一条指令,函数立刻就会在栈顶构建属于自己的专属栈帧,所有这个函数运行时需要的动态数据,全部都在这个栈帧里分配。

栈帧的最底部,首先存放的是上一个函数的栈帧基地址,也就是栈帧回溯用的栈帧指针。通过这个指针,当前函数就能顺着地址找到调用自己的上一层函数的栈帧,一层一层回溯上去,就能得到完整的函数调用栈,这就是调试器里打印函数调用栈的底层实现原理。紧接着栈帧里会依次存放所有传入函数的参数,根据不同平台的调用约定,一部分参数会通过寄存器直接传递,剩下的参数才会压入栈帧里。

接下来栈帧里的空间,就会用来存放函数内部定义的所有非静态局部变量。你在函数里声明的普通整型、字符数组、自定义结构体这些局部变量,全部都会直接在这个栈帧的空间里分配,不需要向堆申请任何动态内存。这些局部变量的内存地址,全部都在当前栈帧的范围之内,函数的栈指针会自动移动,预留出所有局部变量需要的总空间,整个分配过程只是调整栈指针的位置,没有任何额外的开销,几纳秒就可以完成。

整个栈帧的大小在编译阶段就已经被编译器完全计算出来了,运行时不需要做任何动态计算,函数执行的时候直接移动栈指针就能完成所有动态数据的分配,这也是函数调用速度极快的核心原因。

三、执行结束后的栈帧销毁:内存的自动回收与生命周期边界

当函数执行完所有逻辑,准备执行返回指令的时候,它的栈帧并不会立刻被特殊的机制清空擦除。函数首先把栈指针移动回栈帧的起始位置,直接把整个栈帧占用的栈空间标记为空闲,把上一层函数的栈帧基地址恢复到栈帧指针寄存器里,然后从栈里弹出之前保存的返回地址,CPU跳转到这个返回地址,回到调用者的函数上下文里继续执行。

到这一步,这个函数的栈帧就已经逻辑上被销毁了,它之前占用的栈空间就变成了完全空闲的状态,后续下一个被调用的新函数,构建自己的栈帧的时候,就会直接复用这块刚刚被释放的栈空间,把里面原有的旧数据直接覆盖掉。这就是为什么函数返回之后,你不能返回一个指向局部变量的指针:函数栈帧销毁之后,这块内存很快就会被其他函数覆盖,指针指向的内容很快就会变成完全随机的垃圾数据,后续访问就会触发未定义行为。

如果函数内部定义了static修饰的静态局部变量,这个变量根本不会存放在栈帧里,它会被编译器放到进程的全局静态数据段,整个程序的整个运行周期里,这块内存只会被初始化一次,哪怕函数被调用成千上万次,所有调用实例共享同一个静态变量的内存副本,函数执行结束之后这个变量的值也不会消失,下一次调用函数的时候,还能读到上一次调用结束时留下的数值。

函数的整个内存生命周期,从静态的代码段里的指令序列,到调用时动态生成栈帧,再到执行结束后栈帧被自动回收复用,整个过程完全由CPU和操作系统自动管理,不需要开发者手动分配和释放任何资源。理解这套完整的内存形态,你就能彻底避开局部变量野指针、栈溢出这类极其隐蔽的底层Bug,写出更稳定的C语言代码。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭