![编译系统透视:图解编译原理](https://wfqqreader-1252317822.image.myqcloud.com/cover/487/844487/b_844487.jpg)
1.1 一个简单C程序的运行时结构
解决编程过程中的实际问题,需要透彻了解程序在内存中的运行时结构,而透彻的程度自然成为衡量计算机语言学习水平的重要标准,也成为衡量软件项目开发水平的重要标准。
C程序运行的核心是函数的执行和调用,它构成了整个C程序运行时结构的基础框架。这一运行过程主要是在程序指令的驱动以及数据压栈、清栈的支持下实现的。为了介绍这一过程,我们设计了一个简单C程序,如下所示:
int fun(int a,int b); int m=10; int main() { inti=4; int j=5; m = fun(i,j); return 0; } int fun(int a,int b) { int c=0; c=a+b; return c; }
程序很简单,却凸现了函数调用和执行的最基本情况。我们把此情景展现在内存中,共有三个区域,分别是代码区、静态数据区和动态数据区。情景如图1-1所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0002.jpg?sign=1738916001-CWKDKVfULcxxYukm2e6EfjSGolkNXNgJ-0-37a05d66caa35b37098728a8df87473c)
图1-1 内存区域特性的总体介绍
代码区装载了这个程序所对应的机器指令,main函数和fun函数的机器指令装载位置如图1-2所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0003.jpg?sign=1738916001-4rjggfeO9siZ0vo3NsZE8Z04cliztWgD-0-b7e64be97d1edb7c73245e0be91c01b7)
图1-2 main函数和fun函数在代码区的位置
全局变量m的数值装载在静态数据区中,情景如图1-3所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0004.jpg?sign=1738916001-qphqeqXnDOwl0FqY3I5MtwDJHBpVemRh-0-2bcad83816e5688a3aca3725a1b49e65)
图1-3 全局变量m在静态数据区的位置
程序开始执行前,动态数据区中没有数据,情景如图1-4所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0005.jpg?sign=1738916001-gxzfo4USIeu5RSXDhnOP4O2KngMt3QZq-0-3e946ad687dd90e8594fa9e866f8e281)
图1-4 动态数据区没有数据
这是因为,只有程序开始执行后,在指令的驱动下,这一区域才会产生数据,压栈和清栈的工作就是在这一区域完成的,情景如图1-5所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0006.jpg?sign=1738916001-8GzMd0sA5BR3ZgxOvTESBGTwyJPAwOX9-0-0cd8082c2b816e81a56dcb688ccdf141)
图1-5 建栈和清栈的情景
程序执行的本质就是代码区的指令不断执行,驱使动态数据区和静态数据区产生数据变化。这一过程需要计算机的管控。下面我们着重介绍对代码区和动态数据区的管控。CPU中有三个寄存器,分别是eip、ebp和esp,情景如图1-6所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0007.jpg?sign=1738916001-6kc7JrZyUtrBCUlCKaLosRvIMLO4UmlP-0-d2de2d7e82aa27f2f7be77b5d0f574d1)
图1-6 对代码区和动态数据区的管控
其中eip永远指向代码区将要执行的下一条指令,它的管控方式有两种,一种是“顺序执行”,即程序执行完一条指令后自动指向下一条执行;另一种是跳转,也就是执行完一条跳转指令后跳转到指定的位置。
ebp和esp用来管控栈空间,ebp指向栈底,esp指向栈顶,在代码区中,函数调用、返回和执行伴随着不断压栈和清栈,栈中数据存储和释放的原则是后进先出。
内存的划分及程序执行的总体情况先介绍到这里。下面详细介绍案例程序的运行时结构。初始情景是这样的,eip指向main函数的第一条指令,此时程序还没有运行,栈空间里还没有数据,ebp和esp指向的位置是程序加载时内核设置的(详情请看《Linux内核设计的艺术》一书),情景如图1-7所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0008.jpg?sign=1738916001-iwNHun91DnavOe3pReR3rkuW6itqoSli-0-37a5aa36dcd757b3a5498a3f075f0e3b)
图1-7 程序加载时esp和ebp的起始位置
程序开始执行main函数第一条指令,eip自动指向下一条指令。第一条指令的执行,致使ebp的地址值被保存在栈中,保存的目的是本程序执行完毕后,ebp还能返回现在的位置,复原现在的栈。随着ebp地址值的压栈,esp自动向栈顶方向移动,它将永远指向栈顶,情景如图1-8所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0009.jpg?sign=1738916001-25RN6Cp9ZbkzsT592IaODxjAAKm1BDU8-0-a77d6ec3b77e7a7e464df0fa8889d8b5)
图1-8 保存ebp
程序继续执行,开始构建main函数自己的栈,ebp原来指向的地址值已经被保存了,它被腾出来了,用来看管main函数的栈底,此时它和esp是重叠的,情景如图1-9所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0010.jpg?sign=1738916001-u68oGCxx82McPnm8B0KnI1C09bQRa2nO-0-fdb9194a834818e0aad8dd7041ef3b30)
图1-9 准备构建main函数的栈
程序继续执行,eip指向下一条指令,此次执行的是局部变量i的初始化,初始值4被存储在栈中,esp自动向栈顶方向移动,情景如图1-10所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0011.jpg?sign=1738916001-AxvOLT6tYNTuQLZNWckpGl7Er7jBIzHh-0-03d5060d0ab03fae355376bca5ea52b3)
图1-10 局部变量i压栈并初始化
继续执行下一条指令,局部变量j的初始值5也被压栈,情景如图1-11所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0012.jpg?sign=1738916001-WFR1R7EO25FwV7brqUtwPmU66uE6x19T-0-3e46e1ba42bece3e56765066abda3b03)
图1-11 局部变量j压栈并初始化
这两个局部数据都是供main函数自己用的,接下来调用fun函数时压栈的数据虽然也保存在main函数的栈中,但它们都是供fun函数用的。可以说fun函数的数据,一半在fun函数中,一半在主调函数中,下面来看函数调用时留在main函数中的那一半数据。
先执行传参的指令,此时参数入栈的顺序和代码中传参的书写顺序正好相反,参数b先入栈,数值是main函数中局部变量j的数值5,情景如图1-12所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0013.jpg?sign=1738916001-RCs3h7ShssSSD6STbd1A8rxHHxaa3aDb-0-bf4a35c0492d19cb628e02afcebaefd0)
图1-12 j的数值作为参数被压栈
程序继续执行,参数a被压入栈中,数值是局部变量i的数值4,情景如图1-13所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0014.jpg?sign=1738916001-achaZ7oBytqlrSDVgOO9cNkKDkk65DLN-0-6f487190c434e5bbeb6dbb00beca2503)
图1-13 i的数值作为参数被压栈
程序继续执行,此次压入的是fun函数返回值,将来fun函数返回之后,这里的值会传递给m,情景如图1-14所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0015.jpg?sign=1738916001-Lmsz9wwzyQ7E1mDDcGWMHBA9Pms9qAO7-0-e7b41b3c0b8d2f5e9c7b7b025b198df4)
图1-14 设定fun函数返回值的位置
还剩最后一步,跳转到fun函数去执行,这一步分为两部分动作,一部分是把fun函数执行后的返回地址压入栈中,以便fun函数执行完毕后能返回到main函数中继续执行,情景如图1-15所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0016.jpg?sign=1738916001-yVTQ05QJRHQqGl3BtTXILQKVVOxJl0Ph-0-54cebe8d7274888fb1ddc5ccb0c0afee)
图1-15 fun函数执行后的返回地址被压栈
到这里,函数调用的数据准备工作就完成了。另一部分就是跳转到被调用的函数的第一条指令去执行,情景如图1-16所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0017.jpg?sign=1738916001-umtItIM4IJHQWUlCk5LGavUGtHdEevlT-0-7e6f1a8a46c9dc935f0a697ff561b9af)
图1-16 跳转到fun函数去执行
fun函数开始执行,第一件事就是保存ebp指向的地址值,此时ebp指向的是main函数的栈底,保存的目的是在返回时恢复main函数栈底的位置,这和前面main函数刚开始执行时第一步就保存ebp的地址值的目的是一样的,情景如图1-17所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0018.jpg?sign=1738916001-Rl5dbXGlNUTI3iMEqftAhd4maWKEXDAS-0-c95e88b9c2c38fafe96d4abea81acbf6)
图1-17 fun函数开始执行后先保存main函数栈底地址值
再往后就要构建fun函数的栈了。程序继续执行,仍然使用腾出来的ebp看管栈底,ebp和esp此时指向相同的位置,情景如图1-18所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0019.jpg?sign=1738916001-bLvmOiPnt0aviHJ0YlazT97BdNZ6B9tl-0-e9688e38e68dddf7f18d39a33e02732a)
图1-18 准备建立fun函数的栈空间
程序继续执行,局部变量c开始初始化,入栈,数值为0,这个c就是fun函数的数据,存在于fun函数的栈中,情景如图1-19所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0020.jpg?sign=1738916001-L0KBHSWj0Z3lCqNb9ifO03umCxzwX7US-0-ee2f4a9f7a6b05052028d1280e82c3fb)
图1-19 局部变量c被压栈
此时回顾fun函数的数据,可以发现一半在main函数中,情景如图1-20所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0021.jpg?sign=1738916001-OvZUPAdbfCvBxBSxPt6ok6vTFO7oMZQG-0-22290c008a3e6d7753432f169ad11978)
图1-20 fun函数的数据一半在main函数中
另一半在fun函数中,情景如图1-21所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0022.jpg?sign=1738916001-Yf4YRdMfPmXdBO2ItWtZWiQkTa5kvHPI-0-c09337bd4a72ed773e7d4e49ff491598)
图1-21 fun函数的数据的另一半在fun函数中
接下来会执行几个运算指令,展现对这些数据的应用。以ebp为基点,很容易找到main函数栈和fun函数栈中数据的位置,情景如图1-22所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0023.jpg?sign=1738916001-S9PR5adc546tN1IpBloBg5iBgYaMvrVv-0-8178e0fae5284c5593b4c0ab119be4b8)
图1-22 将加法运算结果赋值给局部变量c
程序继续执行,fun函数中局部变量c的数据当成返回值返回,情景如图1-23所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0024.jpg?sign=1738916001-14bpmkZtDoXK8HT1dXdSeiu9nvU5eaxY-0-e3584b621a64ca9708cc61b9b1b1f8d2)
图1-23 c的数值返回
现在fun函数已经执行完毕,要恢复main函数调用fun函数的现场,这一现场包括两个部分,一部分是main函数的栈要恢复,包括栈顶和栈底,另一部分是要找到fun函数执行后的返回地址,然后再跳转到那里继续执行。
我们来看ebp的恢复。前面存储了ebp的地址值,现在可以把存储的地址值赋值给ebp,使之指向main函数的栈底,情景如图1-24所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0025.jpg?sign=1738916001-XsaHxvgOmj2FLp4qrMIoE4gTbSzjDWW5-0-d104831a21335d90a861da77c06d57db)
图1-24 恢复main函数栈底地址值
ebp地址值出栈后,esp自动退栈,指向fun函数执行后的返回地址,之后执行ret指令,即返回指令,把地址值传给eip,使之指向fun函数执行后的返回地址,情景如图1-25所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0026.jpg?sign=1738916001-yWcw3YSdgrgT2CswKCxzVy8wJt0T2VT3-0-03dac0115c4f124a5ee717525dea1e22)
图1-25 返回到main函数中执行
恢复现场以后,把fun函数返回值传递给m,情景如图1-26所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0027.jpg?sign=1738916001-u0Z1AJuF8lK9Wg6yN4uFadhPOAMCccns-0-e16a5870fa19e7f720265c32f31fb909)
图1-26 将返回值赋值给m
该处理fun函数调用时的传参和返回值设置了,这两者已经没有存在的必要了,全部清栈,情景如图1-27所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0028.jpg?sign=1738916001-203ea10hH3ONZJZZmofR0gKIIqlfbXvj-0-4b325f8ff9e6c7d4952446d29f1620d4)
图1-27 参数和返回值清栈
剩下就是main函数的内容了,main函数执行完毕以后,栈也全部清掉。清栈的方式与fun函数执行完后采用的清栈方式一致,情景如图1-28所示。
![](https://epubservercos.yuewen.com/CCC398/5922738704546701/epubprivate/OEBPS/Images/figure_0029.jpg?sign=1738916001-p791HZ9GPqFzObQMTRNvAxTVf1qlBmTA-0-ba2fcdf71fea68fa6a2934ae65305d91)
图1-28 main函数清栈
操作系统已经为整个程序执行完的善后工作做了准备,详情请看《Linux内核设计的艺术》一书。