【问题标题】:Switch case assembly level code开关盒装配级代码
【发布时间】:2011-03-02 00:27:44
【问题描述】:

我在 cygwin windows 上编程 C。在完成了一些 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 也没有这样做。 godbolt.org/g/JrSwU3(不过,他们确实将printf 优化为puts,并将尾调用优化为jmp 而不是call/ret

标签: c gcc assembly cygwin


【解决方案1】:

首先,代码将 i 与 10 进行比较,并在值大于 10 时跳转到默认情况(cmpl $10, -4(%ebp) 后跟 ja L13)。

下一位代码将输入向左移动 2 (sall $2, %eax),这与乘以 4 相同,从而生成跳转表的偏移量(因为表中的每个条目都是 4 字节长)

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

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

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

需要注意的一点是L13 代表默认情况。它既是跳转表中的第一个条目(当 i 为 0 时),又是在开头特别处理的(当 i > 10 时)。

【讨论】:

  • 我明白了...这是信息。但是为什么编译器不生成一个跳转表以防万一(比如 2 或 3)?
  • @puffadder:大多数现代编译器都使用启发式方法来确定何时使用分支和跳转表更有效。例如。如果您的案例级别是 1、100 和 1000,您可能希望使用分支。
  • 我不明白为什么每一步都跳到默认情况下,而不是完全跳出开关。有人可以解释一下吗?
  • @mharris7190,看起来每个表条目都跳转到标签“L2”,该标签完全不在交换机中。默认条目“L13”没有跳转到“L2”,因为它是表中的最后一个条目,因此“L13”的下一条指令是“L2”。
【解决方案2】:

是的,它是一个跳转表。第一个检查是检查值是否在 case 中,如果不在则跳转到默认值。不要忘记,在这样的表中,如果 %eax 为 0,则 L14(%eax) 指向表的第一个元素(L13)。所以在表中,case 10: 的索引是 9,而不是 10。

切换的方式取决于你在case中的值;在这种情况下它们是“顺序”的,所以简单的跳转表是可能的。

【讨论】:

    【解决方案3】:

    对于[1..10],编译器会生成一个表,这样它就不需要比较值去某个地方,它直接做一个:goto table[i]。这样会更快。

    但如果i > 10 它会跳转到您的默认语句。跳转前必须先检查,否则程序会惨死。

    如果您有稀疏值(例如 23、9233、91238,而不是 1、2、3...),编译器将不会生成这样的表并比较每个值。

    【讨论】:

      【解决方案4】:

      是的,第一个eax是通过开关值(sallshift作为乘法)从跳转表中得到地址(跟标签L14:

      jmp *%eax 几乎可以跳转到您的案例标签。 (在 eax 附近 jmp)

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-09-01
        • 2023-03-23
        • 2019-04-25
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多