标签:c编译器 语义检查 提领运算dereference解引用
在这一小节中,我们来讨论一元运算符表达式的语义检查,与其相关的代码如图4.2.35所示。对于“前加加”和“前减减”运算符而言,我们采取的策略跟处理“后加加”和“后减减”一样,都是将--a转换为a -= 1,而将++a转换为a += 1,所以图4.2.35第5行调用的函数,就是我们在讨论后缀表达式语义检查时介绍过的函数TransformIncrement()。对于形如+a或-a的表达式,我们需要检查一下a是否为算术类型,由于表达式+a的结果存放在一个临时变量中,所以+a是个右值,此时表达式+a的值即为a,第14行的Adjust()函数把a所对应的语法树结点视为右值;而对于-a,如果a对应一个常数,比如(3.0+4.0),那我们在编译时就可以进行-(3.0+4.0)的计算从而得到-7.0,第18行的FoldConstant()完成了这个常量折叠的操作。若操作数a的类型小于int型,则我们通过第16行的DoIntegerPromotion()进行整型提升,由第17行我们置表达式+a的类型为“子表达式a的类型”。对于按位取反运算符~来说,按C的语义规则,表达式~a中操作数a的类型应为整型,第21至28行的其余代码不难理解,这里不再啰嗦。
图4.2.35 CheckUnaryExpression()
而对于逻辑非运算符!来说,与之对应的代码在图4.2.35第29至35行。表达式!a中的子表达式a的类型应是标量类型,即整型、浮点型和指针类型,而数组类型通常被视为向量类型,按数学上的定义,向量相当于是一个多维空间中的坐标(x1,x2,….,xn),而标量只是一维空间上的坐标x。结构体类型也不是标量类型。图4.2.35第31行的IsScalarType()用于判断某类型是否为标量。当然,逻辑非!a的运算结果应为布尔型,但C语言中并没有布尔型,而是用0和非0来表示,这对应的是int型,第32行我们置!a对应的语法树结点为int型。
对于一元运算符sizeof而言,图4.2.35第36至42行的代码用于处理形如sizeof(a+b)的一元表达式,此时sizeof运算符的操作数是一个表达式,但按C标准的规定,结构体中的位域成员不可以作为sizeof的操作数,第39至41行对此进行了检查;而第43至45行的代码用于处理形如sizeof(int *)的表达式,此时sizeof的操作数为一个类型名,第44行的CheckTypeName()用于对类型名进行语义检查,我们会在讨论declchk.c时对这个函数进行分析,其函数返回值是一个指向struct type对象的指针,该structtype对象中就包含了类型信息,例如该类型的变量要占多大内存。整个sizeof表达式应是个编译时的无符号整数常量,第50至52行完成了这些设置。
接下来我们来看一下图4.2.35第55行的CheckTypeCast()函数,该函数用于对形如(int)a这样的强制类型转换表达式进行语义检查,其代码如图4.2.36所示。在图4.2.36第7行,我们调用CheckTypeName函数来对(int) a中的类型名int进行语义检查,从而得到与类型名对应的struct type对象;第9行则通过调用Adjust()函数来对操作数a进行必要的类型调整。第11至14行,我们引用了来自C标准文档ucc\ansi.c.txt中的一段语义规则,由此可知,形如(T) expr的强制类型转换表达式中,T的类型和expr的类型都得是标量类型,不可以是数组类型或结构体类型,当然T可能是void,第16至19行的代码实现了这些语义规则。第20行调用Cast函数来进行类型转换,我们已在之前的章节中分析过Cast()函数。
图4.2.36 CheckTypeCast()
在图4.2.35中,我们还需要在第6行检查形如&a的取地址运算,或者在第9行处理形如*ptr的“提领”操作,由于这些代码相对复杂,我们没有在图4.2.35中给出。接下来我们先举个例子来说明一下形如*ptr的提领运算,如图4.2.37所示。
图4.2.37 提领运算Dereference
虽然在图4.2.37第8至10行,我们都是用*运算符来表达C语言的提领运算Dereference。但生成的汇编代码中,是否需要进行间接寻址,则要看操作数的类型。换言之,语法上相似的表达式,如**arr,**ptr和**ptr2,由于arr、ptr和ptr2等操作数在类型上的差别,导致其经语义检查后生成的语法树也有较大差别,其内存寻址模式也是不一样的。
例如对于图4.2.37第8行的**arr = 1而言,我们不需要进行任何的间接寻址,与其对应的汇编指令为第40行的movl $1, arr+0,在链接时,arr就相当于一个地址常量。第8行的赋值运算的左操作数**arr,对应的内存地址就是arr+0。所以在**arr经语义检查后得到的语法树([] ([] arr 0)0)中,并没有出现提领运算*,而是数组索引运算符[],在生成中间代码时,对于数组索引运算符,我们实际上进行的运算主要是加法,此处只要进行arr+0+0,就可得到左操作数的地址。我们做这些决策背后的依据是,arr对应的类型是数组类型int [3][4],而不是指针类型。
由图4.2.27第6行我们可知,ptr2是个指针类型int **,并且不是指向数组的指针。对于图4.2.37第10行的**ptr2 = 3来说,我们需要进行两次的间接寻址,才能得到左操作数的内存地址,其汇编代码如图4.2.37第43至45行所示。第43行把ptr2对应内存单元的内容送到寄存器eax中,第44行取寄存器eax所指向内存单元的内容,存到寄存器ecx中,这里我们做了一次寄存器间接寻址;第45行把立即数3存到ecx所指向的内存单元中,这里我们又做了一次寄存器间接寻址。寄存器间接寻址在AT&T汇编代码上的语法特征形如(%eax),表示寄存器eax中存放的是某个内存单元的地址。语义检查后,与**ptr2对应的语法树为第24行的(*(* ptr2)),此时在语法树上出现的*运算符,表示我们确实需要进行提领运算,汇编指令通过间接寻址来实现提领运算。
对于图4.2.37第9行的**ptr = 3,由第3和第4行的声明我们可知,ptr的类型是“指向数组int[4]”的指针。语义检查后,我们为**ptr生成的语法树是([] ([] ptr 0) 0),这和**arr经语义检查后的语法树([] ([] arr 0) 0)相似。由于ptr是指向数组的指针类型,而arr是数组类型,我们在中间代码生成时,会为([] ([] ptr 0) 0)和([] ([] arr 0)0)生成不同的代码。因此,与**ptr对应的汇编代码为第41和42行,第41行的“movl ptr,%eax”用于把ptr对应内存单元的内容送到寄存器eax中,第42行的“mov $2 (%eax)”通过寄存器间接寻址,把立即数2送入eax所指向的内存单元,这里我们做了一次间接寻址操作。
简而言之,对于形如*p的表达式,“如果p是数组类型,或者*p是数组类型”,语法分析后我们得到的语法树为(* p),但是在语义检查后,我们为*p构造的语法树为([] p 0)。稍作推广一下,对于形如*(p+i)的表达式,语法分析后对应的语法树为(* (+ p i)),“如果p是数组类型,或者*(p+i)是数组类型”,我们可将其转换为p[i]来处理。在语义检查后,我们为之构造的语法树为([] p k),其中k为i*sizeof(*p)。例如对intarr[3][4]来说,表达式*(arr+1)经语义检查后对应的语法树为([] arr 16),其中16源于1*sizeof(*arr),即1*sizeof(int[4])。函数CheckUnaryExpression中,与Dereference运算符相关的代码如图4.2.38所示。
图4.2.38 CheckUnaryExpression_case_OP_DEREF
图4.2.38第14至22行的代码用于把表达式*(arr+i)对应的语法树(*(+ arr i) ),转换为([] arr k)来处理,由于我们是后序遍历语法树,所以由i*sizeof(*arr)得到k的计算会在访问运算符+结点时进行,而访问完+运算符结点后,我们才会去访问其父结点,即此处的*运算符结点。第31至39行的代码则用于把表达式*ptr的语法树(* ptr)转换为([] ptr 0)。而第10至13行的代码则用于把形如*(&a)的表达式转换为a,第27至30行的代码则用于把形如(*f)()的函数调用转换为f()。
接下来,我们来看一下取地址运算符&。我们结合一个简单的例子来说明一下数组名在不同场合的微妙的语义区别。例如,对数组int arr[3][4]而言,在符号表中存放的标识符arr的类型为数组类型int [3][4]。在表达式(arr+1)中,arr对应的语法树结点的类型,在CheckPrimaryExpression函数中,经查找符号表后会被置为int [3][4]类型。由于arr结点是二元运算符+的左操作数,在对二元运算符进行语义检查时,我们会在CheckBinaryExpression函数中,调用Adjust()函数来对左子树进行类型调整,从而把arr结点的类型从int [3][4]调整为int (*)[4],这就是我们平时在C语言中所说的“数组名arr代表的是数组第0个元素arr[0]的首地址”。严格说来,这句话并不够准确,在符号表中,符号arr的类型始终都是数组类型int [3][4],至于arr对应的语法树结点的类型,则要看arr所处的表达式上下文,在(arr+1)中,arr结点的类型会被调整为int (*)[4]。但是在表达式&arr中,按C的语义,我们不需要对arr结点的类型进行调整,即不需要调用UCC编译器中的Adjust()函数,所以表达式&arr的类型为int(*)[3][4],即指向“数组int [3][4]”的指针类型。上机做个小实验就会发现arr+1和&arr+1的值是不一样的。
printf("%p %p %p\n",arr,arr+1,&arr+1);
//实验结果 804a060 804a070 804a090
其原因是,表达式arr+1中arr结点的类型被调整为为int (*)[4]。按指针运算的语义,T * 类型的指针ptr进行(ptr+1)的运算,其含义是指向下一个T类型的对象,这意味着在汇编代码层次,真正执行的加法运算为(ptr + sizeof(T))。此处类型T为int[4],而sizeof(int[4])为16,写成十六进制即为0x10。而对表达式&arr+1中的arr而言,其所处的子表达式为&arr,子表达式&arr的类型为int (*)[3][4],此处T为int [3][4],sizeof(T)为48,对应十六进制的0x30。若数组arr的首地址为0x804a060,则arr+1的值为0x804a070,而&arr+1的值为0x804a090。由此,我们对“第1.5节 结合C语言学汇编”时,我们讨论过的数组名做了进一步解释。
与取地址运算符&其相关的代码如图4.2.39所示。可以看到,在图4.2.39第6行中,我们并没有调用Adjust()函数进行类型调整。第8至11行用于把形如&(*ptr)的表达式转换为ptr来处理,由于&(*ptr)是个右值,所以我们要把转换后得到的ptr结点也当右值来处理,因此在第10行置lvalue域为0。而第12至18行则用于把&arr[i]的语法树转换为
(+ arr k)来处理,由于是后序遍历,由i进行i*sizeof(*arr)从而得到k的运算,已经在处理子树arr[i]时完成。只有操作数a是左值时,我们才能进行&a的运算,左值意味着对C程序员而言,该单元是可寻址的。如果&a中的结点a对应的是函数名,由于我们在CheckPrimaryExpression函数中,已把该结点的lvalue域置为0,所以这里需要在第19行进行特殊判断一下。当然,按C的语义,C程序员不可以对结构体中的位域成员和被声明为register的变量进行取地址运算,第20行对此进行了判断。由于表达式&a是右值,所以第23行把&a对应结点的lvalue域置为0,第24行完成了对其类型的设置。
图4.2.39 CheckUnaryExpression_case_OP_ADDR
至此,我们完成了对一元运算符表达式的语义检查,在后续章节中,我们会对二元运算符表达式进行语义检查。
C编译器剖析_4.2 语义检查_表达式的语义检查(6)_一元运算符表达式
标签:c编译器 语义检查 提领运算dereference解引用
原文地址:http://blog.csdn.net/sheisc/article/details/44261377