选择语句的机器级表示
条件运算表达式的机器级表示
C 语言中唯一的三目运算符是由符号 ?
和 :
组成的。它可以构成一个条件运算表达式。这个条件运算表达式的值可以赋值给一个变量。其通用形式如下:
x=cond_expr ? then_expr : else_expr;
对应的机器级代码可以使用比较指令,条件传送指令或条件设置指令。
if ~ else 语句的机器级表示
if ~ else 选择结构根据判定条件来控制一些语句是否被执行。其通用形式如下:
if (coond_expr)
then_statement
else
else_statement
通常,编译后得到的对应汇编代码如下:
// 否定跳转
c=cond_expr;
if (!c)
goto false_label;
then_statement
goto done;
false_label:
else_statement
done:
// 正确跳转
c=cond_expr;
if(c)
goto true_label;
else_statement
goto done;
true_label:
then_statement
done:
细节:对于条件转移指令,它在条件满足时会跳转到其他地方执行,因而会破坏程序既定的执行流程,这在用流水线方式执行的情况下,就会破坏流水线的执行,导致流水线停顿,从而影响程序的性能。但用条件传送指令来代替条件转移指令是否更好?
条件传送指令在满足条件时,将源数据送到目的地,否则什么也不做。也就是该指令执行完后,CPU 还是继续执行它后续的指令,不会改变程序既定的执行流程,因而不会破坏指令流水线的执行,这样看来好像使用条件传送指令比使用条件转移指令更好。
实际上,条件传送指令并不比条件转移指令好,不建议使用条件传送指令来代替条件转移指令。这是因为条件传送指令会使指令之间的依赖性加大,因此在乱序执行指令时使用传送指令反而会降低程序的执行效率。
这是因为现代处理器的微结构实现中有一个分支预测器,在绝大多数情况下能保证条件转移指令不会破坏流水线的执行,所以大部分情况下条件转移执行的开销很低。
switch 语句的机器级表示
使用 if ~ else 语句只能按顺序一一测试条件,而用 switch 语句来实现多分枝选择功能,它可以直接跳到某个条件处的语句执行,而不用一一测试条件。
循环结构的机器级表示
do ~ while 循环的机器级表示
do
{
loop_body_statement
}while(cond_expr);
汇编如下:
loop:
loop_body_statement
c=cond_expr;
if(c) goto loop;
while 循环的机器级表示
while(cond_expr)
loop_body_statement
c=cond_expr;
if(!c) goto done;
loop:
loop_body_statement
c=cond_expr;
if(c) goto loop;
done:
for 循环的机器级表示
for(begin_expr; cond_expr; update_expr)
loop_body_statement
begin_expr;
c=cond_expr;
if(!c) goto done;
loop:
loop_body_statement
update_expr;
c=cond_expr;
if(c) goto loop;
done:
复杂数据类型的分配和访问
在机器级代码中,基本类型对应的数据通常通过单条指令就可以访问和处理,这些数据在指令中或者是以立即数的方式出现,或者是以寄存器数据数据的形式出现,或者是以存储器数据的形式出现;而对于构造类型的数据,由于器包括多个基本类型数据,因而不能直接用单条指令来访问和运算,通常需要特定的代码结构和寻址方式对其进行处理。
数组的分配和访问
数组可以将同类基本类型数据组合起来形成一个大的数据集合,因而不能放在一个寄存器中或者作为立即数存放在指令中,他一定被分配在存储器中 ,数组中的每个元素在存储器中连续存放,可以用一个索引值来访问数据元素。
数组的存储分配和初始化
数组可以定义为静态存储型(static),外部存储型(extern),自动存储型(auto),或者定义为局部静态区数组。其中,只有 auto 型数组被分配在栈中,其他存储型数组都分配在静态数据区。
数组的初始化就是在定义数组时给数组元素赋初始值。
因为在编译,链接时可以确定在静态区中的数组的地址,所以在编译,链接阶段就可以将数组首地址和数组变量建立关联。对于分配在静态区的已初始化的数组,机器级指令中可以通过数组首地址和数组元素下标来访问相应的数组元素。对于 auto 型数组,由于被分配在栈中,因此数组首地址通过 ESP 或 EBP 来定位,机器级代码中数组元素地址由首地址与数组元素的下标值进行计算得到。
数组和指针
C 语言中指针与数组直接的关系十分密切,他们均用于处理存储器中连续存放的一组数据,因而在访问存储器时两者的地址计算方法是统一的,数组元素的引用可以用指针来实现。
指针数组和多维数组
指针数组中每个元素都是指针,每个元素指向的目标数据类型都相同。一个指针数组可以实现二维数组。
结构体数据的分配和访问
C 语言的结构体可以将不同类型的数据结合在一个数据结构中。组成结构体的每个数据称为结构体的成员或字段。
结构体成员在存储空间的存放和访问
结构体中的数据成员存放在存储器中一段连续的存储区中,指向结构的指针就是其第一个字节的地址。编译器在处理结构型数据时,根据每个成员的数据类型获得相应的字节偏移量,然后通过每个成员的字节偏移量来访问结构成员。
结构体数据作为入口参数
当结构体变量需要作为一个函数的形式参数时,形式参数和调用函数中的实参应该具有相同的结构。和普通变量传递参数的方式一样,他也有按值传递和按址传递两种方式。
如果采用按值传递,则结构的每个成员都要被复制到栈中的参数区,这增加了时间和空间的开销,因而对于结构体变量通常采用按址传递的方式。
采用按址传递,只需要把相应结构体的首地址存到栈的参数区中,就能够访问到整个结构体中的成员。
数据对齐
对于底层机器级代码来说,它能够支持任意地址访问存储器数据的功能,因此无论数据是否对齐,IA-32 都能正确工作。只是为了让程序的执行效率更高,采用了数据对齐模式。
1. 按照基本类型的长度进行对齐
int 型数据长度是 4 字节,因此规定 int 型数据的地址是 4 的倍数,其他类比,char 型数据则无需对齐。
微软 Windows 采用的就是这种对齐策略(具体参考 Windows 的 ABI 规范)。在这种情况下,对于 8 字节宽的存储器机制来说,所有基本类型数据都仅需要访存一次。
2. 按规定的长度进行对齐
例如 Linux 采用的对齐策略,除了数据长度小于 4 字节的数据类型,其他的数据类型的地址都是 4 的倍数。例如 short 数据的地址是 2 的倍数,int 是 4 的倍数,double 也是 4 的倍数。对于 8 字节宽的存储器机制来说,double 型数据就可能要进行两次存储器访问。
- 对于扩展精度浮点数,IA-32 中规定长度是 80 位,即 10 字节。为了使随后的相同类型的数据都能够落在 4 字节地址边界上,i386 System V ABI 规范定义 long double 型数据长度为 12 字节。
- 对于结构体数据类型,对齐方式有如下规则:① 整个结构体变量的对齐方式与其中对齐方式最严格的成员相同。② 每个成员在满足其对齐方式的前提下,取最小的可用位置作为成员在结构体中的偏移量,这可能导致内部插空。③ 结构体大小应为对齐边界长度的整数倍,这可能导致尾部插空。前两条规则是为了保证结构体中的任意成员都能以对齐的方式访问。