3.6.5 用条件控制来实现条件分支

3.6.5 用条件控制来实现条件分支

将条件表达式和语句从 C 语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。(另一种方式在 3.6.6 节中会看到,有些条件可以用数据的条件转移实现,而不是用控制的条件转移来实现。)例如,图 3-16a 给出了一个计算两数之差绝对值的函数的 C 代码1。这个函数有一个副作用,会增加两个计数器,编码为全局变量 lt_cntge_cnt 之一。GCC 产生的汇编代码如图 3-16c 所示。把这个机器代码再转换成 C 语言,我们称之为函数 gotodiff_se(图 3-16b)。它使用了 C 语言中的 goto 语句,这个语句类似于汇编代码中的无条件跳转。使用 goto 语句通常认为是一种不好的编程风格,因为它会使代码非常难以阅读和调试。本文中使用 goto 语句,是为了构造描述汇编代码程序控制流的 C 程序。我们称这样的编程风格为“goto 代码”。

1 实际上,如果一个减法溢出,这个函数就会返回一个负数值。这里我们主要是为了展示机器代码,而不是实现代码的健壮性。

在 goto 代码中(图 3-16b),第 5 行中的 goto x_ge_y 语句会导致跳转到第 9 行中的标号 x_ge_y 处(当 x >= y 时会进行跳转)。从这一点继续执行,完成函数 absdiff_seelse 部分并返回。另一方面,如果测试 x >= y 失败,程序会计算 absdiff_seif 部分指定的步骤并返回。

汇编代码的实现(图 3-16c)首先比较了两个操作数(第 2 行),设置条件码。如果比较的结果表明 x 大于或者等于 y,那么它就会跳转到第 8 行,增加全局变量 ge_cnt,计算 x - y 作为返回值并返回。由此我们可以看到 absdiff_se 对应汇编代码的控制流非常类似于 gotodiff_se 的 goto 代码。

long lt_cnt = 0;
long ge_cnt = 0;

long absdiff_se(long x, long y)
{
    long result;
    if (x < y) {
        lt_cnt++;
        result = y - x;
    }
    else {
        ge_cnt++;
        result = x - y;
    }
    return result;
}

a)原始的 C 语言代码

1  long gotodiff_se(long x, long y)
2  {
3      long result;
4      if (x >= y)
5          goto x_ge_y;
6      lt_cnt++;
7      result = y - x;
8      return result;
9  x_ge_y:
10     ge_cnt++;
11     result = x - y;
12     return result;
13 }

b)与之等价的 goto 版本

long absdiff_se(long x, long y)
x in %rdi, y in %rsi

1  absdiff_se:
2      cmpq  %rsi, %rdi          Compare x:y
3      jge   .L2                 If >=, goto x_ge_y
4      addq  $1, lt_cnt(%rip)    lt_cnt++
5      movq  %rsi, %rax
6      subq  %rdi, %rax          result = y - x
7      ret                       Return
8  .L2:                          x_ge_y:
9      addq  $1, ge_cnt(%rip)    ge_cnt++
10     movq  %rdi, %rax
11     subq  %rsi, %rax          result = x - y
12     ret                       Return

c)产生的汇编代码

图 3-16 条件语句的编译。a)C 过程 absdiff_se 包含一个 if-else 语句;b)C 过程 gotodiff_se 模拟了汇编代码的控制;c)给出了产生的汇编代码

C 语言中的 if-else 语句的通用形式模板如下:

if (test-expr)
    then-statement
else
    else-statement

这里 test-expr 是一个整数表达式,它的取值为 0(解释为“假”)或者为非 0(解释为“真”)。两个分支语句中(then-statementelse-statement)只会执行一个。

对于这种通用形式,汇编实现通常会使用下面这种形式,这里,我们用 C 语法来描述控制流:

t = test-expr;
if (!t)
    goto false;
then-statement
goto done;
false:
    else-statement
done:

也就是,汇编器为 then-statementelse-statement 产生各自的代码块。它会插入条件和无条件分支,以保证能执行正确的代码块。

旁注 用 C 代码描述机器代码

图 3-16 给出了一个示例,用来展示把 C 语言控制结构翻译成机器代码。图中包括示例的 C 函数 a 和由 GCC 生成的汇编代码的注释版本 c,还有一个与汇编代码结构高度一致的 C 语言版本 b。机器代码的 C 语言表示有助于你理解其中的关键点,能引导你理解实际的汇编代码。

练习题 3.16 已知下列 C 代码:

void cond(long a, long *p)
{
    if (p && a > *p)
        *p = a;
}

GCC 会产生下面的汇编代码:

void cond(long a, long *p)
a in %rdi, p in %rsi

cond:
    testq %rsi, %rsi
    je    .L1
    cmpq  %rdi, (%rsi)
    jge   .L1
    movq  %rdi, (%rsi)
.L1:
    rep; ret

A. 按照图 3-16b 中所示的风格,用 C 语言写一个 goto 版本,执行同样的计算,并模拟汇编代码的控制流。像示例中那样给汇编代码加上注解可能会有所帮助。

B. 请说明为什么 C 语言代码中只有一个 if 语句,而汇编代码包含两个条件分支。

练习题 3.17if 语句翻译成 goto 代码的另一种可行的规则如下:

t = test-expr;
if (t)
    goto true;
else-statement
goto done;
true:
    then-statement
done:

A. 基于这种规则,重写 absdiff_se 的 goto 版本。

B. 你能想出选用一种规则而不选用另一种规则的理由吗?

练习题 3.18 从如下形式的 C 语言代码开始:

long test(long x, long y, long z) {
    long val = ____________________;
    if (____________________) {
        if (____________________)
            val = ____________________;
        else
            val = ____________________;
    } else if (____________________)
        val = ____________________;
    return val;
}

GCC 产生如下的汇编代码:

long test(long x, long y, long z)
x in %rdi, y in %rsi, z in %rdx

test:
    leaq  (%rdi,%rsi), %rax
    addq  %rdx, %rax
    cmpq  $-3, %rdi
    jge   .L2
    cmpq  %rdx, %rsi
    jge   .L3
    movq  %rdi, %rax
    imulq %rsi, %rax
    ret
.L3:
    movq  %rsi, %rax
    imulq %rdx, %rax
    ret
.L2:
    cmpq  $2, %rdi
    jle   .L4
    movq  %rdi, %rax
    imulq %rdx, %rax
.L4:
    rep; ret

填写 C 代码中缺失的表达式。