1. 第一章 运行时结构及编译过程

1.1. 一个简单的C程序

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int fun(int a, int b);
int m = 10;
int main()
{
int i = 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.2. 程序运行之前

执行程序之前,我们需要对程序进行编译。

编译分为以下几个步骤:

1. 预处理: 宏定义展开、头文件展开、条件编译、在这里并不会检查语法。
2. 编译: 检查语法,将预处理后的文件编译生成汇编文件
3. 汇编: 将汇编文件生成目标文件(二进制文件)
4. 链接: 将目标文件链接为可执行文件

编译完成生成可执行文件之后,我们通过在linux下size命令可以查看一个可执行二进制文件基本情况:

image-20201103194803509

通过上图可以得知,在程序没有运行前,也就是说程序没有加载到内存前,可执行程序的内部已经分好了3段信息,分别为代码区(text),数据区(data)和未初始化数据区(bss) 3个部分。

程序开始执行前,动态数据区没有数据。只有程序开始执行后,在指令的驱动下,这一区域才会产生数据。压栈和清栈多的工作就是在这一区域完成的。

image-20201103204628715

1.3. 程序的初始化

初始情景是这样的,eip 指向 main 函数的第一条指令,此时程序还没有运行,栈空间里还没有数据,ebp 和 esp 指向的位置是程序加载时内核设置的。

image-20201103210051065

1.4. 程序的执行

  • 程序开始执行 main 函数第一条指令,eip 自动指向下一条指令。第一条指令的执行,致使 ebp 的地址值被保存在栈中。
  • 随着 ebp 地址值的 压栈,esp 自动向栈顶方向移动,它将永远指向栈顶。
image-20201103210746239
  • 程序继续执行,开始构建 main 函数自己的栈,ebp 原来指向的地址值已经被保存了,它被腾出来了,用来看管 main 函数的栈底,此时它和 esp 是重叠的

  • 程序继续执行,eip 指向下一条指令,此次执行的是局部变量 i 的初始化,初始值 4 被存储在栈中,esp 自动向栈顶方向移动。

  • 继续执行下一条指令,局部变量 j 的初始值 5 也被压栈

  • 接下来调用 fun 函数时压栈的数据虽然也保存在 main 函数的栈中, 但它们都是供 fun 函数用的。 可以说 fun 函数的数据, 一半在 fun 函数中, 一半在主调函数中, 下面来 看函数调用时留在 main 函数中的那一半数据。

    • 先执行传参的指令,此时参数入栈的顺序和代码中传参的书写顺序正好相反,参数 b 先入栈,数值是 main 函数中局部变量 j 的数值 5。
    • 程序继续执行,参数 a 被压入栈中,数值是局部变量 i 的数值 4
image-20201103211138499
  • 程序继续执行, 此次压入的是 fun 函数返回值, 将来 fun 函数返回之后, 这里的值会传递给 m
  • 还剩最后一步,跳转到 fun 函数去执行。
    • 一部分是把 fun 函数执行后的返回地址压入栈中,以便 fun 函数执行完毕后能返回到 main 函数中继续执行
    • 另一部分就是跳转到被调用的函数的第一条指令去执行
  • fun 函数开始执行,第一件事就是保存 ebp 指向的地址值,此时 ebp 指向的是 main 函数的栈底,保存的目 的是在返回时恢复 main 函数栈底的位置,这和前面 main 函数刚开始执行时第一步就保存 ebp 的地址值的目的 是一样的
image-20201103211526043
  • 执行几个运算指令
image-20201103211648452
  • 恢复现场以后,把 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>

void fun1()
{
int m = 10;
char num[4];
strcpy(num, "bbbbbbbbbbbbb\x0F\x10\x40\x00");
}

void fun2()
{
printf("you were attacked!!!\n");
}

int main()
{
fun1();
return 0;
}

在C语言中,栈的方向是从高地址向低地址延伸,而数组中数据在栈中的存储方向与此正好相反。

字符串拷贝等数组操作是不对数据长度做审核的,如果实际的数据长度超过了栈中预留的空间,就会将栈中其他数据覆盖,这种现象被称为“栈溢出”。栈溢出可能导致一个不可预期的错误,也可能导致一个精心策划的执行流程发生改变。

image-20201104213741370

1.8. 编译过程概述

1.8.1. 词法分析

词法分析的作用是从连续的字符中识别出标识符、关键字、数字、运算符并存储为符号(token)流。

1.8.2. 语法分析

语法分析的作用是从词法分析识别出的符号流中识别出符合C语言语法的语句。

1.8.3. 语法书->中间代码->目标代码

中间代码的设计思想: 计算机存在多种CPU硬件平台,要考虑到程序在不同CPU之间的移植性。先转换成通用的、抽象的CPU指令。

语法树是二维结构。

image-20201104215246865

中间代码是准一维结构。

image-20201104215312619

选定具体的 CPU、操作系统后, 中间代码就可以转换为目标代码——汇编代码

最后链接器把一个或多个目标文件(库文件本质上也是目标文件)链接成符合选定操作系统指定格式的可 执行文件。

2. 第二章 词法分析

2.1. 总体结构图

image-20201104215858679