前面,已经详细介绍了寄存器分配的基本思想与实现。本小节将关注代码生成器主体结构的源码实现。通过本程序的源代码分析,读者可以了解到许多代码生成的实现细节,如指令模板的应用、寄存器的调度、寄存器溢出等。在详细分析源代码之前,笔者先引入一个指令模板的相关数据结构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”模板生成汇编文件结尾部分的相关代码。
至此,笔者已经详细介绍了代码生成器的基本结构。本程序的源代码篇幅较长,阅读理解可能有一定难度,建议读者仔细推敲。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。