汇编级别的Switch case代码

24

我正在使用Cygwin Windows编程。在进行了一些C语言编程并熟悉了这门语言后,我想看看编译器为我编写的代码做了什么操作。

因此,我编写了一个包含switch case语句的代码块,并使用以下方法将其转换为汇编:

gcc -S foo.c  

以下是C源码:

switch(i)
{
    case 1:
    {
        printf("Case 1\n");
        break;
    }
    case 2:
    {           printf("Case 2\n");
        break;
    }
    case 3:
    {
        printf("Case 3\n");
        break;
    }
    case 4:
    {
        printf("Case 4\n");
        break;
    }
    case 5:
    {
        printf("Case 5\n");
        break;
    }
    case 6:
    {
        printf("Case 6\n");
        break;
    }
    case 7:
    {
        printf("Case 7\n");
        break;
    }
    case 8:
    {
        printf("Case 8\n");
        break;
    }
    case 9:
    {
        printf("Case 9\n");
        break;
    }
    case 10:
    {
        printf("Case 10\n");
        break;
    }
    default:
    {
        printf("Nothing\n");
        break;
    }
}  

现在的汇编代码如下:

movl    $5, -4(%ebp)
cmpl    $10, -4(%ebp)
ja  L13
movl    -4(%ebp), %eax
sall    $2, %eax
movl    L14(%eax), %eax
jmp *%eax
.section .rdata,"dr"
.align 4
L14:
.long   L13
.long   L3
.long   L4
.long   L5
.long   L6
.long   L7
.long   L8
.long   L9
.long   L10
.long   L11
.long   L12
.text
L3:
movl    $LC0, (%esp)
call    _printf
jmp L2
L4:
movl    $LC1, (%esp)
call    _printf
jmp L2
L5:
movl    $LC2, (%esp)
call    _printf
jmp L2
L6:
movl    $LC3, (%esp)
call    _printf
jmp L2
L7:
movl    $LC4, (%esp)
call    _printf
jmp L2
L8:
movl    $LC5, (%esp)
call    _printf
jmp L2
L9:
movl    $LC6, (%esp)
call    _printf
jmp L2
L10:
movl    $LC7, (%esp)
call    _printf
jmp L2
L11:
movl    $LC8, (%esp)
call    _printf
jmp L2
L12:
movl    $LC9, (%esp)
call    _printf
jmp L2
L13:
movl    $LC10, (%esp)
call    _printf
L2:  

现在,在汇编中,代码首先检查最后一种情况(即第10种情况)。这很奇怪。然后它将“i”复制到“eax”中并执行我无法理解的操作。

我听说编译器为switch..case实现了一些跳转表,这就是这段代码在做什么吗?或者它在做什么以及为什么这样做?因为在少数情况下, 该代码与生成if...else梯形时非常相似,但当情况数量增加时,会出现这种不寻常的实现。

提前致谢。


很不幸,它不能将字符串指针优化为查找表并调用call _printf。即使在-O3下,gcc/clang/icc也都没有这样做。https://godbolt.org/g/JrSwU3(但是它们会将`printf`优化为`puts`,并将尾调用优化为`jmp`而不是`call`/`ret`)。 - Peter Cordes
4个回答

32

首先,代码比较了i和10,当值大于10时跳转到默认情况(cmpl $10, -4(%ebp)后面是ja L13)。

接下来的代码将输入向左移位两位(sall $2, %eax),这相当于乘以4,生成跳转表的偏移量(因为表中每个条目占用4个字节)。

然后它从跳转表中加载一个地址(movl L14(%eax), %eax)并跳转到该地址(jmp *%eax)。

跳转表只是一个地址列表(在汇编代码中用标签表示):

L14:
.long   L13
.long   L3
.long   L4
...

需要注意的一件事是,L13 表示默认情况。它既是跳转表中的第一个条目(当 i 为0时),也在开头被特殊处理(当 i > 10 时)。


1
我明白了...这很有启发性。但是,如果情况较少(比如2或3),为什么编译器不生成跳转表呢? - puffadder
1
@puffadder:大多数现代编译器使用启发式算法来确定何时使用分支和跳转表更有效。例如,如果您的情况级别为1、100和1000,则可能会使用分支。 - Paul R
1
我不明白为什么每一步都跳到默认情况,而不是完全跳出switch。有人能解释一下吗? - mharris7190
1
@mharris7190,看起来每个表项都跳转到标签“L2”,这超出了switch的范围。默认条目“L13”没有跳转到“L2”,因为它是表中的最后一个条目,所以“L13”的下一条指令是“L2”。 - Nick

4
对于[1..10],编译器将生成一张表格,这样它就不需要比较值来确定跳转位置,直接使用:goto table[i]。这样可以提高速度。
但是如果i > 10,则会跳转到默认语句。程序必须在跳转之前进行检查,否则程序将崩溃。
如果你有稀疏的值(如23、9233、91238,而不是1、2、3...),编译器就不会生成这样的表格,而是逐个比较每个值。

4

是的,这是一个跳转表。第一个检查是检查值是否在案例中,并在不在时跳转到默认值。请注意,在这样的表格中,如果 %eax 为 0,则 L14(%eax) 指向表格的第一个元素(L13)。因此,在表格中,case 10: 的索引为9,而不是10。

切换的方式取决于您在 case 中拥有的值;在这种情况下,它们是“顺序”排列的,因此可以使用简单的跳转表。


0

是的,首先通过 switch 值计算 eax(使用 sall 移位作为乘法)来获取跳转表中的地址(跟随标签 L14:

jmp *%eax 是一个近跳转到您的 case 标签。 (jmp near eax)

其他标签后面的代码只是打印并跳过其他 case。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接