理论教育 编译器设计之路-代码生成器结构

编译器设计之路-代码生成器结构

时间:2023-11-04 理论教育 版权反馈
【摘要】:前面,已经详细介绍了寄存器分配的基本思想与实现。本小节将关注代码生成器主体结构的源码实现。前面主要讨论了指令模板的相关数据结构,这是剖析代码生成器源码的重要基础。代码生成器是以操作数来源作为主线实现的,下面就来看看代码生成器的实现细节。代码生成器对数据流信息的依赖程度是比较大的,尤其在评估死变量时。代码生成器将从系统文件夹中的AsmScheme.txt文件读取指令模板库。代码生成是以过程为单位进行的。

编译器设计之路-代码生成器结构

前面,已经详细介绍了寄存器分配的基本思想与实现。本小节将关注代码生成器主体结构的源码实现。通过本程序的源代码分析,读者可以了解到许多代码生成的实现细节,如指令模板的应用、寄存器的调度、寄存器溢出等。在详细分析源代码之前,笔者先引入一个指令模板的相关数据结构Pattern。

【声明9-6】

szPattern:模式串,这是指令模板的关键字。

CodeList:指令序列,这是目标代码的实体,Code结构说明如声明9-6所示。

Flg:操作数的来源信息,包括来源标记的相关信息,RegFlg结构说明如声明9-7所示。

SaveReg:不可用寄存器列表,即使用本指令模板前必须回存的寄存器。

【声明9-7】

Label:指令的标号。

Op:指令的操作码。

Operandl:指令的操作数1。

Operand2:指令的操作数2。

Operand3:指令的操作数3。

Comment:指令的注释。

【声明9-8】

cFlg:操作数的来源标志。

cReg:如果操作数的值必须来源于某一特定寄存器,则cReg记录的是该寄存器的编号。

iSize:操作数的大小。

前面主要讨论了指令模板的相关数据结构,这是剖析代码生成器源码的重要基础。代码生成器是以操作数来源作为主线实现的,下面就来看看代码生成器的实现细节。

程序9-4 Target.cpp

第3行:调用CDataFlowAnalysis::DataFlowAnalysis函数完成数据流分析。代码生成器对数据流信息的依赖程度是比较大的,尤其在评估死变量时。因此,数据流信息是必不可少的。

第4行:pCurrBlock指针用于指向当前正在分析的基本块,在算法实现中,pCurrBlock指针是非常有用的。

第5行:调用Genlnit函数,初始化指令模板库。代码生成器将从系统文件夹中的AsmScheme.txt文件读取指令模板库。关于指令模板的格式,请参考9.3.2节。

第6行:CurrProcld用于记录当前正在分析的函数符号。

第7行:CurrAsmCodeList用于指向目标代码的插入点。关于目标代码的数据结构,笔者稍后详解。

第8行:调用IRtoAsmList函数,生成目标代码。IRtoAsmList函数的第1个参数就是用于匹配指令模板的模式串。而第2个参数是根据IR产生的参数列表,这个参数列表将在匹配到指令模板后,用于替换模板中指令序列的参数,以便最终生成目标代码。“FILESTART”模板主要用于在汇编代码列表中生成文件头部声明。

第9行:调用GetLibAsm函数,在汇编代码列表中生成库引用的声明。

第10行:调用MemoryAlloc函数,在汇编代码列表中生成变量、常量等符号声明。

第11行:调用RecOpAsm函数,在汇编代码列表中生成“.code”文件。

第12~306行:遍历过程信息表。代码生成是以过程为单位进行的。

第14~15行:忽略未被引用的过程。这些过程是不需要生成目标代码的。

第16~17行:忽略外部过程或函数,这些过程也是不需要生成目标代码的。

第18~19行:忽略匿名过程或函数,这些过程可以视作未实例化的过程类型。

第20行:令CurrProcId指向当前过程符号。

第21行:令CurrAsmCodeList指针指向当前过程的汇编代码列表。

第22~23行:在汇编代码列表中生成关于当前过程的注释说明。

第24~25行:如果当前过程是主程序,则应用“MAINSTART”模板生成“start:”标号。(www.daowen.com)

第27~29行:应用“FUNCSTART”模板生成函数起始部分的相关代码,主要是运行时刻栈的维护代码。其中,ParaStrl列表的第2个参数是当前过程或函数所属局部变量的空间大小。

第30行:获取当前过程的基本块列表。

第31~286行:遍历当前过程的所有基本块。基本块是寄存器分配与管理的单位。在Neo Pascal中,跨越基本块的寄存器管理不予考虑。

第34~285行:遍历当前基本块的IR序列,逐一生成相应的目标代码。

第39行:获取当前IR。

第40行:在汇编代码列表中生成关于当前IR的注释说明。

