C编译器剖析_6.1 汇编代码生成_简介

6.1 汇编代码生成简介

历经词法分析、语法分析、语义检查和中间代码生成阶段,我们终于来到了“目标代码生成阶段”,由于UCC编译器的目标代码即为32位x86汇编代码,因此我们就把本章称为“汇编代码生成”。UCC编译器中的大部分源代码都适用于Windows和Linux平台,但Windows平台上缺省的汇编器支持Intel风格的x86汇编代码,而Linux平台默认的汇编器则采用AT&T风格的x86汇编代码,两者在汇编语法上有一些差别,为节省篇幅,我们主要针对Linux平台的AT&T汇编进行讨论。到了这一章,UCC编译器面对的输入早已不再是C源代码,而主要是由各个基本块构成的链表,我们要把基本块中的中间代码翻译成x86汇编代码。我们在“第1.5节结合C语言来学汇编”中,已介绍过x86汇编代码的相关语法和语义,这里不再重复。

还是通过一个简单的例子来了解一下UCC编译器的汇编代码生成,如图6.1.1所示,第1至6行是一个简单的C程序,第11至58行是由UCC编译器产生的汇编代码。形如第13行C++风格的注释是我们人为加上去的,用于说明第14行的汇编代码“.data”是由Segment()函数产生。我们省略了main函数对应的汇编代码。

图6.1.1 汇编代码生成的例子

在C程序中出现的字符串可被视为全局的字符数组(当然对C程序员而言,该字符数组的名称不可见),通过第15行的EmitStrings()函数,UCC编译器可以为字符串产生形如第16行的字符数组,而通过第18行的EmitFloatConstants函数,则可以为形如“1.0”的浮点常数分配存储空间。UCC编译器会隐式地为字符串和浮点常数命名,例如第16行的“.str0”和第19行的“.flt0”,这些名字都以“.”开始。按C语言的语法,由C程序员命名的变量名或函数名不会以“.”开始,这就可保证不会发生名称上的重名。在汇编代码中,符号名是允许用“.”开始的。第21行的EmitGlobals函数用于处理C程序员定义的全局变量,这会产生形如第22至23行的汇编代码。第26行的EmitFunction()用于产生函数f对应的汇编代码,如图第27至58行所示。

图6.1.1第30至34行的代码用于保存寄存器的值,第35行用于在栈空间中预留内存空间,用来存放局部变量和临时变量,这部分工作被称为“Prologue序言”,即在函数开始执行时要处理的工作。而图6.1.1第53至57行被称为“Epilogue尾声”,用于恢复原先保存的寄存器值,第58行的汇编指令ret用于从栈中取出返回地址并返回。而函数f的返回值在第50至51行进行计算,并保存于寄存器eax。第37行的EmitBlock函数用来为某一基本块生成汇编代码。UCC编译器内部用英文单词generate来表示中间代码的生成,而用emit来表示汇编代码的生成,这里我们统一翻译为“生成”。

我们还注意到,图6.1.1第2行的形参num在汇编代码中,对应的名称是第50行的“20(%ebp)”,而第3行的局部变量b对应的是第40行的“-8(%ebp)”,这再次提醒我们,在汇编层次,我们需要为局部变量和形式参数设置新的名称。为了得到某个符号对象p在汇编代码中的名称,UCC编译器定义了函数GetAccessName,其接口如下所示,稍后,我们会对这个函数进行分析。

static char* GetAccessName(Symbol  p);

下面,我们来分析一下用于生成图6.1.1汇编代码的函数EmitTranslationUnit,如图6.1.2所示,第1至11行的函数EmitTranslationUnit会为整个翻译单元产生汇编代码,我们已在图6.1.1的注释中标出BeginProgram等函数的作用。第8行的ImportFunctions用于在Windows平台Intel风格的汇编中,产生形如“EXTRN
 f:NEAR32”的函数声明,在Linux平台上ImportFunctions并无作用。图6.1.2第12至26行为EmitFunctions的代码,通过第16行的while循环,我们在第21行调用EmitFunction来为各个函数产生汇编代码。

图6.1.2  EmitTranslationUnit()

图6.1.2第27至58行为函数EmitFunction的代码,在第33行调用的Export函数用于在Linux平台上产生形如“.globl          f”的函数声明,第35行的DefineLabel函数用于产生形如“f:”的标号。按C标准的规定,当“函数的返回值是结构体对象,且该对象的大小不落在{1,2,4,8}”时,C编译器会隐式地为该函数添加一个参数,该参数的类型是指向结构体对象的指针。例如,以下结构体struct
 Data的对象要占32字节,C编译器会为函数GetData隐式地添加一个structData * recvaddr参数,图6.1.2第36至47行的if语句用于对此进行处理。

struct  Data  {int  dt[8];};

