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

6.3.5  为类型转换产生汇编代码

在这一小节中,我们来讨论一下整型和浮点型之间的类型转换。有些类型转换并不需要在汇编层次进行数据转换,例如int和unsigned  int之间的转换只是改变了表达式的类型,对数据本身并无影响,以下表达式“(unsigned int) a”对应的二进制数据为0xFFFFFFFF,而表达式“a”对应的二进制数据也为0xFFFFFFFF。但对相同内容的二进制数据来说,进行“有符号整数的右移”和“无符号整数的右移”的结果是不一样的。换言之,对于int和unsigned 
int之间的转换,我们只要在语义检查时,保存转型后的新类型即可,由于其数据本身并没有发生变化,我们不需要产生任何进行数据格式转换的汇编代码。

int   a = -1;

unsigned  int  b;

b = ((unsigned int) a)>> 30;      //b为3

b = a >> 30;                      //b为0xFFFFFFFF

还有一点需要注意的是,UCC编译器总是把低于int的整型先隐式地提升为int,然后再进行其他转换。如下所示,当要进行“short-- > char”的转换时,我们会以int作为中转,实际进行的转换为“short -- > int -- > char”。

short  s = 3;

char  c=(char)s;

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

t0 : (int)(short)s;     // short先提升为int

c = (char)(int)t0;      //再把int截断为char

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

movswl  s, %eax         //两字节的short扩展为4字节的int

movb  %al, c            //只取低字节赋给c

因此,在UCC编译器的类型转换中,“需要通过汇编代码来进行数据格式转换的”主要有以下几种,其中In分别代表占n字节的有符号整数,Un分别代表占n字节的无符号整数,F4代表float,F8代表double。

(1) “I1、U1、I2或者U2”提升为I4

由“单字节或双字节”的整数进行符号位扩展,得到4字节的整数。

C程序员可以进行“U1 -->U2”的转换,但UCC在语义检查阶段已改为

“U1  --> int  -- > U2”的转换。在汇编代码生成阶段,不会遇到“U1 --> U2 ”。

(2) “I4或者U4”截断为I1

只取“4字节的整数”低8位,得到一个单字节的数据。

在汇编层次,“I4或者U4”截断为U1,也是取低8位的数据。

(3) “I4或者U4”截断为I2

只取“4字节的整数”低16位,得到一个双字节的数据。

在汇编层次,“I4或者U4”截断为U2,也是取低16位的数据。

(4) “I4或者U4”转换为“F4或者F8”

通过x87协处理器进行数据格式转换,将整型转换为浮点数。

(5) “F4或者F8”转换为I4或者U4

通过x87协处理器进行数据格式转换,将浮点数转换为整数。

(6) “F4”与“F8”之间互相转换

通过x87协处理器进行数据格式转换,实现float与double之间的转换。

当然,在一些没有浮点协处理器的中低端嵌入式平台上,浮点数运算需要由CPU通过整数运算来得到,这一般会由相应的浮点运算函数库来实现。下面,我们通过一个简单的例子来熟悉一下用于类型转换的汇编代码,如图6.3.12所示。第15行的movsbl指令用于对i1进行符号位扩展,从而得到4字节的int;第18行的movb指令用于传送一个字节的数据,第20行的movw用于传送两个字节的数据。第21至24行用于实现int到float的数据转换,我们在第21行把i4的值入栈,第22行把栈内存中的整数加载到x87中,第23行把经x87转换后的浮点数赋值给f4,第24行进行esp寄存器的调整以保存栈的平衡,这几行汇编代码是由以下模板得来,并非最优,但简单易懂。

TEMPLATE(X86_CVTI4F4,  "pushl %1;fildl (%%esp);fstps %0;addl$4, %%esp")

图6.3.12 类型转换对应的汇编代码