第41~45行:如果当前IR的操作码为PARA,则调用GenParaAsm生成相应的目标代码。

第46~50行:如果当前IR的操作码为ASSIGN N,则调用GenAssignNAsm生成相应的目标代码。

第51~58行:如果当前IR的操作码为ASM,则将内嵌汇编文本直接插到目标代码中。

第59~63行:如果当前IR的操作码为RETV,则调用GenRetAsm生成相应的目标代码。

第64~75行:根据IR的操作码、操作数等情况,生成相应模式串。

第78~283行:运用模式串匹配指令模板库,如果检索成功,则根据模板指示生成代码。

第81行:清空ExtReg向量。值得注意的是,ExtReg向量的作用就是标识指令模板中的辅助寄存器。事实上,通用的指令模板仍有一定局限,对于较复杂的应用,不得不借助于辅助的寄存器实现。因此,在应用此类指令模板时,也必须关注因辅助寄存器而产生的寄存器溢出。这里,借助于ExtReg向量记录当前模板的辅助寄存器。

第82行:清空Forbid集合。Forbid集合用于记录指令模板中的“不可用”寄存器。

第83~87行:遍历当前指令模板的SaveReg集合。SaveReg集合中记录的正是当前指令模板中“不可用”的寄存器号,也就是说,指定寄存器在模板中有特殊应用。因此,需要调用SaveRegs函数,生成代码将指定寄存器的值回存到相应的内存中,如果指定寄存器并没有绑定变量,则不作处理。

第88~95行:根据指令模板的辅助寄存器列表,生成相应的汇编代码。

第96~166行:根据操作数1生成相应的参数,以便替换指令模板中的“%V.l%”。

第99~104行:如果操作数1的m_bRef为真,则必须调用OpRef函数,并生成用于间接寻址访问的汇编代码。值得注意的是,为了保证安全,这里涉及的寄存器将在当前IR翻译过程中不可再分配,因此,也可以视为“不可用”寄存器。

第107~113行:指令模板中操作数来源标记为0,则表示该操作数必须为直接内存寻址。如果操作数存在于寄存器中,则必须将其存到内存中,然后再从内存直接取值。

第115~131行:指令模板中操作数来源标记为1,则表示该操作数必须为寄存器寻址。如果操作数不存在于寄存器中,则必须先将其传输至寄存器,然后再从该寄存器取值。其中,LoadReg函数用于生成将变量值读入寄存器的指令代码。而SetVal函数用于将变量与寄存器绑定。

第132~139行:指令模板中操作数来源标记为2,则表示该操作数为优先寄存器寻址。如果操作数存在于寄存器中,则优先从寄存器取值,否则从内存取值。

第144~158行:如果操作数1为常量且操作数来源不为直接内存寻址,则生成代码将操作数1的值传送到寄存器。

第160~164行:在其他情况下,根据操作数1生成参数相应的参数字符串即可。

第167~236行:根据操作数2,生成相应的参数,以便替换指令模板中的“%V.2%”。这部分源码的实现与操作数1的处理类似,不再赘述。

第237~278行:根据操作数3,生成相应的参数,以便替换指令模板中的“%V.3%”。

第242~255行:如果操作数3的m_bRef为真,则必须调用OpRef函数,并生成用于间接寻址访问的汇编代码。

第260~267行:指令模板中操作数来源标记为1,则表示需要申请一个寄存器,用于存储运算结果。

第268~269行:指令模板中操作数来源标记为2,则表示将运算结果存放在操作数2的寄存器中。因此,这里需要调用SetVal函数将结果变量与操作数2的寄存器绑定。

第270~271行:指令模板中操作数来源标记为3,则表示将运算结果存放在操作数1的寄存器中。因此,这里需要调用SetVal函数将结果变量与操作数1的寄存器绑定。

第279~281行:将ExtReg集合中指示的寄存器名也加入ParaStrl中,供IRtoAsmList函数使用。

第282行:调用IRtoAsmList函数,根据当前指令模板及传入参数列表ParaStrl,生成相应的目标代码。

第288~301行:如果当前过程存在返回类型,则需要处理返回值的传递。根据返回类型的大小,分别应用“FUNCRET4”、“FUNCRET8”、“FUNCRETA”模板进行代码生成。

第302~305行:应用“FUNCEND”模板生成函数结尾部分的相关代码,主要是运行时刻栈的维护代码。其中,ParaStrl列表的第2个参数是当前过程或函数所属参数的空间大小。

第307~309行:应用“FILEEND”模板生成汇编文件结尾部分的相关代码。

至此,笔者已经详细介绍了代码生成器的基本结构。本程序的源代码篇幅较长,阅读理解可能有一定难度,建议读者仔细推敲。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