はじめに
Linux カーネルのモジュール作成時に、コンパイラの最適化が影響して while が意図せず無限ループになってしまったことの現象と対処の紹介
現象
以下のコードは、void *p に 0 以外の数字が書かれるとループを抜けることが期待値です。
(説明上の都合で noinline を書いています)
static void noinline wait_for_update(void *p)
{
volatile unsigned long number;
do {
number = *(unsigned long *)p;
} while (number == 0);
}
このコードを含むカーネルモジュールを作成して、いざモジュールを動かすとシステムがハングしちゃいました。メモリダンプを見てみると、上記のループを抜けないことが原因のようです。*p を更新する別のカーネルスレッドが動いていないことを考えましたがどうも動いているようです。
そこで、上記のコードをディスアセンブルしてみると、衝撃の事態が・・・
0000000000000000 <wait_for_update>:
0: e8 00 00 00 00 callq 5 <wait_for_update+0x5>
5: 55 push %rbp
6: 48 89 e5 mov %rsp,%rbp
9: 48 83 ec 08 sub $0x8,%rsp
d: 48 8b 17 mov (%rdi),%rdx
10: 48 89 55 f8 mov %rdx,-0x8(%rbp) ★ ここから
14: 48 8b 45 f8 mov -0x8(%rbp),%rax
18: 48 85 c0 test %rax,%rax
1b: 74 f3 je 10 <wait_for_update+0x10> ★ ここ
1d: c9 leaveq
1e: c3 retq
1f: 90 nop
なんということでしょう!オフセット 0x10 から 0x1b のループになっているではありませんか!これでは *p を更新したことを気がつけず、無限ループしちゃいます。
オフセット 0xd で *p の更新を確認しているので、ループの期待値は 0xd から 0x1b です。
d: 48 8b 17 mov (%rdi),%rdx
原因
コンパイラ(gcc)が最適化したことが原因です。p はローカルスコープで、更新するコードは無いから最初に一回だけ参照すればいいよね、とコンパイラさんは判断したんですね。
カーネルモジュールをコンパイルする時はデフォルトで最適化オプション(-O2)が付きます。
対処
最適化をしないようにするため、volatile をつけてみます。
static void noinline wait_for_update(void *p)
{
volatile unsigned long number;
do {
number = *(volatile unsigned long *)p;
} while (number == 0);
}
そうすると、期待通りのループになりました。
d: 48 8b 07 mov (%rdi),%rax ★ここから
10: 48 89 45 f8 mov %rax,-0x8(%rbp)
14: 48 8b 45 f8 mov -0x8(%rbp),%rax
18: 48 85 c0 test %rax,%rax
1b: 74 f0 je d <wait_for_update+0xd> ★ここ
1d: c9 leaveq
補足 無限 jmp 地獄
ちなみに、以下のように number 変数にすら volatile をつけないと、
static void noinline wait_for_update(void *p)
{
unsigned long number;
do {
number = *(unsigned long *)p;
} while (number == 0);
}
以下のように無限 jmp 地獄に落ちることになります。これであなたも A 級ジャンパー
0000000000000000 <wait_for_update>:
0: e8 00 00 00 00 callq 5 <wait_for_update+0x5>
5: 48 83 3f 00 cmpq $0x0,(%rdi)
9: 55 push %rbp
a: 48 89 e5 mov %rsp,%rbp
d: 75 02 jne 11 <wait_for_update+0x11>
f: eb fe jmp f <wait_for_update+0xf> ★ここ!
11: 5d pop %rbp
12: c3 retq