控制
以上操作,我们只考虑了直线代码的行为,即指令按顺序执行。但是还有一些代码,比如条件语句、循环语句和分支语句,要求有条件的执行,这时需要根据数据测试的结果来改变条件码,结合跳转指令决定操作执行的顺序。
-
条件码
除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,它们描述了最近算数或逻辑操作的属性。常用的条件码有:
-CF:进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出。
-ZF:零标志。最近的操作得出的结果为0。
-SF:符号标志。最近的操作得到的结果为负数。
-OF:溢出标志。最近的操作导致一个补码溢出——正溢出或负溢出。图3-7中除了leal指令都会设置条件码。还有两类指令,只设置条件码而不改变任何其他寄存器,CMP指令与SUB指令行为相似,TEST指令与AND指令行为相似,见下图:
-
访问条件码
条件码通常不会直接读取,常用的使用方法有三种:- 可以根据条件码的某个组合,将一个字节设置为0或者1;
- 可以条件跳转到程序的某个其他的部分;
- 可以有条件地传送数据。
对于第一种情况,图3-11中描述的指令根据条件码的某个组合,将一个字节设置为0或者1。
一条SET指令的目的操作数是8个单字节寄存器元素之一,或是存储一个字节的存储器位置,将这个字节设置成0或者1。为了得到一个32位结果,我们必须对最高的24位清零。以下是一个计算表达式<nobr><span class="math" id="MathJax-Span-2989" style="width: 2.883em; display: inline-block;"><span style="display: inline-block; position: relative; width: 2.296em; height: 0px; font-size: 125%;"><span style="position: absolute; clip: rect(1.869em 1000em 2.936em -0.424em); top: -2.717em; left: 0.003em;"><span class="mrow" id="MathJax-Span-2990"><span class="mi" id="MathJax-Span-2991" style="font-family: MathJax_Math-italic;">a</span><span class="mo" id="MathJax-Span-2992" style="font-family: MathJax_Main; padding-left: 0.269em;"><</span><span class="mi" id="MathJax-Span-2993" style="font-family: MathJax_Math-italic; padding-left: 0.269em;">b</span></span><span style="display: inline-block; width: 0px; height: 2.723em;"></span></span></span><span style="border-left-width: 0.003em; border-left-style: solid; display: inline-block; overflow: hidden; width: 0px; height: 1.07em; vertical-align: -0.13em;"></span></span></nobr><script type="math/tex" id="MathJax-Element-430">a<b</script>的典型指令序列,这里a和b都是int类型: -
跳转指令及其编码
正常执行的情况下,指令按照它们出现的顺序一条一条地执行。跳转指令会导致执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的地通常用一个标号(label )指明。如下:
指令jmp.Ll会导致程序跳过movl指令,从popl指令开始继续执行。在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。
jmp指令是无条件跳转,它可以是直接跳转,即跳转目标是作为指令的一部分编码的;也可以是间接跳转,即跳转目标是从寄存器或存储器位置中读出的。汇编语言中,直接跳转是给出一个标号作为跳转目标的,例如上面所示代码中的标号”.L1”。间接跳转的写法是‘*’后面跟一个操作数指示符,如指令jmp *%eax
是用寄存器%eax中的值作为跳转目标,指令
jmp *(%eax)
是以%eax中的值作为读地址,从存储器中读出跳转目标。
其他跳转指令都是有条件的——它们根据条件码的某个组合,或者跳转,或者继续执行下一条指令。
接下来我们了解跳转指令是如何执行的,即其机器编码的形式。
跳转指令在C语言中就是goto语句,下面是一个与PC相关的寻址的例子,这个汇编代码的片断由编译文件silly.c产生的。它包含两个跳转:第1行的jle指令前向跳转到更高的地址,而第8行的Jg指令后向跳转到较低的地址。
汇编器产生的“.o”格式的反汇编版本如下:
其中,silly代表代码片段的起始地址。第1行跳转指令的跳转目标指明为0x17,第7行跳转指令的跳转目标是0xa。这个值是怎么来的呢?以第1行的0x17为例,其值等于dest2处将要运行的指令地址,即A处的地址。只不过跳转指令是基于偏移寻址的,其值等于起始地址加偏移量,这里起始地址为0,所以0x17=0+0x17。
再来看机器码,X处的地址为9,其值为0xd,这个值是怎么来的呢?当执行与PC相关的寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。即当指令执行到第一行,需要寻址时,其值不是0x9,而是0xa,这时我们加上偏移量0xd即可得到0x17。
第7行跳转指令的目标用单字节补码表示为0xf3(十进制-13),同样基于偏移寻址。
下面是链接后的程序反汇编的版本:
这些指令被重定位到不同的地址,但是第1行和第7行跳转目标的编码并没有变。通过使用与PC相关的跳转目标编码,指令编码很简洁(只需要2个字节),而且目标代码可以不做改变就移到存储器中不同的位置。 -
翻译条件分支(翻译if/else语句)
上文中我们提到,跳转指令在C语言中就是goto语句。将条件表达式从C语言翻译成机器代码,最常用的方式就是翻译为跳转。还可以使用条件传送指令,会在下文介绍。看如下例子:
C语言中的if-else语句的通用形式模板是这样的:
if (test-expr) then-statement else else-statement
对于这种通用形式,汇编实现通常会使用下面这种形式,我们用C语法来描述:
t = test-expr; if(!t) goto false; then-statement goto done; false: else-statement done:
-
循环
循环在汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。大多数汇编器根据循环的do-while形式来产生代码,其他循环方式会先转成do-while形式,然后再编译成机器代码。-
do-while循环
do-while语句的通用形式如下:do body-statement while (test-expr)
do-while的通用形式可以翻译成如下所示的条件和goto语句:
loop: body-statement t = test-expr; if (t) goto loop;
-
while循环
while语句的通用形式如下:while (test-expr) body-statement
将代码转换成do-while循环:
if (! test-expr) goto done; do body-statement while (test-expr); done:
翻译成goto代码:
t = test-expr; if (!t) goto done; loop: body-statement t = test-expr; if (t) goto loop; done:
-
for循环
for循环的通用形式如下:for (init-expr; test-expr; update-expr) body-statement
转换成while循环:
init-expr; while (test-expr) { body-statement update-expr; }
转换成do-while循环:
init-expr; if (! test-expr) goto done; do{ body-statement update-expr; } while (test-expr); done:
翻译成goto代码:
init-expr; t = test-expr; if (!t) goto done; loop: body-statement update-expr; t = test-expr; if (t) goto loop; done:
-
-
条件传送指令
实现条件操作的传统方法是利用控制的条件转移。当条件满足时,程序沿着一条执行路径进行,而当条件不满足时,就走另一条路径。这种机制简单而通用,但是在现代处理器上,可能效率不高。
数据的条件转移是一种替代的策略。这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选取一个。条件传送指令更好地匹配了现代处理器的性能特性。看如下例子:
条件表达式同条件语句相似。关键就在于汇编代码第8行的cmovl指令,这条指令除了只在指定的条件满足时才执行数据传送之外,它的语法与MOV指令的相同。考虑下面的条件表达式和赋值的通用形式:
v = test-expr?then-expr:lese-expr;
编译器产生的代码,条件转移形式:
if (!test-expr) goto false; v = then-expr; goto done; false: v = else-expr; done:
基于条件传送的代码:
vt = then-expr; v = else-expr; t = test-expr; if(t) v = vt;
这个序列中的最后一条语句是用条件传送实现的——只有当测试条件t满足时,vt的值才会被复制到v中。
现代处理器通过使用流水线来获得高性能,即重叠连续指令的步骤。当机器遇到条件跳转时,它常常不能确定是否要进行跳转。处理器会进行分支预测,但是预测错误要求处理器丢掉它为跳转指令后所有指令已经做了的工作,招致很严重的惩罚,导致性能下降。但是使用条件传送也不是总会改进代码的效率。例如,如果then-expr或者else-expr的求值需要大量的计算,那么当相对应的条件不满足时,这些工作就白费了。编译器必须考虑浪费的计算和由于分支预测错误所造成的性能处罚之间的相对性能。
-
switch语句
switch语句可以根据一个整数索引值进行多重分支。它通过跳转表实现。
其中jt就是跳转表。可以观察到,跳转表对重复情况的处理就是简单地对表项4和6用同样的代码标号(loc_ D),而对于缺失情况的处理就是对表项1和5使用默认情况的标号(loc_def)。
上述代码的汇编代码如下:
执行switch语句的关键步骤是通过跳转表来访问代码位置,汇编代码中,跳转表用以下声明:
这些声明表明,在“.rodata”(只读数据,Read-Only Data )的目标代码文件的段中,应该有一组7个“long”(4个字节),每个字的值都是与指定的汇编代码标号相关的指令地址。标号.L7标记出这段分配地址的起始。使用跳转表是一种非常有效的实现多重分支的方法。在我们的例子中,程序可以只用一次跳转表引用就分支到5个不同的位置;当switch语句有上百种情况的时候,也可以只用一次跳转表访问去处理。