# 3.6.8 switch 语句

switch(开关)语句可以根据一个整数索引值进行多重分支(multiway branching)。在处理具有多种可能结果的测试时,这种语句特别有用。它们不仅提高了 C 代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高效。跳转表是一个数组,表项 i 是一个代码段的地址,这个代码段实现当开关索引值等于 i 时程序应该采取的动作。程序代码用开关索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。和使用一组很长的 if-else 语句相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。GCC 根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句。当开关情况数量比较多(例如 4 个以上),并且值的范围跨度比较小时,就会使用跳转表。

图 3-22a 是一个 C 语言 switch 语句的示例。这个例子有些非常有意思的特征,包括情况标号(case label)跨过一个不连续的区域(对于情况 101 和 105 没有标号),有些情况有多个标号(情况 104 和 106),而有些情况则会落入其他情况之中(情况 102),因为对应该情况的代码段没有以 break 语句结尾。

图 3-23 是编译 switch_eg 时产生的汇编代码。这段代码的行为用 C 语言来描述就是图 3-22b 中的过程 switch_eg_impl。这段代码使用了 GCC 提供的对跳转表的支持,这是对 C 语言的扩展。数组 jt 包含 7 个表项,每个都是一个代码块的地址。这些位置由代码中的标号定义,在 jt 的表项中由代码指针指明,由标号加上“&&”前缀组成。(回想运算符 & 创建一个指向数据值的指针。在做这个扩展时,GCC 的作者们创造了一个新的运算符 &&,这个运算符创建一个指向代码位置的指针。)建议你研究一下 C 语言过程 switch_eg_impl,以及它与汇编代码版本之间的关系。

void switch_eg(long x, long n, long *dest)
{
    long val = x;

    switch (n) {
    case 100:
        val *= 13;
        break;

    case 102:
        val += 10;
        /* Fall through */

    case 103:
        val += 11;
        break;

    case 104:
    case 106:
        val *= val;
        break;

    default:
        val = 0;
    }
    *dest = val;
}

a)switch 语句

1  void switch_eg_impl(long x, long n,
2                      long *dest)
3  {
4      /* Table of code pointers */
5      static void *jt[7] = {
6          &&loc_A, &&loc_def, &&loc_B,
7          &&loc_C, &&loc_D, &&loc_def,
8          &&loc_D
9      };
10     unsigned long index = n - 100;
11     long val;
12
13     if (index > 6)
14         goto loc_def;
15     /* Multiway branch */
16     goto *jt[index];
17
18 loc_A:     /* Case 100 */
19     val = x * 13;
20     goto done;
21 loc_B:     /* Case 102 */
22     x = x + 10;
23     /* Fall through */
24 loc_C:     /* Case 103 */
25     val = x + 11;
26     goto done;
27 loc_D:     /* Cases 104, 106 */
28     val = x * x;
29     goto done;
30 loc_def:   /* Default case */
31     val = 0;
32 done:
33     *dest = val;
34 }

b)翻译到扩展的 C 语言

图 3-22 switch 语句示例以及翻译到扩展的 C 语言。该翻译给出了跳转表 jt 的结构,以及如何访问它。作为对 C 语言的扩展,GCC 支持这样的表

原始的 C 代码有针对值 100、102~104 和 106 的情况,但是开关变量 n 可以是任意整数。编译器首先将 n 减去 100,把取值范围移到 0 和 6 之间,创建一个新的程序变量,在我们的 C 版本中称为 index。补码表示的负数会映射成无符号表示的大正数,利用这一事实,将 index 看作无符号值,从而进一步简化了分支的可能性。因此可以通过测试 index 是否大于 6 来判定 index 是否在 0~6 的范围之外。在 C 和汇编代码中,根据 index 的值,有五个不同的跳转位置:loc_A(在汇编代码中标识为 .L3)、loc_B.L5)、loc_C.L6)、loc_D.L7)和 loc_def.L8),最后一个是默认的目的地址。每个标号都标识一个实现某个情况分支的代码块。在 C 和汇编代码中,程序都是将 index 和 6 做比较,如果大于 6 就跳转到默认的代码处。

void switch_eg(long x, long n, long *dest)
x in %rdi, n in %rsi, dest in %rdx

1  switch_eg:
2      subq  $100, %rsi             Compute index = n-100
3      cmpq  $6, %rsi               Compare index:6
4      ja    .L8                    If >, goto loc_def
5      jmp   *.L4(,%rsi,8)          Goto *jt[index]
6  .L3:                             loc_A:
7      leaq  (%rdi,%rdi,2), %rax    3*x
8      leaq  (%rdi,%rax,4), %rdi    val = 13*x
9      jmp   .L2                    Goto done
10 .L5:                             loc_B:
11     addq  $10, %rdi              x = x + 10
12 .L6:                             loc_C:
13     addq  $11, %rdi              val = x + 11
14     jmp   .L2                    Goto done
15 .L7:                             loc_D:
16     imulq %rdi, %rdi             val = x * x
17     jmp   .L2                    Goto done
18 .L8:                             loc_def:
19     movl  $0, %edi               val = 0
20 .L2:                             done:
21     movq  %rdi, (%rdx)           *dest = val
22     ret                          Return

图 3-23 图 3-22 中 switch 语句示例的汇编代码

