[前へ]   [目次へ]   [次へ]

ポインタ(5)(C/C++)


今回は、ポインタ系のバグが原因で起こる現象を追跡していこうと思います。
バグのパターンについては前回解説しましたが、起こる現象は確率の違いはあれど、
基本的には全て同じ現象を発生させる可能性があります。

ポインタ系バグから発生する現象は厄介モノ揃いなので、
まずは「どういう現象が起こり得るのか」ということを覚えて、
現象を見た時に思い当たれるようになってください。

起きる現象と原因パターンの対応としては、おおむね以下のようになります。
ただし発生可能性については私見であり、統計情報など信用できるソースに基づいていないことをご了承ください。
可能性は なし→希少→少→若干→中→多→確実 の順で多いです。
可能性が高いものから順に疑った方がデバッグが効率よく進みます。

凡例:
現象の概要:起きた現象を簡単に示します。(地雷)と付いているものは即発現しないものです。
NULL:「無効ポインタ参照:NULL参照」の時にその現象が起きる可能性
未初期化:「無効ポインタ参照:未初期化のポインタを参照」の時にその現象が起きる可能性
範囲外:「無効ポインタ参照:範囲外のポインタを参照」の時にその現象が起きる可能性
破棄:「無効ポインタ参照:破棄済みの実体を参照」の時にその現象が起きる可能性
参照レベル:「参照レベルのミス」の時にその現象が起きる可能性
型不一致:「型の不一致」の時にその現象が起きる可能性
現象の概要NULL未初期化範囲外破棄参照レベル型不一致
他の変数が化ける(地雷)なし希少
アクセス違反確実若干
管理情報破壊(地雷)なし希少希少希少
ここで一番タチが悪いのは「他の変数が化ける」ケースです。
というのも「表面化しにくいことがある」上に「発現パターンが無数」という特性を持つからです。

ようするに、他の変数が化けるということは、それ以降のコードの実行結果は一切信用できなくなるということです。
たとえ a=5; /*aを使わない処理*/ if(a==5){ というコードであっても、
途中の /*aを使わない処理*/ の中で「他の変数が化ける」パターンが発現すれば、
後の if(a==5){ が真になることは保証されないわけです。

また、ループの継続条件に使う変数が化ければループ回数は読めなくなります。
必要な数のループをしないかもしれませんし、無限ループになるかもしれません。

このような時にポインタ系バグに思い当たれないと a に関する部分をひたすら探し回るなど、
くたびれ損なことを延々やるハメになるので、ポインタ系バグについては常時警戒しておきましょう。

C/C++は背後でポインタが動き続けているようなものなので、
「ポインタを使ってない」と思っていても案外あちこちで使っていたりします。
(これまでの講座を読み返してみれば、そこらじゅうでポインタに触れていることが分かるはずです)


アクセス違反は、その場で強制終了なのでタチはいいです。
デバッガは確実にその発生座標をつかむことができます。
ただし他の原因からの地雷発現でもアクセス違反は発生するので、
アクセス違反した位置が必ずしもバグっていることは保証されない点は留意しなければいけません。


また、管理情報破壊については、そう遠くないうちにバグるので気付かないという展開にはなりづらいです。
管理情報破壊が原因でバグった場合は特徴的な発現をするのですが、
デバッガを使用していないと捉えるのが非常に難しいので、
できるだけデバッガを使用し、以下の状況を見つけられるようにしておくといいでしょう。

●管理情報破壊が原因で起きる主な現象
DEP(データ領域実行防止)に捕まる
意味不明な座標で異常終了する
ブロックの終点で異常終了する
メモリリークが報告される
謎のループにハマる
デバッガで変数や関数の呼び出し履歴が正常に見れない

などが、管理情報破壊時に遭遇しやすい現象です。
異常発生時の実行位置が分からないと認識できないものが多いので、
デバッガがないとほとんど認識できないというわけです。

これらのうち、最初の二個は関数の呼び出し情報が破壊されたことに起因します。
管理情報の中には、「リターンアドレス」という「この関数はどこから呼び出されたか」を
保存している情報があります。
return はこの情報を元にして呼び出し元に実行座標を戻しているわけですが、
この情報が破壊されると、 return した瞬間に意味不明な場所に実行座標を移動してしまいます。
その結果、DEPに捕まったり、意味不明な場所を実行しようとして落ちます。
稀にですが、飛んだ時にどこかのコード内に偶然入ることもあり、
その場所の実行を続けようとしてさらなる謎現象に発展することもあります。

次の二個はローカル変数の管理情報が破壊されたことに起因します。
ローカル変数の管理情報が破壊されたことで、C++が行うオブジェクト破棄処理、
「デストラクタ」を正常に呼び出せなかったり、正常に実行できなくなったりします。
この結果、使用しているメモリを正しく解放できなくなったり(メモリリーク)、
呼び出そうとした瞬間に異常終了してしまったりします。

謎のループというのは、上記のような破壊による異常動作の結果、
無限ループが構成されてしまうようなケースです。
それほど遭遇確率は高くありませんが、発生時のタチが悪いので書いておきます。
例えばリターンアドレスを自関数を呼び出す直前に設定して return してしまえば、
直ちに同じ関数が再実行されます。状況が同じなので、また同じように管理情報を破壊して再実行・・・というようなケースです。
他にも通常ループが無限ループに変貌するケースもあります。

また、これらの結果、「スタックオーバーフロー」というエラーが発生することがあります。
「スタックオーバーフロー」発生時の対応はコンパイラに依存するものの、コンパイラによっては即強制終了である場合もあります。
その場合、エラーダイアログなどを一切出さない(問題が発生したため〜すら出ません)こともあるため、
「勝手に終わった」印象になる場合があります。
意図せず突然終了するような状況に遭遇した時は、「スタックオーバーフロー」の可能性も考慮しましょう。
また、発生をWindowsが検出した場合はエラーコード 0xc00000fd として「問題が発生したため〜」ダイアログが出ます。
(「問題が発生したため〜」ダイアログでのエラーコードの見方は 第18回 を参照)

可能性では困るので、使っているコンパイラの「スタックオーバーフロー」発生時の対応も調べておきましょう。
調べるには以下のようなコードを使います。
(システムの空きメモリを全消費し、再起動する必要があるかもしれませんので、
実行する前に未保存のファイルを保存しておくことを推奨します)

また、コンパイラの最適化機能は切っておいた方がいいでしょう。
EndlessLoop 関数で何もしていないため、消される危険性があります。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
#include <stdio.h>

void EndlessLoop(int n){
   
char a[1024*1024];//1MB
   
EndlessLoop(n+1);
}
int main(void){
   EndlessLoop(0);
//この呼び出しは無限にスタックを消費させます。
   //↓本来はこの位置に辿り着くことはなく、必ずスタックオーバーフローします。
   //しかし最適化しすぎるコンパイラだとEndlessLoopを消してくる可能性もあるので一応。
   
puts("スタックオーバーフローを起こせませんでした");
   getchar();
   
return 0;

次回はポインタ系バグの発生を追跡してみる予定です。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17