私はcygwinウィンドウで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ラダーで生成されたものと非常に似ていますが、ケースの数が増えると、この異常な外観の実装が見られます。
前もって感謝します。
最初に、コードは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
...
_
注意すべき点の1つは、_L13
_がデフォルトのケースを表していることです。これは、ジャンプテーブルの最初のエントリ(iが0の場合)であり、最初に特別に処理されます(i> 10の場合)。
はい、ジャンプテーブルです。最初のチェックは、値がケースに含まれているかどうかをチェックし、含まれていない場合はデフォルトにジャンプすることです。このようなテーブルでは、%eaxが0の場合、L14(%eax)はテーブルの最初の要素(L13)を指すことを忘れないでください。したがって、テーブルではcase 10:
は、10ではなく9でインデックス付けされます。
切り替えを行う方法は、case
にある値によって異なります。この場合、それらは「シーケンス」にあるため、単純なジャンプテーブルが可能です。
ために [1..10
]コンパイラはテーブルを生成するので、どこかに移動するために値を比較する必要はありません。直接次のことを行います。goto table[i]
。そうすればそれはより速くなります。
しかし、場合i > 10
デフォルトのステートメントにジャンプします。ジャンプする前に最初にチェックする必要があります。そうしないと、プログラムが惨めにクラッシュします。
スパース値(23、9233、91238など、1、2、3 ...ではない)がある場合、コンパイラーはそのようなテーブルを生成せず、各値を比較します。
はい、最初のeaxは、ジャンプテーブル(ラベルL14:
に続く)からアドレスを取得するためのスイッチ値(乗算としてのsall
シフト)によって計算されます。
jmp *%eax
は、ケースのラベルに近いジャンプです。 (eaxの近くのjmp)
他のラベルに続くコードは印刷するだけで、他の場合はスキップします。