C 开关箱装配级别代码
我正在用cygwinwindows编程C。在完成了一点C编程并熟悉了该语言之后,我想了解一下编译器对我编写的代码所做的工作 因此,我写下了一个包含switch case语句的代码块,并使用以下命令将其转换为汇编:C 开关箱装配级别代码,c,gcc,assembly,cygwin,C,Gcc,Assembly,Cygwin,我正在用cygwinwindows编程C。在完成了一点C编程并熟悉了该语言之后,我想了解一下编译器对我编写的代码所做的工作 因此,我写下了一个包含switch case语句的代码块,并使用以下命令将其转换为汇编: gcc -S foo.c 以下是C源代码: switch(i) { case 1: { printf("Case 1\n"); break; } case 2: { printf("Cas
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)。这很奇怪。然后,它将“我”复制到“eax”中,做一些我无法理解的事情
我听说编译器为switch..case实现了一些跳转表。这就是代码所做的吗?或者它在做什么,为什么?因为在案件数量减少的情况下,
代码与为if…else梯形图生成的代码非常相似,但当案例数量增加时,就会看到这种外观不同寻常的实现
提前感谢。首先,代码将i与10进行比较,并在值大于10时跳转到默认情况(
cmpl$10,-4(%ebp)
后接ja L13
)
下一位代码将输入向左移动2(sall$2,%eax
),这与乘以4相同,这将生成跳转表中的偏移量(因为表中的每个条目都有4字节长)
然后它从跳转表(movl14(%eax),%eax
)加载一个地址并跳转到它(jmp*%eax
)
跳转表只是地址列表(在汇编代码中由标签表示):
需要注意的是,
L13
表示默认情况。它既是跳转表中的第一个条目(当i为0时),又是在开始时(当i>10时)专门处理的。对于[1..10
]编译器将生成一个表,因此它不需要比较值就可以转到某个地方,它直接执行:转到表[i]
。那样更快
但在casei>10
中,它跳转到您的默认语句。它必须在跳转之前先检查,否则,程序将严重崩溃
如果您有稀疏值(如23923391238,而不是1,2,3…),编译器将不会生成这样一个表,并比较每个值。是的,第一个eax是通过开关值(
sall
shift as乘法)来计算的,以从跳转表中获得地址(以下标签L14:
)
jmp*%eax
几乎可以跳到您案例的标签上。
(eax附近的jmp)
其他标签后面的代码只是打印并跳过其他情况。是的,它是一个跳转表。第一个检查是检查值是否在案例中,如果不在案例中,则跳转到默认值。不要忘记,在这样的表中,如果%eax为0,则L14(%eax)指向表的第一个元素(L13)。因此,在表中,
案例10:
的索引是9,而不是10
切换的方式取决于您在
案例中的值
;在这种情况下,它们是“顺序”的,因此简单的跳转表是可能的。我明白了。。。这是有益的。但是为什么编译器不在较少的情况下生成一个跳转表(如2或3)?@puffadder:大多数现代编译器使用启发式来确定何时使用分支比使用跳转表更有效。例如,如果您的案例级别是1、100和1000,您可能希望使用分支。我不明白为什么每一步都跳到默认案例,而不是完全跳出开关。有人能解释一下吗?@mharris7190,看起来每个表项都跳转到标签“L2”,而标签完全脱离了开关。默认条目“L13”没有跳转到“L2”,因为它是表中的最后一个条目,所以“L13”的下一条指令是“L2”。不幸的是,它没有优化到字符串指针的表查找和调用\u printf
。即使在-O3
上,gcc/clang/icc也没有这样做。(不过,它们确实将printf
优化为put
,并将尾部调用优化为jmp
,而不是调用
/ret
)
L14:
.long L13
.long L3
.long L4
...