読者です 読者をやめる 読者になる 読者になる

Pebble's Diary

プログラマーの作業メモ

C/C++/ObjC メモリ破壊系バグのつぶし方

C++11

メモリ破壊系バグとは

メモリ破壊系バグとは、プログラマーが想定して割り当てたメモリ領域のサイズを超えた部分にデータを書き込んでしまい、プログラムが意図通り動作しなくなるバグのことです。

このバグは以下の特徴を持っています。

  • 再現性が100%ではない場合が多い。
  • バグの原因箇所の特定が難しい。

破壊するメモリの領域としては、スタック領域、ヒープ領域、その他があります。
また、プログラマが明示的に確保した領域またはライブラリが確保した領域に分けられます。

スタック領域の例としては、関数内で以下のように宣言した a[32] の32バイトなどです。

void hello(void){
    char a[32];
    memset(a, 0, 33); // 1バイト分オーバー!
}

このようにスタック領域を破壊した場合は、hello()関数の戻り先アドレスを上書きしてしまい、この関数以降は命令自体 まともに実行されないため、どこかでクラッシュしたり、全く関係のないところに行ったりして、動作が不定になります。

ヒープ領域は例えば以下のようにnewで割り当てたbの 32バイトが該当します。

void hi(void){
    char* b = new char [32];
    memset(b, 0, 33); // 1バイト分オーバー!
}

この場合、ヒープ領域の先はおそらくヒープ領域なので、別のメモリデータが破壊されることになります。

バグの発生箇所を特定する方法

まず、バッファオーバーランを引き起こしやすい関数や処理に着目します。

  • strcpy
  • memcpy
  • memset
  • vDSP系関数
  • for文でメモリ書きこみしている処理

怪しそうな箇所の直前に、以下のようなassert文を追加します。

assert(その箇所で実際にアクセスしているメモリサイズ<=有効なメモリサイズ);

例えば、以下のような感じです。

void hi(void){
    const int bufsize = 32;
    const int copysize = 33;
    char* b = new char [bufsize];
    assert(copysize <= bufsize); // メモリ破壊する前にassertで検知!
    memset(b, 0, copysize); // 1バイト分オーバー!
}

この例は分かりやすいですが、バグっている場合はサイズ指定がかなり複雑な変数になっている場合が多く、正しくassert文を入れるのにも慣れが必要です。 これを怪しそうな箇所全てに入れてからデバッグ実行すれば、バッファオーバーランしている箇所が炙り出せるというわけです。