summaryrefslogtreecommitdiffstats
path: root/content/posts/从编译原理到物理原理剖析程序的编译与执行.md
blob: b7ff5e5ef5ee4437228841f1fa05138c1a1b891a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
---
categories:
- 技术
date: "2025-07-29T15:55:14+08:00"
draft: false
slug: ""
tags:
- 黑历史
- 底层技术
- 编译器
- 编译原理
title: 从编译原理到物理原理剖析程序的编译与执行
---
## 编译过程

以C语言为例,编译成可执行文件一共要经历:**预处理=>狭义编译=>汇编=>链接**,最终成为可执行文件。

### 预处理

输入源代码文件以及其包含的头文件,由预处理器执行展开宏定义、处理,条件编译指令、将包含的头文件直接插入到指令位置,删除注释。

输出纯净的、宏已展开、注释已删除、头文件已包含的中间代码文件,通常为`.i`。

编写一个简单的程序:

```c
#include <stdio.h>
#define STRING "Program"

int
main() {
  // 打印一些内容
  printf("Hello world!\n");
  printf("%s\n", STRING);
  return 0;
}
```

使用gcc的`-E`选项生成中间代码:

`gcc exam.c -E -o ./exam.i`

他生成了非常长的代码

可以验证预处理器是直接把头文件替换进来的。而代码中类似`# 5 "exam.c"`的标记用于记录源代码的位置信息,便于调试器和错误诊断程序追踪代码位置。

### 编译(狭义编译)

#### 词法分析

输入预处理后的源代码文件,有编译器执行词法分析,将字符流分解成有意义的词素(Token),例如:

```c
int sum = a + b;
```

分解成`int`, `sum`, `=`, `a`, `+`, `b`等Token。

#### 语法分析

根据语言的语法规则,将Token序列组合成抽象语法树(Abstract Syntax Tree),简称AST,表达了代码的结构和层次关系。

#### 语义分析

检查程序的语义是否正确,例如变量是否声明、类型是否匹配等。

#### 中间代码生成

将AST转换成一种独立于CPU架构的中间表示形式(intermediate Representation),即IR。常见的IR有三地址码、LLVM IR、Java字节码等,为了优化和转移成多种目标机器码。

#### 优化

对IR进行处理,目的是在不改变程序行为的前提下,提高代码效率。包括:

- 常量折叠:如计算`3 + 5`为`8`。
- 死代码消除:移除永远不会执行到的代码。
- 循环优化:如循环展开。
- 函数内联。
- 寄存器分配:决定哪些变量存储在高速的CPU寄存器中。

输出优化后的中间代码。

### 汇编

输入IR,或直接由编译器生成的汇编代码。由汇编器`as`执行。将汇编指令一对一地转换成特定CPU架构的机器操作指令。处理伪汇编指令,如定义数据段`.data`,定义代码段`.text`,分配存储空间`space`等。最后解析符号、如函数名、变量名等,生成符号表。

输出目标文件,如.o,.obj,包含机器指令、全局变量、静态变量的初始值等、符号表、重定位信息。

### 链接

输入一个或多个.o文件夹+库文件(静态.a/.lib,动态.so/.dll)。由链接器进行符号解析、重定位、库处理,最输出可执行文件或库文件。

---


## CPU如何识别指令

CPU执行程序的循环称为Fetch-Decode-Execute Cycle(取指-译码-执行周期)。

### 取指

CPU内部有一个寄存器叫程序计数器(Program Counter, PC),它保存着下一条要执行的指令的内存地址,CPU将PC中的地址发送到地址总线,内存控制器根据地址总线上的地址,找到对应的内存单元,将其存储的指令通过数据总线送回CPU。取回来的指令被放入指令寄存器IR。PC的值自动增加,指向下一条指令的地址。

### 译码指令

CPU的控制单元读取IR中的指令,控制单元包含一指令译码器,译码器分析指令 操作码部分,操作码唯一表示CPU应该执行什么操作,如ADD MOV JMP等。

根据操作码,译码器决定:
- 操作的性质(算术、逻辑、数据传输、跳转等)。
- 操作需要多少个操作数。
- 操作数存放位置(寄存器、内部地址指令本身中的立即数)

译码器激活执行该操作所需要的CPU内部电路通路和控制信号,结果决定了下一个阶段(执行)需要做什么。

### 执行指令

CPU的算术逻辑单元(Arithmetic Logic Unit, ALU)或去他功能单元(FPU MMU)根据译码器产生的控制信号执行实际操作。

操作数可能从寄存器文件中读取,或者从内存中加载,ALU执行加减与或移位等操作,如果指令是JMP/CALL/RET/分支指令等跳转指令,可能会修改PC的值,从而改变下一条指令的位置。计算结果可能写回寄存器,或者通过数据总线写会内存。

上述的操作循环以极高的速度(GHz级别)不断重复,构成了CPU运行程序基础。

## 编译后的指令在物理层面上是什么

答:**内存中的电荷状态**。

程序被操作系统加载到计算机的RAM中,RAM由无数的存储单元(通常是电容)组成,每一个单元可以存储一个bit的信息。

高电平电荷(通常代表1)或低电平电荷(通常代表0)的状态,就表示一个二进制位。

每条指令由多个bit组成。指令序列在RAM中是一系列连续存储单元的电荷状态。当CPU取指令时,PC寄存器的值,也就是一组触发器的电平状态被送到地址总线,也就是一组物理导线。

地址总线上的电平信号激活内存控制器和特定的存储单元。

被选中的内存单元的电荷状态被读出,转换为相应的电平信号,通过数据总线传回CPU。

这些电平信号进入CPU的指令寄存器IR,也是一组触发器的电平状态。

一组一组电平状态激活着CPU中的组件,从而执行指令。