ランタイムエラーでたまに出てくるIllegal instructionを意図的に出すにはどうすればいいかを知りたくなっていろいろみた。 その中で以下のコードが環境によってはIllegal instructionになるらしいと知り、いろいろ試してみた。
#include <stdio.h>
int main(void)
{
int x = 1;
int y = 0;
printf("%d / %d = %d\n", x, y, x / y);
}
とりあえず単純にGCCとClangでコンパイル・実行してみた。
$ gcc main.c && ./a.out
zsh: floating point exception ./a.out
$ clang main.c && ./a.out
zsh: floating point exception ./a.out
オプションなしのコンパイルだとどちらでも浮動小数点例外が出る。 それぞれのアセンブリはこちら
GCC
.file "main.c"
.text
.section .rodata
.LC0:
.string "%d / %d = %d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $1, -4(%rbp)
movl $0, -8(%rbp)
movl -4(%rbp), %eax
cltd
idivl -8(%rbp)
movl %eax, %ecx
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 10.3.0-9) 10.3.0"
.section .note.GNU-stack,"",@progbits
Clang
.text
.file "main.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $1, -4(%rbp)
movl $0, -8(%rbp)
movl -4(%rbp), %esi
movl -8(%rbp), %edx
movl -4(%rbp), %eax
movl %edx, -12(%rbp) # 4-byte Spill
cltd
idivl -8(%rbp)
movabsq $.L.str, %rdi
movl -12(%rbp), %ecx # 4-byte Reload
movl %ecx, %edx
movl %eax, %ecx
movb $0, %al
callq printf
xorl %ecx, %ecx
movl %eax, -16(%rbp) # 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
.cfi_def_cfa %rsp, 8
retq
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.type .L.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "%d / %d = %d\n"
.size .L.str, 14
.ident "Debian clang version 11.0.1-2"
.section ".note.GNU-stack","",@progbits
.addrsig
.addrsig_sym printf
どちらもidivl
で例外が投げられているらしい。
idivl -8(%rbp)
次に最適化オプションをつけてコンパイルしてみた。 まずはGCCでやってみる。
$ gcc -O2 main.c && ./a.out
zsh: illegal hardware instruction ./a.ou
目的のIllegal instructionが出てきた。
次にClangでもやってみる。
$ clang -O2 main.c && ./a.out
1 / 0 = 1223354136
Illegal instructionが出ないどころか0徐算も出なくなってしまった。 何度か実行してみると、
$ clang -O2 main.c && ./a.out
1 / 0 = -1048254696
$ ./a.out
1 / 0 = 1310734104
$ ./a.out
1 / 0 = -39168232
$ ./a.out
1 / 0 = 752305944
毎回結果が変わってしまう。 というわけで、最適化されたアセンブリをGCCとClangで見比べてみる。
GCC
.file "main.c"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
ud2
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (Debian 10.3.0-9) 10.3.0"
.section .note.GNU-stack,"",@progbits
GCCではコンパイル時に0徐算であることがわかりud2
命令に置き換える最適化が走っているように見える。
Clang
.text
.file "main.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rax
.cfi_def_cfa_offset 16
movl $.L.str, %edi
movl $1, %esi
xorl %edx, %edx
xorl %eax, %eax
callq printf
xorl %eax, %eax
popq %rcx
.cfi_def_cfa_offset 8
retq
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.type .L.str,@object # @.str
.section .rodata.str1.1,"aMS",@progbits,1
.L.str:
.asciz "%d / %d = %d\n"
.size .L.str, 14
.ident "Debian clang version 11.0.1-2"
.section ".note.GNU-stack","",@progbits
.addrsig
printf
の引数に明示的に渡しているのはedi
, esi
, edx
の3つ。
それぞれフォーマット文字列、1
、0
である。
x / y
が渡されていない。
ただ、フォーマット文は3つの指定子(esi
, edx
, ecx
)を持つため、printf
呼び出し時のecx
の値を使うことになる。
そのため、実行するたびに出力される値が変わってしまう。
コンパイラの最適化オプションはパフォーマンス面で優れる反面、今回のようなエラーを隠してしまうことにも繋がる。 開発時は最適化あり・なしでいろいろ検証しながら進めるのがいいなと思った。