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

6.3.4        为函数调用与返回产生汇编代码

在这一小节中,我们来讨论一下如何为函数调用和函数返回生成汇编代码。函数调用对应的中间指令如下所示:

//中间指令的四元式: < opcode, DST, SRC1, SRC2>

<CALL, 用于接收返回值的变量retVal, 函数名func,  参数列表[arg1,arg2, …,argn]>

让我们先熟悉一下C函数的调用约定CallingConvention,我们需要把参数从右向左入栈(即从argn到arg1依次入栈),不妨记这些参数所占用的总内存为stksize字节。当函数调用返回后,主调函数要负责把这些参数出栈,这可通过形如“addl  stksize, %esp”的汇编指令来实现。{eax, ecx, edx}这3个寄存器要由主调函数负责保存,在产生call汇编指令之前,要对这几个寄存器进行必要的回写操作。而{ebx,esi,edi}这3个寄存器则由被调函数负责保存,UCC编译器会在所有函数的入口处保存这几个寄存器。    
为了加快返回值的传递,我们会尽量把返回值放在寄存器中。在x86平台上,按C标准的约定:

(1) 若返回值为整型,则存于eax寄存器中(我们只考虑32位平台);

(2) 若返回值为浮点型,则存于x87栈顶寄存器中;

(3) 按C的语法要求,返回值不可以是数组类型。如果返回值是1、2或4字节的结构体对象,则存于寄存器eax中,若是8字节,则存于[edx: eax]中。若返回值是其他大小的结构体对象,则C编译器会为函数添加结构体指针作为第1个参数,如下所示。

typedef struct  Data{

int  num[8];      //共32字节

} dt;

dt =  GetData();

//经C编译器处理后,真正执行的函数调用为:

GetData(&dt);

因此,我们可按以下步骤来翻译形如“<CALL, retVal, func, [arg1,arg2, … ,argn]>”的中间指令,对应的C函数调用相当于“retVal =func(arg1, …, argn);”。

(1) 参数从右至左,即从argn到arg1依次入栈;

(2) 对{eax,ecx,edx}这几个寄存器进行必要的回写操作;

(3) 当retVal为结构体对象,且大小不为{1,2,4,8}时,我们要取retVal的地址,并把该地址入栈;

(4) 若func为函数名myadd,则产生形如“call  myadd”的汇编指令;如果func为函数指针fptr,则产生形如“call  * fptr”的汇编指令。

(5) 根据入栈参数所占的内存总和,调整寄存器esp,即生成形如“addl  stksize,esp”的汇编指令,其中stksize代表所有参数总共占用的栈内存大小。

(6) 根据返回值的类型从相应寄存器中获取返回值。对于大小不为{1,2,4,8}的结构体对象,不需要由主调函数来取返回值。由于已把形如“dt = GetData();”的函数调用改为“GetData(&dt)”,在被调函数内部,我们可通过指针对结构体对象dt进行赋值。

例如,对于C程序中的函数调用myadd而言,UCC编译器生成的汇编代码如下所示:

int  myadd(int  a,int  b);

result = myadd(num1,num2);

///////////////对应汇编代码////////////

pushl  num2            //参数num2入栈

pushl  num1            //参数num1入栈

call  myadd             //函数调用

addl  $8, %esp        //所有参数出栈

movl  %eax,  result     //取返回值

在此基础上,我们来看一下用于生成这些汇编代码的函数EmitCall,如图6.3.9所示。

第7至12行把参数从右到左依次入栈,第13至15行调用SpillReg函数对寄存器eax、ecx和edx进行必要的回写。当返回值是大小不为{1,2,4,8}的结构体对象时,我们会在第19行取“返回值接收对象retVal”的地址,然后在第20行将该地址入栈。第23行用于产生函数调用指令,形如“call  myadd”或者“call  * fptr”,第25至28行会把所有参数出栈。

图6.3.9  EmitCall()

当返回值为浮点数时,如果主调函数不需要该返回值,我们要在第31行把x87栈顶寄

存器弹出,以避免x87协处理器的寄存器栈过满。如果主调函数需要浮点数返回值,则通过第35至39行从x87栈顶寄存器中取出返回值,并弹出x87栈顶寄存器。而第40至55行则用于从寄存器eax或edx中获取“整数返回值”或者“大小为{1,2,4,8}的结构体返回值”。

接下来,我们来分析一下图6.3.9第9行调用的函数PushArgument,其代码如图6.3.10第1至24行所示。第3至5行压入float类型的参数,而第6至8行压入double类型的参数,第9至20行用于把结构体对象复制到栈中,第17行的opds[1]记录要复制的字节数ty->size,第18行的opds[2]是在栈中为结构体对象预留的内存大小,opds[2]要大于或等于opds[1]的大小。第21至22行把4字节的整数入栈。

