《图解编译原理》读书笔记
1. 第一章 运行时结构及编译过程
1.1. 一个简单的C程序
代码如下:
1 | int fun(int a, int b); |
1.2. 程序运行之前
执行程序之前,我们需要对程序进行编译。
编译分为以下几个步骤:
| 1. 预处理: 宏定义展开、头文件展开、条件编译、在这里并不会检查语法。 |
|---|
| 2. 编译: 检查语法,将预处理后的文件编译生成汇编文件 |
| 3. 汇编: 将汇编文件生成目标文件(二进制文件) |
| 4. 链接: 将目标文件链接为可执行文件 |
编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:
通过上图可以得知,在程序没有运行前,也就是说程序没有加载到内存前,可执行程序的内部已经分好了3段信息,分别为代码区(text),数据区(data)和未初始化数据区(bss) 3个部分。
程序开始执行前,动态数据区没有数据。只有程序开始执行后,在指令的驱动下,这一区域才会产生数据。压栈和清栈多的工作就是在这一区域完成的。
1.3. 程序的初始化
初始情景是这样的,eip 指向 main 函数的第一条指令,此时程序还没有运行,栈空间里还没有数据,ebp 和 esp 指向的位置是程序加载时内核设置的。
1.4. 程序的执行
- 程序开始执行 main 函数第一条指令,eip 自动指向下一条指令。第一条指令的执行,致使 ebp 的地址值被保存在栈中。
- 随着 ebp 地址值的 压栈,esp 自动向栈顶方向移动,它将永远指向栈顶。
程序继续执行,开始构建 main 函数自己的栈,ebp 原来指向的地址值已经被保存了,它被腾出来了,用来看管 main 函数的栈底,此时它和 esp 是重叠的
程序继续执行,eip 指向下一条指令,此次执行的是局部变量 i 的初始化,初始值 4 被存储在栈中,esp 自动向栈顶方向移动。
继续执行下一条指令,局部变量 j 的初始值 5 也被压栈
接下来调用 fun 函数时压栈的数据虽然也保存在 main 函数的栈中, 但它们都是供 fun 函数用的。 可以说 fun 函数的数据, 一半在 fun 函数中, 一半在主调函数中, 下面来 看函数调用时留在 main 函数中的那一半数据。
- 先执行传参的指令,此时参数入栈的顺序和代码中传参的书写顺序正好相反,参数 b 先入栈,数值是 main 函数中局部变量 j 的数值 5。
- 程序继续执行,参数 a 被压入栈中,数值是局部变量 i 的数值 4
- 程序继续执行, 此次压入的是 fun 函数返回值, 将来 fun 函数返回之后, 这里的值会传递给 m
- 还剩最后一步,跳转到 fun 函数去执行。
- 一部分是把 fun 函数执行后的返回地址压入栈中,以便 fun 函数执行完毕后能返回到 main 函数中继续执行
- 另一部分就是跳转到被调用的函数的第一条指令去执行
- fun 函数开始执行,第一件事就是保存 ebp 指向的地址值,此时 ebp 指向的是 main 函数的栈底,保存的目 的是在返回时恢复 main 函数栈底的位置,这和前面 main 函数刚开始执行时第一步就保存 ebp 的地址值的目的 是一样的
- 执行几个运算指令
- 恢复现场以后,把 fun 函数返回值传递给 m
- 该处理 fun 函数调用时的传参和返回值设置了,这两者已经没有存在的必要了,全部清栈
- 剩下就是 main 函数的内容了,main 函数执行完毕以后,栈也全部清掉。清栈的方式与 fun 函数执行完后采用的清栈方式一致
1.5. 运行之后
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,操作系统把物理硬盘程序load(加载)到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
| 代码区 | 描述 |
|---|---|
| 代码区(TEXT) | 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。 |
| 未初始化数据区(BSS) | 加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。 |
| 静态数据区(DATA) | 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。 |
| 栈区(stack) | 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。 |
| 堆区(heap) | 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。 |
常见的类型以及存储位置:
| 类型 | 作用域 | 生命周期 | 存储位置 |
|---|---|---|---|
| auto变量 | {}内 | 当前函数 | 栈区 |
| static局部变量 | {}内 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| extern变量 | 整个程序 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| static全局变量 | 当前文件 | 整个程序运行期 | 初始化在data段,未初始化在BSS段 |
| extern函数 | 整个程序 | 整个程序运行期 | 代码区 |
| static函数 | 当前文件 | 整个程序运行期 | 代码区 |
| register变量 | {}内 | 当前函数 | 运行时存储在CPU寄存器 |
| 字符串常量 | 当前文件 | 整个程序运行期 | data段 |
| const全局变量 | 当前文件 | 整个程序运行期 | 初始化放在data段,未初始化在BSS段 |
| const局部变量 | {}内 | 当前函数 | 栈区 |
1.6. CPU中的三个寄存器
- eip:eip永远指向代码区将要执行的下一条指令。
- 顺序执行:程序执行完一条指令后自动指向下一条执行
- 跳转:执行完一条跳转指令后跳转到指定位置
- ebp:管控栈空间,指向栈底
- esp:管控栈空间,指向栈顶
1.7. 更为复杂的C语言运行结构
代码如下:
1 |
|
在C语言中,栈的方向是从高地址向低地址延伸,而数组中数据在栈中的存储方向与此正好相反。
字符串拷贝等数组操作是不对数据长度做审核的,如果实际的数据长度超过了栈中预留的空间,就会将栈中其他数据覆盖,这种现象被称为“栈溢出”。栈溢出可能导致一个不可预期的错误,也可能导致一个精心策划的执行流程发生改变。
1.8. 编译过程概述
1.8.1. 词法分析
词法分析的作用是从连续的字符中识别出标识符、关键字、数字、运算符并存储为符号(token)流。
1.8.2. 语法分析
语法分析的作用是从词法分析识别出的符号流中识别出符合C语言语法的语句。
1.8.3. 语法书->中间代码->目标代码
中间代码的设计思想: 计算机存在多种CPU硬件平台,要考虑到程序在不同CPU之间的移植性。先转换成通用的、抽象的CPU指令。
语法树是二维结构。
中间代码是准一维结构。
选定具体的 CPU、操作系统后, 中间代码就可以转换为目标代码——汇编代码
最后链接器把一个或多个目标文件(库文件本质上也是目标文件)链接成符合选定操作系统指定格式的可 执行文件。
2. 第二章 词法分析
2.1. 总体结构图