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,number,NULL>  // t2: &number;

由于在一条x86汇编指令中,最多只允许出现2个操作数,而中间指令“DST:SRC1+SRC2”有3个操作数,我们需要产生多条x86汇编指令来实现该中间指令。在汇编指令中,整数加法运算对寄存器没什么特别要求,我们可按以下步骤来处理:

(1) 调用AllocateReg函数依次为SRC1、SRC2和DST分配寄存器。DST是用于保存运算结果的临时变量,必然可分配到一个寄存器,不妨记为R0。而如果SRC1和SRC2不是临时变量,则没有分配到寄存器。

(2) 若DST和SRC1对应的寄存器不一样,我们可产生一条movl指令,把SRC1的值传送到寄存器R0中。

(3) 产生加法指令,进行SRC2和R0的加法,并把结果存于寄存器R0中。

按这样的思路,我们可为“t1 : a+b;”产生以下汇编代码:

movl  a,  %eax

addl  b,  %eax

而形如“t2: &number”的中间指令只有两个操作数,在上述第(1)步中,我们就不必为

SRC2分配寄存器,其他的步骤类似,我们可为“t2: &number”产生以下汇编代码:

leal  a,  %ecx

不过,有些x86汇编指令对寄存器有特定的要求,比如整数的乘法运算就要求源操作数SRC1的值被加载到寄存器eax中。而整数的除法运算或者取余运算,要求源操作数SRC1的值被加载到eax中,如果要进行的是有符号数的除法运算,则寄存器edx的所有位都被设置为SRC1的符号位;如果要进行的是无符号数的除法运算,则寄存器edx被置为全0。例如我们可为中间指令“t3: a
/b;”产生以下汇编代码,其中a和b为有符号整数。

movl a, %eax   //把SRC1加载到eax

cdq            //把符号位扩展到edx寄存器

idivl  b    //进行除法运算[edx: eax] / SRC2,商存于eax,余数存于edx,

//此时eax中的值就是临时变量t3的值

在x86“左移或右移”的汇编指令中,如果要把左移或右移的位数存放于寄存器中,则必须使用单字节寄存器cl,如下所示:

int  a, c;  char  len = 3;

c = a <<len;

////中间代码//////////////

t4 : (int)(char)len;       //把char提升为int

t5 : a << t4;

c = t5;

对应的汇编代码如下所示,我们可以看到,在汇编指令“shll  %cl,%edx”中,我们是用单字节寄存器cl来存放操作数len的值。

movsbl  len,  %eax      // t4 : (int)(char)len;

movl  %eax,  %ecx       //t5: a << t4

movl  a,  %edx

shll  %cl,  %edx

movl  %edx,  c          // c = t5;

有了这些基础后,我们可以来讨论一下“为算术运算生成汇编代码”的函数EmitAssign,如图6.3.5所示。第47至56行用于处理对寄存器没有特别要求的二元运算,形如“DST:SRC1+SRC2;”。第44至45行用于处理形如“DST: ~SRC1”的一元运算,此时我们不必为SRC2分配寄存器。第33至43行用于为左移或右移指令里的SRC2分配寄存器ecx,并在第39行把SRC2加载寄存器ecx,之后在第41行把SRC2改为单字节寄存器cl,即4字节寄存器ecx的低8位。

图6.3.5  EmitAssign()

图6.3.5第8至32行用于为整数乘法、除法和取余运算产生汇编指令,第11至16行把SRC1的值暂存于寄存器eax中,第17行把edx寄存器的值回写到内存中,我们会在[edx:eax]中存放经符号位扩展后的SRC1。第19至24行用于为SRC2分配必要的寄存器,第26行产生汇编指令来进行乘法或除法运算,之后寄存器eax中存放的是“乘法运算的结果”或者“除法运算的商”,而寄存器edx中存放的是“除法运算的余数”,第27至31行用于记录“存放运算结果的临时变量DST对应的寄存器为edx或者eax”。

由于浮点数运算的汇编指令与整数不同,我们需要在图6.3.5第4行调用EmitX87Assign函数,来对浮点数算术运算进行处理。我们先举一个例子来说明,对于形如“DST: SRC1+SRC2”的二元浮点数运算来说,我们可按以下步骤来生成汇编代码:

(1)若SRC1不在x87栈顶寄存器中,则先对x87栈顶寄存器进行必要的回写,然后把SRC1加载到x87栈顶寄存器中。

(2)对x87栈顶寄存器与SRC2进行加法运算,结果存于x87栈顶寄存器中。

按这个思路,我们可为浮点数中间指令“t6: d+e”产生以下汇编代码:

flds  d     //把浮点数d从内存加载到x87栈顶寄存器

fadds  e    //完成加法运算后,x87栈顶寄存器即为t6的值

而对于形如“DST:-SRC1”的一元运算来说,我们在上述第(2)步中,只要对x87栈顶寄存器进行一元运算即可,运算后的结果仍存于x87栈顶寄存器中。例如,我们可为“t7: -d”产生以下汇编代码。

flds  d     //把浮点数d从内存加载到x87栈顶寄存器

fchs        //把符号位取反,x87栈顶寄存器即为t7的值

现在,我们可以来分析一下“用于产生这些浮点运算汇编代码”的函数EmitX87Assign,如图6.3.6所示。如果SRC1还未加载到x87栈顶寄存器中,我们就通过第5行进行必要的回写,然后在第6行把SRC1从内存加载到x87栈顶寄存器中。如果SRC1的值已经在x87栈顶寄存器中,此时SRC1必为临时变量,若SRC1还要在后续的中间指令中被使用,我们需要在第10行把SRC1的值回写到内存中,因为进行浮点运算后,栈顶寄存器保存的是DST的值,而非SRC1的值。如果操作数SRC2就是SRC1,由于遇到形如第13行的模板“faddl 
%2”时,我们要把x87栈顶寄存器与位于内存中的SRC2进行加法运算,而此时SRC2就是SRC1,因此我们也要在第10行把SRC1的值回写到内存中。第15行根据汇编指令模板,调用PutASMCode函数产生浮点数运算的汇编指令。

图6.3.6  EmitX87Assign()

时间: 2024-11-05 16:02:45

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

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编译器剖析_5.4.2 中间代码生成及优化_基本块的合并

5.4.2  基本块的合并 我们在第5.4.1节时给出了由基本块构成的双向链表和控制流图,为阅读方便,我们这里再次给出"图5.1.4 基本块的静态结构和动态结构".在这一小节中,我们试图把双向链表中相邻的基本块进行合并,当然这种合并需要满足一定条件,同时要保持程序的原有语义.在合并后,控制流图中的前驱与后继关系也要进行调整.我们需要改动的数据结构有图5.4.1中的双向链表和控制流图.需要注意的是,虽然基本块BB5和BB6在双向链表中相邻,但控制流却不会由BB5流入BB6,双向链表只是维

C编译器剖析_5.3.2 中间代码生成及优化_switch语句的翻译

5.3.2.Switch语句的翻译 在这一小节中,我们来讨论一下switch语句的翻译,switch语句的产生式如下所示. SwitchStatement: switch( expr ) statement 当C程序员编写出如下代码时,UCC编译器会在语义检查阶段进行报错"error:The  break shall  appear  in  a  switch  or loop",从语法上来看,以下"case  3: b = 30;"是case语句,而"

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> ////////对应的汇编代码////////