// C程序员设定的函数接口

struct  Data GetData(int  num);

//被C编译器隐式地改为

void GetData(struct  Data * recvaddr,  int  num);

图6.1.2第48行调用的LayoutFrame函数用来计算“形式参数、局部变量和临时变量”在活动记录中的偏移,并返回“局部变量和临时变量”在栈中所占内存的总和,我们会在稍后对这个函数进行分析。图6.1.2第50行调用EmitPrologue来产生“序言”,如图6.1.1第29至35行所示,图6.1.1第35行“subl $16,%esp”中的常数16,就是“函数f里的局部变量和临时变量所占栈内存的总和”。通过图6.1.2第51至55行的while循环,我们可为各基本块产生汇编代码,这主要是由第54行调用的EmitBlock函数来完成。图6.1.2第56行调用的EmitEpilogue函数来产生“尾声”部分,如图6.1.1第52至58行所示。

图6.1.2中调用的EmitStrings和EmitFloatConstants等函数并不复杂,我们就不再展开讨论,在后续的章节中,我们把分析的焦点放在基本块的翻译上,即EmitBlock函数。在本节中,我们再来分析一下前面遇到的LayoutFrame和GetAccessName这两个函数,其中LayoutFrame函数的代码如图6.1.3所示。按照C标准的规定,在被调函数返回后,寄存器ebx、esi和edi的值要和函数调用前的值一样,这些寄存器被称为“保值寄存器”。UCC编译器会在“函数的序言”中把这几个寄存器的值入栈;而在“函数的尾声”中恢复这几个寄存器的值,从而实现“保值”的要求。另外,当被调函数返回时,寄存器ebp需要再次指向主调函数的活动记录,因此被调函数也要在栈中先保存ebp寄存器的值,因此总共需要由被调函数保护的寄存器有4个,即6.1.3第16行的宏PRESERVE_REGS所对应的值。

图6.1.3   LayoutFrame()

图6.1.3第3至13行给出了栈的布局图,第1个形参的位置为“20(%ebp)”,而第1个局部变量或临时变量的位置为“-4(%ebp)”。第24至35行的while循环用于为各个形参计算其偏移,其偏移从20开始依次递增;第39至54行的while循环用于为局部变量和临时变量计算偏移,其偏移从“-4”开始依次递减。第56行返回“局部变量和临时变量所占用的栈空间的总和”。

接下来,我们来看一下Linux版的函数GetAccessName,其代码如图6.1.4所示,第6至8行用于处理整型常数,在AT&T汇编指令中其符号形如”$4”;浮点常数对应的名称已在图6.1.1第18行的EmitFloatConstants函数中,被设置为形如“.flt0”;第9至12行用来产生形如“.str0”的字符串名和“.BB2”标号。全局变量名和“处于函数体外的静态变量名”,仍可用于汇编代码中,第17行的赋值语句对此进行设置。为了避免重名,UCC编译器会对“位于函数体内的静态变量名”进行改名,得到的名称形如第23行的“c.1”,第18至26行对此进行处理。

图6.1.4 GetAccessName()

对于局部变量、形式参数和临时变量,在汇编代码中,我们用形如“20(%ebp)”这样的符号来表示,图6.1.4第27至32行会根据我们在图6.1.3的LayoutFrame函数中计算出来的偏移,来设置相应的符号名。在汇编代码中,函数名仍然可以直接使用,图6.1.4第33至34行对此进行处理。对于全局或静态的“数组元素和结构体成员”,我们可用形如第42行的“arr+12”符号来表示,而对于局部的数组或结构体对象,则使用形如“20(%ebp)”的符号,如图6.1.4第35至56行所示。当我们已经得到某一符号p在汇编代码中的名称p->aname后,就没有必要再重复计算,第2行的if语句会对此进行判断。

在这小节中,结合图6.1.1的例子,讨论了汇编代码生成的总体流程。在汇编代码生成中,一个很重要的问题就是寄存器的分配。在后面的章节中,我们会对“用于为基本块产生汇编代码”的函数EmitBlock进行讨论。

时间: 2024-12-20 01:21:07

C编译器剖析_6.1 汇编代码生成_简介的相关文章

C编译器剖析_6.2 汇编代码生成_寄存器的管理

