上一部分,我们算是对汇编语言开了个头,介绍了基本操作指令相关的内容。这一部分,我们重点研究汇编语言的另外一块内容:栈帧结构。
7.3 栈帧结构
大多数机器只提供简单的指令,数据传递、局部变量的分配和释放需要通过操纵程序栈来实现,而为单个过程分配的那部分栈就成为栈帧。先来一张典型的结构图,直观感受一下:
在函数体内部,我们将本地变量和临时变量存储在当前帧下,也将被调用的函数的参数放进当前函数的栈帧中,并按照顺序往上生长。当调用其它函数时,按照如图所示的顺序传进实际参数,返回地址则被压入栈中,形成调用者栈帧的末尾。对于调用者帧区而言,它代表了对应的调用函数的定义所在的帧区,同当前帧的结构是完全相同的。具体的细节,大家可以参见任何一本讲解汇编语言的书籍。
有了这些知识之后,我们重点关注几类寄存器以及特殊情况的处理。
7.3.1 参数寄存器
x86-64架构下,前六个参数对应着具体的寄存器,如图7-1所示,只需要往对应的寄存器传入实际的参数即可。对于超过六个参数的函数而言,多余的参数则需要按照图7-2所示的结构,逐个的分配地址并传递实际参数,相当于参数1成为了第7个函数参数,往上依次类推。
由于寄存器有限,参数寄存器也会被当成常规寄存器使用,为了避免数据丢失,对于当前仍然被使用着的参数寄存器而言,需要先行保存,调用结束之后再恢复原来的数据。这时,也用栈来保存当前的数据。
7.3.2 调用者和被调用者保存寄存器
在函数体中,我们经常会调用别的函数,这些函数在自己函数体内部的处理过程中,往往也只有这些寄存器可以利用,势必会出现调用者和被调用者使用同一个寄存器的问题。为了防止数据被覆盖,我们还是会提前保存这些寄存器,调用结束之后再恢复。于是,我们约定俗成了两组寄存器,称为调用者保存寄存器和被调用者保存寄存器,分别由调用者和被调用者单独保存。为此,我们需要在操作数管理器中体现这样的一个特点:
def __init__(self):
...
self.callee_save_regs_used = []
self.caller_save_regs = [R10, R11]
self.callee_save_regs = [Rbx, Rbp]
def save_caller_saves(self):
for reg in self.caller_save_regs:
if reg not in self.regs_free:
self.copy_reg_to_temp(reg)
self.regs_free.append(reg)
def save_callee_saves(self):
for reg in self.callee_save_regs_used:
return f"push {reg.to_str('pointer')}"
def restore_callee_saves(self):
for reg in self.callee_save_regs_used:
return f"pop {reg.to_str('pointer')}"
def get_free_reg(self, valid_regs=None, preferred_reg=None):
if len(self.regs_free) > 0:
...
if reg is not None:
self.remove_free_reg(reg)
return reg
def remove_free_reg(self, reg):
self.regs_free.remove(reg)
if reg in self.callee_save_regs and \
reg not in self.callee_save_regs_used:
self.callee_save_regs_used.append(reg)
在remove_free_reg
中,我们会将当前被使用的被调用者寄存器加入到callee_save_regs_used
中保存。此时,如果我们调用函数,通过在前后调用save_callee_saves
和restore_callee_saves
就能实现寄存器被被调用者保存和恢复的功能。对于调用者负责保存的寄存器而言,我们会将正在使用的寄存器通过copy_reg_to_temp
函数将对应的内容先保存到临时地址上(我们会在下面详细介绍),然后再让寄存器变成可以使用的状态。
7.3.3 寄存器的特殊处理
为了减少寄存器的使用,防止可能存在的寄存器缺乏引起的数据迁移,我们会为每个用到的局部变量分配地址,这些地址具有相对于帧指针具体的偏移量。因此,可以直接使用帧指针的相对偏移量进行操作,减少了栈指针的移动。但是,由于汇编语言规定,操作指令的两个操作数不能同时为存储器。这时候,就需要将存储器的内容临时提取到寄存器,再进行操作。如果寄存器满负荷运转,则需要使用栈指针后(往低地址方向)的地址存储寄存器中的值,之后再恢复数据。为此,我们需要修改一下操作数管理器来实现这样的一个目的:
class FrameManager:
WORD_SIZE = 8
def __init__(self, parent):
...
self.mem_free = []
self.next_temp = 0
def set_base_fp(self, base_fp):
self.next_temp = base_fp - self.WORD_SIZE
def copy_reg_to_temp(self, valid_reg):
if len(self.mem_free) == 0:
self.mem_free.append(MemoryFrame(f"(%rsp)", self.next_temp))
self.next_temp -= self.WORD_SIZE
mem = self.mem_free.pop()
find_reg = False
for index, item in enumerate(self.stack):
if item == valid_reg:
self.stack[index] = mem
find_reg = True
break
if not find_reg:
comment = f"No free registers inside OR outside of stack!"
raise CompilerError(comment)
return valid_reg
def get_free_reg(self, preferred_reg=None):
if len(self.regs_free) > 0:
...
return self.copy_reg_to_temp(self.regs_almost_free[0])
def get_max_fp(self):
return self.next_temp + self.WORD_SIZE
在get_free_reg
函数中,如果我们无法得到一个可用的寄存器,就会将已经使用的某个寄存器中的值放进临时区域,并替换栈中对应的寄存器。这里,我们使用next_temp
来偏移得到存储寄存器中临时变量的地址,并保存到mem_free
。
至此,我们扩展和完善了操作数管理器。有了这些准备工作,我们将在下一部分为源代码生成汇编代码。
实现简易的C语言编译器(part 0)
实现简易的C语言编译器(part 1)
实现简易的C语言编译器(part 2)
实现简易的C语言编译器(part 3)
实现简易的C语言编译器(part 4)
实现简易的C语言编译器(part 5)
实现简易的C语言编译器(part 6)
实现简易的C语言编译器(part 7)
实现简易的C语言编译器(part 8)
实现简易的C语言编译器(part 9)
实现简易的C语言编译器(part 10)