图6.3.12第37行从内存加载double型浮点数到x87协处理器中,第38行把转换后的float数据存到f4中。而第25至36行的汇编代码用于实现float到int的数据转换,其中很大一部分代码用来设置x87的控制寄存器,从而设定“四舍五入”的精度。我们已在“第1.5节 结合C语言来学汇编_浮点数运算”一节的“图1.30”中分析过这些汇编代码,这里不再重复。第25至36行的汇编代码是根据以下模板产生的,其中用到了寄存器eax,因此在产生这些汇编代码前,我们需要先对寄存器eax进行必要的回写操作。

TEMPLATE(X86_CVTF4I4,

"flds %1;subl $16, %%esp;fnstcw(%%esp);movzwl (%%esp), %%eax;"

"orl $0x0c00, %%eax;movl %%eax,4(%%esp);fldcw 4(%%esp);fistpl 8(%%esp);"

"fldcw (%%esp);movl 8(%%esp),%%eax;addl $16, %%esp")

接下来,我们来讨论一下用于生成这些汇编代码的函数EmitCast,如图6.3.13所示。第9至20行用于实现{I1,U1,I2,U2}到I4的整型提升,我们面对的中间指令形如“< EXTI1,DST, SRC1, NULL>”。我们会在第6行把目的操作数DST保存到局部变量dst中,第12行调用AllocateReg函数为DST分配一个4字节寄存器,如果DST不是临时变量,则分配不成功。此时,我们需要在第14行直接调用GetReg函数来得到一个寄存器,并重新设置中间指令里的DST操作数,这会导致“DST
!= dst”。不论如何,在第16行,我们已得到一个寄存器,不妨记为Rx,第16行通过形如movsbl的汇编指令进行符号位扩展,并把结果存到Rx中。如果“原先的目的操作数dst”不是临时变量,则我们还要在第18行把寄存器Rx中的值传送给dst,最终可得到形如“movsbl  i1, %eax; movl  %eax, i4”这样的汇编代码。

图6.3.13 EmitCast()

图6.3.13第21至30行用于实现{I4,U4}到I1的截断操作,我们面对的中间指令形如

“< TRUI1, DST, SRC1, NULL>”。如果SRC1的值已经在寄存器中,不妨设其为eax,则我们可在第23行得到eax的低8位对应的寄存器al;否则,我们在第26行获取一个单字节寄存器,不妨设其为al,第27行会把“占4字节的操作数SRC1的值”传送到寄存器eax中,由于eax寄存器的低8位就是寄存器al,因此我们可在第30行把al的值传送给目的操作数DST,最终可产生形如“movl
 i4, %eax;movb %al, i1;”的汇编代码。而{I4,U4}到I2的截断操作与此类似,我们在第32行省略了相关代码。

图6.3.13第33至39行用于实现整型到浮点型的转换,我们在第38行调用PutASMCode函数,根据相应的模板来产生汇编代码即可。第42至50行用于实现浮点型到整型的转换,我们面对的中间指令形如“<CVTF4I4, DST, SRC1, NULL>”,其中DST为整数,而SRC1为浮点数。由于在X86_CVTF4I4模板中用到了寄存器eax,因此我们要在第44行调用SpillReg函数对eax进行必要的回写操作,第45行为DST分配一个4字节寄存器,不妨记为Rx,第46行根据X86_CVTF4I4等指令模板产生相应的汇编代码,实现浮点数到整数的转换,结果保存在寄存器eax中。如果中间指令里的操作数DST不是临时变量,或者DST是临时变量但所分配的寄存器不是eax,则我们要在第49行把eax寄存器中的值传送给DST。

时间: 2024-08-09 19:53:49

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

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

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

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

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

C编译器剖析_5.3.1 中间代码生成及优化_If语句和复合语句的翻译

5.3.1   If语句和复合语句的翻译 我们先简单回顾一下对布尔表达式的翻译,我们通过调用TranslateBranch函数来产生跳转指令,从而实现布尔表达式的语义.在使用函数TranslateBranch(expr, bt, bn)时,有这么两个约定: (1) 当expr为真时,跳往bt基本块: (2) 紧随"函数TranslateBranch所生成的跳转指令"之后的基本块为bn. 在表达式的基础上,我们来讨论一下语句的翻译.图5.3.1用于if语句的翻译,第4至8行说明了如何翻译