UCC编译器学习笔记17

UCC编译器学习笔记17,第1张

UCC在处理数组和结构体成员的时候,都是直接转为偏移值了,偏移值是以字节为单位,例如:

int (*ptr)[4] = &arr;

(*ptr)[2] ---> ptr[0][8] -----> (ptr, 8)

ptr 是指向 int[4]数组的指针, C 程序员通过(*ptr)[2]来访问数组元素时,UCC 编译器会在语义检查CheckUnaryExpression 时构造成一棵形如([] ([] ptr 0) 8)的语法树(8是以字节为单位的),在翻译这个语法树时,我们会计算出基地址为 ptr,而偏移为 8,此时我们把这两者相加,再通过间接寻址才能访问到相应的数组元素。

对一个全局变量或静态变量 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 当作数组元素 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]; //假设 sizeof(int)为 4
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 * ptr);
void h3(int (*ptr)[1000]);

总结:

变量名int a;   a表示变量内存空间,&a代表变量的地址

数组名int arr[2];arr按道理说也应该表示所有变量组成的空间,但是不是的,这个依赖于上下文。但是在UCC编译器中,arr代表的是空间,是需要取地址的。

函数名void f();   f按道理说应该代表整个函数所占的空间,但是也不是的,也有可能直接代表空间的首地址,这个也依赖于上下文。但是在UCC编译器中,f代表的是空间,是需要取地址的。例如:

 设置 isfunc 为 0,就能使得不需要为函数名 f 产生取地址指令,直接返回 f 即可。

我们再举个例子来说明一下函数名,对于如下 C 代码,UCC 编译器在遇到“ptr = f;”
中的函数名 f 时要生成一条用于取地址的中间代码“t0:&f”,之后再生成“ptr = t0;”的中间
代码,这样在 UCC 生成汇编代码时就会选用“leal f, %eax”来获取函数 f 的首地址并存于
eax 寄存器中,而如果错误地生成形如“ptr = f;”的中间代码,则对应的汇编指令为“movl
f, %ebx; movl %ebx, ptr;”, 这会错误地把函数 f 代码区的前 4 个字节的内容送到 ptr 中。不
过,按汇编中 call 指令的语法,我们可生成形如“call f”的汇编指令来调用函数 f,因此
在遇到 C 语句“f();”中的函数名 f 时,我们不必为函数名 f 产生取地址指令,直接用返回
函数名 f 即可。
void f(void){
}
void * ptr;
int main(int argc,char * argv[]){
ptr = f;
f();
return 0;
}
// UCC 生成的中间代码和汇编代码如下所示
t0 :&f; // leal f, %eax ; 不可以用 movl f, %eax
ptr = t0; // movl %eax, ptr

f(); // call f

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/874758.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存