执行 switch 语句的关键步骤是通过跳转表来访问代码位置。在 C 代码中是第 16 行,一条 goto 语句引用了跳转表 jt。GCC 支持计算 goto(computed goto),是对 C 语言的扩展。在我们的汇编代码版本中,类似的操作是在第 5 行,jmp 指令的操作数有前缀“*”,表明这是一个间接跳转,操作数指定一个内存位置,索引由寄存器 %rsi 给出,这个寄存器保存着 index 的值。(我们会在 3.8 节中看到如何将数组引用翻译成机器代码。)

C 代码将跳转表声明为一个有 7 个元素的数组,每个元素都是一个指向代码位置的指针。这些元素跨越 index 的值 0~6,对应于 n 的值 100~106。可以观察到,跳转表对重复情况的处理就是简单地对表项 4 和 6 用同样的代码标号(loc_D),而对于缺失的情况的处理就是对表项 1 和 5 使用默认情况的标号(loc_def)。

在汇编代码中,跳转表用以下声明表示,我们添加了一些注释:

1      .section    .rodata
2      .align 8                Align address to multiple of 8
3  .L4:
4      .quad .L3               Case 100: loc_A
5      .quad .L8               Case 101: loc_def
6      .quad .L5               Case 102: loc_B
7      .quad .L6               Case 103: loc_C
8      .quad .L7               Case 104: loc_D
9      .quad .L8               Case 105: loc_def
10     .quad .L7               Case 106: loc_D

这些声明表明,在叫做“.rodata”(只读数据,Read-Only Data)的目标代码文件的段中,应该有一组 7 个“四”字(8 个字节),每个字的值都是与指定的汇编代码标号(例如 .L3)相关联的指令地址。标号 .L4 标记出这个分配地址的起始。与这个标号相对应的地址会作为间接跳转(第 5 行)的基地址。

不同的代码块(C 标号 loc_Aloc_Dloc_def)实现了 switch 语句的不同分支。它们中的大多数只是简单地计算了 val 的值,然后跳转到函数的结尾。类似地,汇编代码块计算了寄存器 %rdi 中的值,并且跳转到函数结尾处由标号 .L2 指示的位置。只有情况标号 102 的代码不是这种模式的,正好说明在原始 C 代码中情况 102 会落到情况 103 中。具体处理如下:以标号 .L5 起始的汇编代码块中,在块结尾处没有 jmp 指令,这样代码就会继续执行下一个块。类似地,C 版本 switch_eg_impl 中以标号 loc_B 起始的块的结尾处也没有 goto 语句。

检查所有这些代码需要很仔细的研究,但是关键是领会使用跳转表是一种非常有效的实现多重分支的方法。在我们的例子中,程序可以只用一次跳转表引用就分支到 5 个不同的位置。甚至当 switch 语句有上百种情况的时候,也可以只用一次跳转表访问去处理。

练习题 3.30 下面的 C 函数省略了 switch 语句的主体。在 C 代码中,情况标号是不连续的,而有些情况有多个标号。

void switch2(long x, long *dest) {
    long val = 0;
    switch (x) {
        /* Body of switch statement omitted */
    }
    *dest = val;
}

在编译该函数时,GCC 为程序的初始部分生成了以下汇编代码,变量 x 在寄存器 %rdi 中:

void switch2(long x, long *dest)
x in %rdi

1  switch2:
2      addq $1, %rdi
3      cmpq $8, %rdi
4      ja   .L2
5      jmp  *.L4(,%rdi,8)

为跳转表生成以下代码:

1  .L4:
2      .quad .L9
3      .quad .L5
4      .quad .L6
5      .quad .L7
6      .quad .L2
7      .quad .L7
8      .quad .L8
9      .quad .L2
10     .quad .L5

根据上述信息回答下列问题:

A. switch 语句内情况标号的值分别是多少?

B. C 代码中哪些情况有多个标号?

练习题 3.31 对于一个通用结构的 C 函数 switcher

void switcher(long a, long b, long c, long *dest)
{
    long val;
    switch(a) {
    case __________:        /* Case A */
        c = __________;
        /* Fall through */
    case __________:        /* Case B */
        val = __________;
        break;
    case __________:        /* Case C */
    case __________:        /* Case D */
        val = __________;
        break;
    case __________:        /* Case E */
        val = __________;
        break;
    default:
        val = __________;
    }
    *dest = val;
}

GCC 产生如图 3-24 所示的汇编代码和跳转表。

void switcher(long a, long b, long c, long *dest)
a in %rdi, b in %rsi, c in %rdx, dest in %rcx

1  switcher:
2      cmpq  $7, %rdi
3      ja    .L2
4      jmp   *.L4(,%rdi,8)
5      .section .rodata
6  .L7:
7      xorq  $15, %rsi
8      movq  %rsi, %rdx
9  .L3:
10     leaq  112(%rdx), %rdi
11     jmp   .L6
12 .L5:
13     leaq  (%rdx,%rsi), %rdi
14     salq  $2, %rdi
15     jmp   .L6
16 .L2:
17     movq  %rsi, %rdi
18 .L6:
19     movq  %rdi, (%rcx)
20     ret

a)代码

1  .L4:
2      .quad .L3
3      .quad .L2
4      .quad .L5
5      .quad .L2
6      .quad .L6
7      .quad .L7
8      .quad .L2
9      .quad .L5

b)跳转表

图 3-24 练习题 3.31 的汇编代码和跳转表

填写 C 代码中缺失的部分。除了情况标号 C 和 D 的顺序之外,将不同情况填入这个模板的方式是唯一的。