LoginSignup
5
3

More than 5 years have passed since last update.

コンパイラ(gcc)の最適化による無限ループの罠と回避

Last updated at Posted at 2017-06-25

はじめに

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 です。

pの更新確認
   d:   48 8b 17                mov    (%rdi),%rdx

原因

コンパイラ(gcc)が最適化したことが原因です。p はローカルスコープで、更新するコードは無いから最初に一回だけ参照すればいいよね、とコンパイラさんは判断したんですね。
カーネルモジュールをコンパイルする時はデフォルトで最適化オプション(-O2)が付きます。

対処

最適化をしないようにするため、volatile をつけてみます。

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 をつけないと、

volatile無し
static void noinline wait_for_update(void *p)
{
        unsigned long number;

        do {
                number = *(unsigned long *)p;
        } while (number == 0);
}

以下のように無限 jmp 地獄に落ちることになります。これであなたも A 級ジャンパー

無限jmp地獄
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   
5
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3