第25行用于在堆空间中分配一个符号对象,第30行设该符号在C源代码中的坐标,第31行置标志位addreesed为1,表示该对象被进行过“取地址操作”(这样,数组元素和结构体成员作表达式中的操作数时,该表达式就不再被当作公共子表达式,我们在第5.2节介绍过相关概念),第32行设置该符号对象的类型为SK_Offset,第33行设置其类型,第34行保存其基地址对应的符号对象,第35行存放常量偏移,第36行用于生成形如“arr[8]”的符号名。当第18行的参数base本身就对应一个数组元素或者“结构体成员”时,例如dt.num[2]中的dt.num,对于dt.num来说,其基地址为dt,按图5.2.9第4至7行的定义,dt.num在结构体对象dt中的偏移为4,而数组元素dt.num[2]在数组dt.num中的偏移为8,在中间代码层次,我们可以把两者相加,得到dt.num[2]在结构体对象中的偏移为12,图5.2.10第26至29行的代码用于完成这些操作。
图5.2.10第40行的函数Deref主要用来生成一条形如“t3:*t2”的间接寻址指令,其中t2中存放的一个地址,*t2表示取“这个地址对应内存单元中的内容”,并把该内容存于临时变量t3中,符号t3就作为“间接寻址”的结果返回。当然如果第40行的参数addr形如第44行的t1,而t1由中间代码“t1:&arr”创建,则间接寻址操作*t1可简化为对arr的访问。
而图5.2.10第52行的函数AddressOf用于在必要时生成形如“t:&num”的取地址指令,其中的num是应是左值(具有C程序员可见的地址)。如果第52行的参数p是进行“间接寻址”后所得的结果t3,其中t3对应的间接寻址指令为“t3: *t2”,则“取地址操作&t3”可简化为t2,第52至57行的if语句对此进行判断,此时直接返回t2即可。当num被取地址后,UCC通过调用第61行的TrackValueChange函数,来使以num为操作数的公共子表达式失效。UCC用这样的策略避免了“别名分析”这样的复杂过程,当然这会影响生成代码的质量,UCC编译器在优化上做得还不够。由于num在其生命周期内的地址是不会变化的,所以对num进行取地址后的值就可以作为公共子表达式使用,第63行调用的TryAddValue用于此目的。
对一个全局变量或静态变量number来讲,我们可以这样来理解出现在C程序中的符号number。在C代码中,我们可把符号number理解为“number相应内存单元中的内容,number位于赋值号右侧,则对该内存单元进行读操作;而number位于赋值号左侧时,则对该内存单元进行写操作”。C程序员如果要获取该内存单元的地址,则使用表达式&number。
// C代码,number对应全局静态区中的一个内存单元
number = 30; //number位于赋值号左侧,表示要改写number的内容
a = number; //number位于赋值号右侧,表示要读取number的内容
但在汇编代码层次,我们可以把符号number看成是地址常数,在请求分页的操作系统中,连接器最终会为全局变量和静态变量分配一个虚空间中的内存单元,相当于把汇编代码中的符号number替换为一个地址常数。如果要访问相应内存单元的内容,则使用如下movl指令;而如果要获取该内存单元的地址,可使用leal指令,如下所示。
// 若全局变量number的地址为0x804a060
movl number, %eax; //寄存器eax中的内容为30
leal number, %ebx; //寄存器ebx中的内容为0x804a060
如果number只是个局部变量,由于其存储空间位于栈中,是动态分配的,其符号名number根本就不会出现在汇编代码中,而是用形如“-4(%ebp)”这样的符号来表示,其中寄存器ebp在运行时会指向栈空间,在编译时,我们只能算出局部变量number在栈中的偏移,其基地址是未知的,运行时会由寄存器ebp来指向。
当然在C语言中,数组名是个特例,按照我们前面的理解,在C语言层次,符号arr就应代表数组的内容。但C编译器会根据上下文来对数组名进行不同的处理,这会造成语义上的不一致。这也是数组名给不少C程序员带来诸多困惑的源头,例如arr和&arr到底有何区别之类。对以下数组arr来说,在符号表中,符号arr的类型始终都是int[4]的数组类型,但当符号arr被用在不同场合时,其对应表达式的类型并不一致。
int arr[4];
(1) sizeof(arr) 的值为16,其中的表达式arr为数组类型int[4];
(2) arr+1,这里的arr被当成数组第0个元素的地址,而arr[0]的类型为int,则&arr[0]的类型为 int *,所以此处表达式arr的类型也为int *
(3) &arr +1, 其中表达式arr的类型为数组类型int[4];
而&arr是指向数组int[4]的指针类型,即int(*)[4]。
我们可以大胆地猜测,C的设计者是出于运行时效率的考虑,才会在一些情况下“把
数组名arr当作第0个数组元素arr[0]的首地址”。例如,在以下函数调用“f(bigArr)”中,若符号bigArr代表的是数组的内容,则在传参时我们需要传递4000字节的数据,这要占用较多的栈空间,同时大量数据的复制也要耗费不少时间。此时,若由C编译器把f(bigArr)中的bigArr当作bigArr[0]的地址,则只要传递一个地址就可以了,同时函数f的形参int num[1000]也可由C编译器隐式地调整为int * num。但是这并不能完全阻止C程序员传递数组的内容,C程序员还是可以写出如下struct Container,通过给函数k传递一个struct Container对象,C编译器还是会复制其中的数组data。
int bigArr[1000];
void f(int num[1000]){
}
void g(void){
f(bigArr);
}
struct Container{
int data[1000];
};
void k(struct Container d){
}
?
如果从语义一致上的角度出发,在C语言层次,让数组名bigArr代表数组中的内容其实也是很好的设计,这或许还更符合“提供机制,而非策略”的思想,C编译器提供传参的各种机制,至于C程序员要选用哪一种,也许由C程序员根据应用的上下文来决定会更好些,如下函数声明h1、h2和h3所示。这可能与设计上的审美有关,不过,当一个决定已成了标准,我们就要严格遵守。
void h1(int arr[1000]);
void h2(int * arr);
void h3(int (*ptr)[1000]);
理解了图5.2.10中的Offset等函数后,由于处理完了“寻址”的问题,我们再来看
tranexpr.c中的表达式翻译就会轻松许多。在下一小节中,我们将对tranexpr.c中用于翻译结构体成员访问的函数CheckMemberAccess,及用于翻译数组元素访问的函数CheckPostfi