はじめに
この記事で、なぜかメモリの不正アクセスをするとSIGILLが出るコードを見つけたのだけれど、その後、調査を進めて、
int main(void) {
__asm__("movl $0, 0(%rbp,%rdx,1)");
}
の一行でSIGILLが出ることがわかった。ここで問題となるのは%rdx
の値で、これが小さいとSIGSEGV、大きいとSIGILLになるっぽい。%rdx
のかわりに%rax
を使ってみよう。
int main(void) {
__asm__("movq $10000000, %rax");
__asm__("movl $0, 0(%rbp,%rdx,1)");
}
$ g++ test1.cpp
$ ./a.out
zsh: segmentation fault ./a.out
int main(void) {
__asm__("movq $10000000000, %rax");
__asm__("movl $0, 0(%rbp,%rax,1)");
}
$ g++ test2.cpp
$ ./a.out
zsh: illegal hardware instruction ./a.out
というわけで%rax
に$10000000
が入ってるとSIGSEGV、$10000000000
が入ってるとSIGILLが出てるので、その間に境目があるっぽい。
というわけで二分探索でどのあたりに境界があるか調べてみる。
コード
def output(n)
f = open("test.cpp","w")
f.puts "int main(void) {"
f.puts "__asm__(\"movq $#{n}, %rax\");"
f.puts "__asm__(\"movl $0, 0(%rbp,%rax,1)\");"
f.puts "}"
f.close
system("g++ test.cpp")
end
def search
s = 10000000
e = 10000000000
while (e != s && e != s+1)
n = (e+s)/2
output(n)
`./a.out`
if $?.to_i == 11
s = n
puts "#{n} SIGSEGV"
else
e = n
puts "#{n} SIGILL"
end
end
end
search
Rubyで外部コマンドを実行した際、エラー終了した時のシグナルは$?.to_i
で取れる。なんとなく$?.exitstatus
を使いたくなるが、こちらは正常終了の返り値で、異常終了した場合はnilになるので注意。
実行結果
$ ruby test.rb
5005000000 SIGILL
2507500000 SIGSEGV
3756250000 SIGSEGV
4380625000 SIGSEGV
4692812500 SIGILL
(snip)
4670126427 SIGILL
4670126424 SIGSEGV
4670126425 SIGILL
というわけで、4670126424がSIGSEGV、4670126425でSIGILLになった。ただし、実行するたびに結果が微妙に変わる。
また、movl $0, 0(%rbp,%rax,1)
をmovl $0, 0(%rbp,%rax,2)
など、スケールファクターを変えると、その分SIGSEGVとSIGILLの間の値も変わる。
movlのスケールファクターを2にするとこんな感じ。
$ ruby test.rb
5005000000 SIGILL
(snip)
2361903002 SIGSEGV
2361903003 SIGILL
概ね半分になった。スケールファクターを4にしてみると境目が1168413942に、8にすると593619583と、だいたいスケールファクター*境目が一定っぽい。
スタックサイズと関係するのかと思ったが、limitで値を変えてみても影響はないようだ。
まとめ
Macで不正なmovl命令を出すことでSIGILLを出す現象を調べてみた。メモリの不正アクセスなのだから、SIGSEGVかSIGBUSを出すのが妥当だと思うのだが、なぜSIGILLを出すのか理解不能。MacではSIGSEGVとSIGBUSの区別がいいかげんという指摘もあり、それに関連するのかと思ったが、さすがにSIGILLを出すのは変だと思うのだが。