在计算机中,CPU的速度比内存的速度快得多,编译器应尽量有效地利用寄存器资源,减少对内存的不必要访问,从而提高由编译器生成的汇编代码的运行速度.在中间代码生成阶段,UCC编译器用临时变量t来存放形如"t: a+b;"的公共子表达式的值:到了汇编代码生成时,UCC编译器会尽可能地把这些公共子表达式的值存放在寄存器,当需要再次重用时,就可以直接由相应的寄存器中得到.不过,CPU中寄存器的资源是很有限的,在32位的x86 芯片上,汇编程序员可用的寄存器有{eax,ebx, ecx,edx,

C编译器剖析_6.3.5 汇编代码生成_为类型转换产生汇编代码

6.3.5  为类型转换产生汇编代码 在这一小节中,我们来讨论一下整型和浮点型之间的类型转换.有些类型转换并不需要在汇编层次进行数据转换,例如int和unsigned  int之间的转换只是改变了表达式的类型,对数据本身并无影响,以下表达式"(unsigned int) a"对应的二进制数据为0xFFFFFFFF,而表达式"a"对应的二进制数据也为0xFFFFFFFF.但对相同内容的二进制数据来说,进行"有符号整数的右移"和"无符号整数

C编译器剖析_6.3.1 汇编代码生成_由中间指令产生汇编代码的主要流程

6.3.1  由中间指令产生汇编代码的主要流程 在这一小节,我们可把关注的焦点放在"如何把某条中间代码翻译成汇编代码"上.UCC编译器的中间代码是如下所示的四元式,包括运算符和3个操作数. <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> 当然有些中间代码只需要用到opcode和DST就可以了,例如,无条件跳转指令"goto  BB2;"就不需要SRC1和SRC2.为了便于汇编代码的生成,UCC编译器在ucl\X86Linux

C编译器剖析_6.3.6 汇编代码生成_为“取地址”产生汇编指令

6.3.6  为"取地址"产生汇编指令 在这一小节中,我们来讨论一下以下两条中间指令的翻译: (1)取地址指令<ADDR,DST,SRC1,NULL> 例如 <ADDR,t0,  number, NULL>,表示取number的地址并保存到临时变量t0中 (2)对象清零指令< CLR,DST,SRC1,NULL> 例如<CLR,arr,16,NULL>,表示把arr所占16字节的内存清零 我们先举一个例子来说明,对于图6.3.14第4行局

C编译器剖析_5.1 中间代码生成及优化_简介

5.1  中间代码生成与优化_简介 在语法分析和语义检查阶段,我们始终在与语句Statement.表达式Expression和外部声明ExternalDeclaration这3个概念打交道.通过声明,我们最终建立起了相应的类型结构,并在符号表中保存了相关标识符的类型信息,到了中间代码生成阶段,我们就不再需要处理外部声明ExternalDeclaration了,只需要为语句和表达式生成相应的中间代码即可.文件ucl\tranexpr.c用于为表达式生成中间代码,文件名tranexpr是Transl

C编译器剖析_6.3.4 汇编代码生成_为函数调用与返回产生汇编代码

6.3.4        为函数调用与返回产生汇编代码 在这一小节中,我们来讨论一下如何为函数调用和函数返回生成汇编代码.函数调用对应的中间指令如下所示: //中间指令的四元式: < opcode, DST, SRC1, SRC2> <CALL, 用于接收返回值的变量retVal, 函数名func,  参数列表[arg1,arg2, -,argn]> 让我们先熟悉一下C函数的调用约定CallingConvention,我们需要把参数从右向左入栈(即从argn到arg1依次入栈),不

C编译器剖析_6.3.3 汇编代码生成_为跳转指令产生汇编代码

6.3.3        为跳转指令产生汇编代码 在这一小节中,我们要为"有条件跳转"."无条件跳转"和"间接跳转"产生相应的汇编指令.中间指令的四元式如下所示: <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> (1) 有条件跳转,例如"if (a <= b) goto BB2;",四元式为: <JLE,BB2,a,b> ////////对应的汇编代码////////

C编译器剖析_6.3.2 汇编代码生成_由EmitAssign函数产生算术运算的汇编代码

6.3.2  由EmitAssign函数产生算术运算的汇编代码 在这一小节中,我们要讨论的中间指令形如"t1: a+b;"或者"t2:&number",这些指令用于进行一元或二元算术运算,并把运算结果保存在临时变量t1或者t2中.UCC中间指令的格式如下所示: <运算符opcode,目的操作数DST,源操作数SRC1,源操作数SRC2> <ADD,t1,a,b>           //  t1: a+b; <ADDR,t2,n

C编译器剖析_5.1 中间代码生成及优化_布尔表达式的翻译

5.2  中间代码生成与优化_布尔表达式的翻译 我们仍然按照语法分析和语义检查时的思路,先讨论表达式的翻译,再处理语句.表达式从概念上来说,可分为算术表达式和布尔表达式,在一些编程语言(例如Java)中对这两者是有严格区分的,算术表达式的结果是整数或浮点数,而布尔表达式的结果是逻辑上的真或假.布尔是英国数学家,由于布尔较早进行了关于"与或非"逻辑运算的研究,为了纪念这位先驱,在Java中引入了关键字boolean,而在C++中引入了bool关键字来表达逻辑上的真或假.C语言中并没有专门