运行时结构内容参考自:编译系统透视图解编译原理 C程序开发并编译完成后,是要载入内存(主存或内存条)才能运行,变量名、函数名都会对应内存中的一块区域。
其实内存中运行着很多程序,C程序只占用其中一小部分空间,这部分空间又可以细分为以下的区域: 1. 程序代码区——存放计算机可识别的函数体的二进制代码 2. 静态数据区——也称全局数据区,包含的数据类型比较多,如全局变量、静态变量、常量数据。其中:
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
常量数据(一般常量、字符串常量)存放在另一个区域。
注意:静态数据区的内存在程序结束后由操作系统释放。 3. 堆区——一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。malloc()、calloc()、free() 等函数操作的就是这块内存。 4. 栈区——由系统自动分配释放,存放函数的参数值、局部变量的值等。 C语言内存模型图如下: 为了更好的理解内存模型,请大家看下面一段代码:
#include<stdio.h> #include<stdlib.h> #include<string.h> int a = 0; // 全局初始化区 char *p1; // 全局未初始化区 int main() { int b; // 栈区 char s[] = "abc"; // 栈区 char *p2; // 栈区 char *p3 = "123456"; // "123456\0" 在常量区,p3在栈上,体会与 char s[]="abc"; 的不同 static int c = 0; // 全局初始化区 p1 = (char *)malloc(10), // 堆区 p2 = (char *)malloc(20); // 堆区 // "123456\0 "放在常量区,但编译器可能会将它与p3所指向的"123456"优化成一个地方 strcpy(p1, "123456"); }在解决编程中遇到的实际问题时,光了解内存模型是不够的,更需要透彻了解程序在内存中的运行时结构。 C语言运行的核心就是函数的执行和调用,这构成了C程序运行时结构的基础框架。而这一过称主要是在程序指令的驱使以及数据压栈、清栈的支持下实现的。下面就通过一段简单的C程序介绍它的运行时结构。
int fun(int a,int b); int m=10; int main() { int i=1; int j=2; m=fun(i,j); return 0; } int fun(int a,int b) { int c=0; c=a+b; return c; }此时内存给此程序分配了段内存,其中包含三个区域:代码区、静态数据区和动态数据区(栈区)。如下图: 代码区装载了程序对应的机器指令,main函数和fun函数的机器指令就装在这里。全局变量m的数值装载在静态数据区。其实程序开始执行前,动态数据区中是没有数据的。程序开始执行后,在指令的驱动下,动态数据区才会产生数据,压栈和清栈的动作就在这里完成。 程序执行的本质就是代码区的指令不断执行,在计算机的管理下驱使动态数据区和静态数据区的发生数据变化。CPU有三个存地址的寄存器:eip、esp和ebp。 eip永远指向代码区将要执行的下一条召指令,它的管理方式有两种:”顺序执行“和跳转。”顺序执行“指程序执行完一条指令后自动指向下一条执行,跳转指执行完一条跳转指令后跳转到指定的位置。 ebp和esp用来管理栈空间,其中ebp指向栈底,esp指向栈顶,在代码区中,函数调用、返回和执行都伴随着不断的压栈和清栈,栈中数据存储和释放遵循后进先出的原则。 程序运行前,eip指向main函数的第一条指令,栈中没有数据,ebp和esp指向程序加载时内核设置的位置。 程序开始执行后,eip自动指向下一条指令。伴随第一条指令的执行,ebp的地址值被保存在栈中,以保证程序执行完后ebp能返回现在的位置,复原现在的栈。ebp地址值压栈后esp自动向栈顶方向移动,并将永远指向栈顶。随着程序继续执行,main函数的栈开始建立,由于ebp指向的地址值已经压入栈中保存起来了,所以它空闲下来,可以用来看管main函数的栈底,此时ebp和esp是重叠的。 程序继续执行,eip逐条指向下面的指令,逐步执行局部变量i和j的初始化,初始值1和2 被压入栈中,esp也自动向栈顶方向移动。 局部变量 i和j都是供main函数之间使用的,而调用fun函数时压栈的数据一半在fun函数中,一半在主调函数main中。参数入栈的原则是从右到左,即b先入栈,数值是main函数中局部变量j的数值2,然后a才被压入栈中,数值则是局部变量i的设置1。 接下来入栈的是fun函数的返回值,fun函数返回时将传递给m。 跳转到fun函数执行时,分两部分动作,一部分是把fun中执行后的返回地址压入栈中,以便fun函数执行完毕后能够返回到main函数中继续执行,这一步骤也称作恢复现场。另一部分就是跳转到被调用的fun函数的第一条指令去执行。 fun函数执行后的第一件事也是保存ebp指向的地址值,即main函数的栈底,以确保返回主函数时可以恢复main函数栈底位置。 现在就开始构建fun函数的栈,其方式与main函数栈一样。 fun函数执行结束并将局部变量c当做返回值返回。 fun函数执行结束后就要恢复现场,现场主要包括main函数的栈的恢复(栈顶和栈底),还有找到fun函数执行后的返回地址,跳回到那里再继续执行。ebp的恢复就是直接将一开始储存的ebp地址值赋值给现在的ebp,使其指向main函数的栈底。 ebp地址值出栈后,esp自动退栈指向fun函数执行后的返回地址,然后执行ret返回指令,把地址值传给eip,使之指向fun函数返回后的返回地址。 现场恢复以后,把fun函数的返回值传递给m。 现在fun函数调用时的传参和返回值没有存在的必要了,全部清栈。 main函数执行结束后也会以相同的方式进行清栈。 以上就是一个C程序完整的运行时结构,其中值得我们注意的是“栈溢出”的问题。其实只要我们做到对自己所编写程序的运行状态心中有数就完全不用担心这个问题。
