#はじめに
過去のプロジェクトでマスター直前に出たメモリ破壊のバグを調査した時の方法を公開します。
#報告内容
プラットフォーム:コンシューマ
言語:C++症状:
メモリアロケート時に門番の情報が破壊されているらしく、不正チェックに引っかかり、ASSERTで停止。
タイミングは不定、アドレスも不定な為ハードウェアブレイクでも追うのが困難な状況。
再現頻度はかなり低い。今ところはデバッグ版のみで発生。
リリース版はエラーチェックに引っかからないので発覚していないだけで、メモリは破壊されている可能性はある。
#調査開始
かなりやっかいなメモリ破壊系の不具合報告が来ました。
破壊している箇所がたまたまヒープの門番の場所だったので、その領域を解放した時のチェックで発覚しました。
メモリアロケート時に不正チェックに引っかかっているということは、それ以前のどこかのタイミングで解放されたメモリ領域を破壊されていると思われます。
もしかしたら他のデータ領域を壊している可能性もありますし、その場合壊されたことに気が付かず動作し続けている可能性もあります。
色々と追っているがとっかかりがない状況です。
他のメンバーはマスター直前で他のチケットにかかりきりで誰も調べられていません。
頻度は高くないが壊されたメモリを確認してみると、必ず1バイトが書き換わっている様で前後の門番のフィルの状態は壊されていませんでした。
バッファオーバーランの可能性もありますが、仮にそのオブジェクトのインスタンスでメモリを破壊してそのタイミングで門番を破壊しているようであればそのインスタンスが破棄されたタイミングなどでもエラーが出てもよさそうなものです。
まずは変数の書き込みを疑ってみることにします。
ということで以下の様なことをやって書き込みのタイミングで拾うようにしてみました。
#調査方法
プロジェクトでは固有の型が定義されています。
簡潔に書くために便宜上以下の様にしています。
typedef char s8;
typedef unsigned char u8;
typedef short s16;
typedef unsigned short s16;
:
:
変数の書き込みのタイミングでチェックを行うために以下の様なclassを定義します。
力技ですがboolやs8,u8という型をGrepでBool,S8,U8に置換します。
これで今までと同じ挙動で変数の型を置き換えることができました。
class check_bool
{
public:
check_bool() : val( false ){}
check_bool( bool b ) : val( b ){}
operator bool() const { return val; }
operator int() const { return val; }
check_bool& operator=( const bool &src )
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
val = src;
return *this;
}
check_bool& operator=( const check_bool &src )
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
val = src.val;
return *this;
}
bool operator==( const bool &src ) const
{
return val == src;
}
bool operator==( const check_bool &src ) const
{
return val == src.val;
}
bool operator!=( const bool &src ) const
{
return val != src;
}
bool operator!=( const check_bool &src ) const
{
return val != src.val;
}
bool operator!( void ) const
{
return !val;
}
bool operator<( const bool &src ) const
{
return val < src;
}
bool operator<( const check_bool &src ) const
{
return val < src.val;
}
bool operator>( const bool &src ) const
{
return val > src;
}
bool operator>( const check_bool &src ) const
{
return val > src.val;
}
private:
bool val;
private:
bool operator<( const bool &src );
bool operator<( const check_bool &src );
bool operator<=( const bool &src );
bool operator<=( const check_bool &src );
bool operator>( const bool &src );
bool operator>( const check_bool &src );
bool operator>=( const bool &src );
bool operator>=( const check_bool &src );
};
template< typename Tp_ > class check_integer
{
public:
check_integer(){}
check_integer( Tp_ i ) : val( i ){}
operator Tp_() const { return val; }
check_integer& operator=( const Tp_ &src )
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
val = src;
return *this;
}
check_integer& operator=( const check_integer &src )
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
val = src.val;
return *this;
}
/** 前置インクリメント.
r = ++i;
int は前置と後置の判別のためのダミー
*/
check_integer& operator ++ (int)
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
++val;
return *this;
}
/** 前置デクリメント
r = --i;
*/
check_integer& operator -- (int)
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
--val;
return *this;
}
/** 後置インクリメント
r = i++;
*/
check_integer operator ++ ()
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
check_integer r( val ); // 壊す前の値を返さなければならない.
++val;
return r;
}
/** 後置デクリメント
r = i--;
*/
check_integer operator -- ()
{
// 書き込みに行く前にヒープ領域のチェック.
if( Allocator::CheckAddressIsFreeBlock( &val ) )
{
ASSERT( " ### Error : free memory access.\n" );
}
check_integer r( val ); // 壊す前の値を返さなければならない.
--val;
return r;
}
/** 単項マイナス
r = -i;
*/
check_integer operator - () const
{
return check_integer( -val );
}
/** 加算
r = i0 + i1;
*/
check_integer operator + ( const check_integer &src ) const
{
return check_integer( val + src.val );
}
check_integer operator + ( const int &src ) const
{
return check_integer( val + src );
}
/** 減算
r = i0 - i1;
*/
check_integer operator - ( const check_integer &src ) const
{
return check_integer( val - src.val );
}
check_integer operator - ( const int &src ) const
{
return check_integer( val - src );
}
/** 乗算
r = i0 * i1;
*/
check_integer operator * ( const check_integer &src ) const
{
return check_integer( val * src.val );
}
check_integer operator * ( const int &src ) const
{
return check_integer( val * src );
}
/** 除算
r = i0 / i1;
*/
check_integer operator / ( const check_integer &src ) const
{
return check_integer( val / src.val );
}
check_integer operator / ( const int &src ) const
{
return check_integer( val / src );
}
/** 剰余
r = obj % o;
*/
check_integer operator % ( const check_integer &src ) const
{
return check_integer( val % src.val );
}
check_integer operator % ( const int &src ) const
{
return check_integer( val % src );
}
/** 加算代入
i0 += i1;
*/
check_integer& operator += ( const check_integer &src )
{
val += src.val;
return *this;
}
/** 減算代入
i0 -= i1;
*/
check_integer& operator -= ( const check_integer &src )
{
val -= src.val;
return *this;
}
/** 乗算代入
i0 *= i1;
*/
check_integer& operator *= ( const check_integer &src )
{
val *= src.val;
return *this;
}
/** 除算代入
i0 /= i1;
*/
check_integer& operator /= ( const check_integer &src )
{
val /= src.val;
return *this;
}
/** 剰余代入
obj %= o;
*/
check_integer& operator %= ( const check_integer &src )
{
val %= src.val;
return *this;
}
bool operator==( const s8 &src ) const
{
return val == src;
}
bool operator==( const u8 &src ) const
{
return val == src;
}
bool operator==( const s16 &src ) const
{
return val == src;
}
bool operator==( const u16 &src ) const
{
return val == src;
}
bool operator==( const s32 &src ) const
{
return val == src;
}
bool operator==( const u32 &src ) const
{
return val == src;
}
bool operator==( const int &src ) const
{
return val == src;
}
bool operator==( const check_integer &src ) const
{
return val == src.val;
}
bool operator!=( const s8 &src ) const
{
return val != src;
}
bool operator!=( const u8 &src ) const
{
return val != src;
}
bool operator!=( const s16 &src ) const
{
return val != src;
}
bool operator!=( const u16 &src ) const
{
return val != src;
}
bool operator!=( const s32 &src ) const
{
return val != src;
}
bool operator!=( const u32 &src ) const
{
return val != src;
}
bool operator!=( const int &src ) const
{
return val != src;
}
bool operator!=( const check_integer &src ) const
{
return val != src.val;
}
bool operator<( const s8 &src ) const
{
return val < src;
}
bool operator<( const u8 &src ) const
{
return val < src;
}
bool operator<( const s16 &src ) const
{
return val < src;
}
bool operator<( const u16 &src ) const
{
return val < src;
}
bool operator<( const s32 &src ) const
{
return val < src;
}
bool operator<( const u32 &src ) const
{
return val < src;
}
bool operator<( const int &src ) const
{
return val < src;
}
bool operator<( const check_integer &src ) const
{
return val < src.val;
}
bool operator<=( const s8 &src ) const
{
return val <= src;
}
bool operator<=( const u8 &src ) const
{
return val <= src;
}
bool operator<=( const s16 &src ) const
{
return val <= src;
}
bool operator<=( const u16 &src ) const
{
return val <= src;
}
bool operator<=( const s32 &src ) const
{
return val <= src;
}
bool operator<=( const u32 &src ) const
{
return val <= src;
}
bool operator<=( const int &src ) const
{
return val <= src;
}
bool operator<=( const check_integer &src ) const
{
return val <= src.val;
}
bool operator>( const s8 &src ) const
{
return val > src;
}
bool operator>( const u8 &src ) const
{
return val > src;
}
bool operator>( const s16 &src ) const
{
return val > src;
}
bool operator>( const u16 &src ) const
{
return val > src;
}
bool operator>( const s32 &src ) const
{
return val > src;
}
bool operator>( const u32 &src ) const
{
return val > src;
}
bool operator>( const int &src ) const
{
return val > src;
}
bool operator>( const check_integer &src ) const
{
return val > src.val;
}
bool operator>=( const s8 &src ) const
{
return val >= src;
}
bool operator>=( const u8 &src ) const
{
return val >= src;
}
bool operator>=( const s16 &src ) const
{
return val >= src;
}
bool operator>=( const u16 &src ) const
{
return val >= src;
}
bool operator>=( const s32 &src ) const
{
return val >= src;
}
bool operator>=( const u32 &src ) const
{
return val >= src;
}
bool operator>=( const int &src ) const
{
return val >= src;
}
bool operator>=( const check_integer &src )
{
return val >= src.val;
}
private:
Tp_ val;
private:
bool operator!( void ) const;
};
typedef check_bool Bool;
typedef check_integer< s8 > S8;
typedef check_integer< u8 > U8;
自前のallocatorを実装しているのでチェック用の関数を仕込みます。
bool Allocator::CheckAddressIsFreeBlock( const void* p )
{
// 独自アロケータで実装されているので書き込みを行ったアドレスが
// ヒープのフリー領域や門番等のアドレスかどうかをチェックします
}
この仕組みを施してテストに回したところ、テストプレイで見事にチェックに引っかかりました。
担当者に報告してデバッガ上で再現してもらい確認してもらったところ、デバッグ機能側で解放されたオブジェクトに対して操作を行っていたらしいです。
デバッグ機能というところまで絞り込めたので、そこまで分かれば再現手順も絞り込めます。
担当者に修正してもらい、テストプレイでも再現しないことが確認できました。
#最後に
今回は本当に1バイトの変数の書き込みのタイミングだったのでバグが特定できましが、違う不具合だったらどう対処してたんでしょうか...
他にももっとスマートな解決方法があったもしれません。
そもそも論で言えばスマートポインタのような仕組みで設計、実装されていればというのもありますが、
起きてしまったバグはなんとか潰さなければなりません。
手法はどうあれ、問題を解決することが重要です。
限られたケースですが何かの参考にでもなればと思います。
そもそもバグに悩まされるのがイヤなので、こちらも参考にしてみてください。