图6.3.10  PushArgument()

在图6.3.10第26行,我们还给出了形如“*ptr = number;”的中间指令,UCC称这样的指令为IndirectMove,我们会在第35行把ptr加载到寄存器中,不妨设其为eax,然后在36至37行把形如“<IMOV,ptr,number,NULL>”的中间指令改为“<MOV,(%eax),number,NULL>”,再通过第19行的EmitMove函数,我们就可以为“*ptr
= number;”产生以下汇编代码,我们已在前面的章节中分析过EmitMove函数,这里不再重复。

movl  num, %ecx

movl  %ecx, (%eax)

与此类似的,图6.3.10第48至58行的EmitDeref函数用来处理形如“t2: *ptr”的中间指令,对应的四元式为<DEREF,t2, ptr, NULL>,我们先在第51行把ptr加载到寄存器中,不妨设其为eax,第52至53行会把中间指令“<DEREF, t2, ptr, NULL>”改为“<MOV,t2,(%eax),NULL>”,之后通过第55行的EmitMove函数产生以下汇编代码:

movl (%eax), %ecx   ;     //临时变量t2对应的寄存器为ecx

当遇到C程序里的return语句时,UCC编译器会产生以下中间代码:

return   retVal;

/////////对应中间代码////////////

<RET, retVal, NULL,NULL>       //中间指令RET只是准备好返回值

<JMP, exitBB,NULL,NULL>       //跳往函数的唯一出口exitBB

在函数的唯一出口exitBB中,UCC编译器会通过EmitEpilogue函数产生以下汇编代码,用于从被调函数返回到主调函数。

exitBB:

movl  %ebp, %esp

popl  %edi

popl  %esi

popl  %ebx

popl  %ebp

ret

因此,中间指令“<RET, retVal, NULL,NULL>”所要完成的工作只是传递返回值,相关代码如图6.3.11所示。当返回值为浮点数且返回值不在x87栈顶寄存器时,我们在第9行调用PutASMCode函数把返回值加载到x87栈顶寄存器中。如果返回值是大小不为{1,2,4,8}的结构体对象,我们通过第31至33行,把形如“<RET,retVal,NULL,NULL>”的中间指令改为“<IMOV,&dt,retVal,
NULL>”,第16至28行的注释对此进行了说明,之后我们可在第34行调用EmitIndirectMove函数进行结构体对象的复制。

图6.3.11  EmitReturn()

图6.3.11第37至55行用于把“整数返回值或者大小为{1,2,4,8}的结构体对象”传送到“寄存器eax或者edx”中。

时间: 2024-08-02 18:19:22

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

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

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

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.5 汇编代码生成_为类型转换产生汇编代码

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

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

6.1 汇编代码生成简介 历经词法分析.语法分析.语义检查和中间代码生成阶段,我们终于来到了"目标代码生成阶段",由于UCC编译器的目标代码即为32位x86汇编代码,因此我们就把本章称为"汇编代码生成".UCC编译器中的大部分源代码都适用于Windows和Linux平台,但Windows平台上缺省的汇编器支持Intel风格的x86汇编代码,而Linux平台默认的汇编器则采用AT&T风格的x86汇编代码,两者在汇编语法上有一些差别,为节省篇幅,我们主要针对Li

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

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

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编译器剖析_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.2.2 中间代码生成及优化_再论符号symbol与公共子表达式

5.2.2    再论符号symbol与公共子表达式 在介绍算术表达式的翻译前,让我们简单重温一下第2.5节中的"图2.5.4 公共子表达式"及"图2.5.5 valueDef和valueUse".为阅读方便,我们再次给出这两张图,更详细的说明请参见第2.5节.对于图2.5.4第2行的a+b,我们会由第7行的中间代码来对a+b进行求值,其结果存于临时变量t1中,之后在第3行中再次遇到表达式a+b时,a和b的值并没有发生变化,我们可在第9行直接把t1赋给变量d.由于我

C编译器剖析_5.2.4 中间代码生成及优化_后缀表达式的翻译

5.2.4 后缀表达式的翻译 在前面的章节中,我们介绍了用于对数组元素和结构体成员进行访问的函数Offset,其接口如下所示,参数addr代表了基地址,参数voff代表可变偏移,而参数coff则代表常量偏移. Symbol Offset(Type ty, Symbol addr,Symbol voff, int coff): 函数Offset的基本想法是产生以下中间代码,我们要先对addr.voff和coff进行相加,得到目标地址(addr+voff+coff),然后再进行"提领Derefere