1
1
# 目标代码生成
2
2
3
- 中间代码生成后,我们就来到了编译器设计的最后阶段了。目标代码生成通常以编译器此前生成的** 中间代码** 、** 符号表** 及其他相关信息作为输入,输出与源程序** 语义等价** 的目标程序代码。 代码生成模块需要面向某一个特定的目标体系结构生成目标代码,这种目标体系结构可以是 X86、MIPS、ARM 等,而且由于我们采用三端设计,因此我们可以把目标代码生成理解成是** 对中间代码的翻译** 。对于不同体系结构,中间代码翻译成目标代码的处理方式也不同。下面我们给出一个中间代码为四元式,目标代码为MIPS指令结构的指导方案 。
3
+ 中间代码生成后,我们就来到了编译器设计的最后阶段了。目标代码生成通常以编译器此前生成的** 中间代码** 、** 符号表** 及其他相关信息作为输入,输出与源程序** 语义等价** 的目标程序代码。 代码生成模块需要面向某一个特定的目标体系结构生成目标代码,这种目标体系结构可以是 X86、MIPS、ARM 等,而且由于我们采用三端设计,因此我们可以把目标代码生成理解成是** 对中间代码的翻译** 。对于不同体系结构,中间代码翻译成目标代码的处理方式也不同。下面我们给出一个中间代码为四元式,目标代码为 MIPS 指令结构的指导方案 。
4
4
5
5
## 一、MIPS 快速回顾
6
6
20
20
21
21
- ` $zero ` :始终为 0,可以用来减少常数 0 的使用。
22
22
- ` $at ` (assembly temporary):汇编器保留寄存器,由汇编器在特定场景(通常是加载大常数)自动生成。
23
- - ` $v0 - $v1 ` :作为函数返回值,一般返回值只使用 ` $v0 ` ,当返回值超过 32 位时会同时使用 ` $v1 ` 。
23
+ - ` $v0 - $v1 ` :作为函数返回值,一般返回值只使用 ` $v0 ` ,当返回值超过 32 位时需要同时使用 ` $v1 ` 。
24
24
- ` $a0 - $a3 ` :函数调用参数,前 4 个参数通常保存在这几寄存器中,更多的参数则是压栈处理。
25
25
- ` $t0 - $t7, $t8 - $t9 ` :临时寄存器,用于基本块内的变量,发生函数调用时不必保存。
26
26
- ` $s0 - $s7 ` (saved):全局寄存器,这些寄存器用于跨基本块的变量,往往需要在发生函数调用时进行保存。
@@ -64,6 +64,8 @@ int main() {
64
64
```
65
65
66
66
> MIPS ` .data ` 段的数据是依次存储的,因此细心的你可能会觉得这里有潜在的字节不对齐的问题。但是不用担心,在 ` .data ` 段汇编器是会自动完成字节对齐的操作的,不过之后在 ` .text ` 段使用 1 字节存储 ` char ` 类型变量时就需要注意变量的分配的位置了。
67
+ >
68
+ > 同时,由于 ` printf ` 中的格式化字符串并没有具体对应变量,因此需要单独为它们分配全局变量名称。
67
69
68
70
通过生成全局数据段,我们可以在程序运行之前为所有的全局变量分配内存空间,并在必要时将它们初始化为指定的值。这样,在程序的执行过程中,全局变量的值就可以被保存在内存中,并随时被读取和修改。
69
71
@@ -75,13 +77,15 @@ int main() {
75
77
76
78
当我们调用函数时,都需要为其在栈上分配一段内存,称为栈帧。其中包含了函数中定义的局部变量,溢出的变量,保存的寄存器以及传出的参数。在 MIPS 中,栈帧的基本结构如下。
77
79
78
- ![ stack-frame] ( imgs/chapter07-3/stack-frame.png )
80
+ ![ stack-frame] ( imgs/chapter07-3/stack-frame.svg )
79
81
80
82
> 尽管 MIPS 中使用 ` $fp ` 和 ` $sp ` 共同维护栈帧,但实际上只使用 ` $sp ` 也是足够的。关于栈帧更详细的内容可以参考 [ MIPS Calling Convention] ( https://courses.cs.washington.edu/courses/cse410/09sp/examples/MIPSCallingConventionsSummary.pdf ) 。
81
83
82
84
在栈帧管理中,比较关键的是参数的处理。不同函数间彼此不可见,因此为了使得被调用的函数能够正确获取参数,调用者需要将参数保存在紧挨着被调用者栈帧的位置,即当前的栈顶。这里需要注意,尽管我们可以将前 4 个参数通过 ` $a0 - $a3 ` 传递,我们仍需要在栈中为其预留位置(不用保存值)。
83
85
84
- 我们的实验中,存在 ` int ` 和 ` char ` 两种类型,其中 ` int ` 占 4 字节,而 ` char ` 只占 1 字节。因此这里就涉及到了字节对齐的问题。MIPS 要求了 ` lw/sw ` 指令的地址必须四字节对齐,因此在为 ` int ` 分配空间时有可能需要添加 padding 来保证 4 字节对齐。此外,在 MIPS 中还要求栈帧 8 字节对齐,因此必要时可以在 local variable 之后添加 4 字节的 padding。
86
+ > 这样的参数传递是 C 语言的调用规范,由于大家实验中的代码不涉及与 C 语言标准库的链接,因此自行设计参数传递方式也是可以的。
87
+
88
+ 我们的实验中,存在 ` int ` 和 ` char ` 两种类型,其中 ` int ` 占 4 字节,而 ` char ` 只占 1 字节。因此这里就涉及到了字节对齐的问题。MIPS 要求了 ` lw/sw ` 指令的地址必须四字节对齐,因此在为 ` int ` 分配空间时有可能需要添加 padding 来保证 4 字节对齐。此外,规范的 MIPS 还要求栈帧 8 字节对齐,因此必要时可以在 local variable 之后添加 4 字节的 padding,不过在实验中可以不用处理栈帧的字节对齐问题。
85
89
86
90
### (2)寄存器分配
87
91
@@ -158,11 +162,11 @@ sw $t3, ($t1)
158
162
- 函数序言:申请所需的栈空间,即更新 ` $sp ` (和 ` $fp ` ) 寄存器,保存使用到的全局寄存器(如果需要),以及自己的 ` $ra ` 寄存器。
159
163
- 函数尾声:释放栈空间,恢复全局寄存器(如果需要)以及 ` $ra ` 寄存器。
160
164
161
- > 由于任意全局寄存器都可能被调用者使用,因此为了不破坏调用现场,被调用者需要保存其使用到的全局寄存器,并在结束时进行恢复 。
165
+ > 由于任意全局寄存器都可能被调用者使用,因此为了不破坏调用现场,被调用者需要保存其使用到的全局寄存器,并在结束时恢复其原本的值 。
162
166
163
167
对于函数调用者,主要有以下几个步骤。
164
168
165
- 1 . 参数传递。在调用函数前,调用者需要将函数参数压入栈中。对于 MIPS,可以将前四个参数通过 ` $a0 - $a3 ` 四个寄存器传递,但仍需要为其在栈中预留位置。参数的位置通常由参数编号和当前的 ` $sp ` 决定,从而被调用者可以在不知道调用者栈帧的情况下获取参数。
169
+ 1 . 参数传递。在调用函数前,调用者需要将函数参数压入栈中。对于 MIPS,可以将前四个参数通过 ` $a0 - $a3 ` 四个寄存器传递,但仍需要为其在栈中预留位置。参数的位置由参数编号和当前的 ` $sp ` 决定,从而被调用者可以在不知道调用者栈帧的情况下获取参数。
166
170
2 . 保存现场(可选),也可以将这一任务交给被调用者的函数序言。
167
171
3 . 函数跳转。通过 ` jal ` 或 ` jalr ` 指令跳转到被调用的函数,函数返回值被保存在 ` $v0 ` 寄存器中。
168
172
4 . 恢复现场(可选),也可以将这一任务交给被调用者的函数尾声。
195
199
196
200
sw $a0, 8($sp) # 如需要,保存参数至预留的栈空间
197
201
198
- lw $t0, 8($sp) # 返回值
202
+ lw $t0, 8($sp) # b = a
203
+ sw $t0, 4($sp)
204
+
205
+ lw $t0, 4($sp) # 准备返回值
199
206
move $v0, $t0
200
207
201
208
lw $ra, 0($sp) # 恢复自己的 $ra
@@ -212,7 +219,7 @@ main:
212
219
jal f # 调用函数
213
220
move $t0, $v0 # 获取返回值(此处未使用该返回值)
214
221
215
- lw $ra, 4($sp) # 恢复自己的 $ra
222
+ lw $ra, 4($sp) # 恢复自己的 $ra
216
223
addu $sp, $sp, 8 # 恢复栈帧
217
224
218
225
li $v0, 10 # 10 号系统调用,结束程序
0 commit comments