Version 6.14
オーバーフローに要注意!
「今回は、前回の続きのようなそうじゃないような」
『何それ』
「今回は〈オーバーフロー〉って問題について紹介するんだけどそれが前回
の演算子関係と少なからず関係があるんだよね」
『でもずばり関係してる、ってわけでもないのね』
「そゆこと。まずはオーバーフローの例から見てもらいましょう」
void CDebugDlg::OnButton1()
{
char ch = 10;
for( int i = 0; i < 10; i++ )
{
TRACE( "%d ", ch );
ch *= 10;
}
// 10 100 -24 16 -96 64 -128 0 0 0
}
『えーっと、 10 ずつ増やしていったら……値が変になってるね』
「そう、これは char の上限 127 を超えたから。こういう〈変数の上限を
超えること〉を〈オーバーフロー〉って言います。日本語だと〈桁溢れ〉。
overflow って言うのは〈溢れる〉って意味だから」
『お風呂に入ったらお湯がざばー、って感じね。でもさ、このエラーって当
たり前じゃない』
「当たり前なんだけど、だからこそ気を付けないと。たとえば」
char Plus( char p_chLh, char p_chRh )
{
return p_chLh + p_chRh;
}
「って関数のことを考えてみて。 char の上限は 127 。もし p_chLh に
127 、 p_chRh にも 127 が入ってたら?」
『あ、戻り値で溢れちゃうね』
「変数だとどんな値が入ってるかわかんないから、こういうことがあるんだ
よね」
『こういうのって char だからなんじゃないの? int だったら大丈夫そう
だけど』
「うん、 int だと安全性は増す、けど、可能性はなくならないから。たと
えばさっきの for みたいな例。ちょっとミスして 100 回ループさせたり、
while なんかでその都度ループ回数が違うときなんかに」
『うっかり無茶苦茶ループさせちゃったりしたらこうなっちゃうわけね』
「ただ、この種類のオーバーフローは、直接的な問題はないかな」
『ないの? あのさ、オーバーってことは、他の変数が置いてあるメモリ領
域とかに書き込んじゃったりしない?』
「うん、それはないんだよね。実行するときにちゃんと char に入るように
切りつめられちゃうから。メモリ見るとわかるよ」
『 TRACE でアドレスだして、メモリ見て……』
「そんなに問題ない、っていうのは整数リテラルだと分かりやすいかも」
void CDebugDlg::OnButton1()
{
char ch = 1000000000;
// warning C4305:
// 'initializing' : 'const int' から 'char' へ切り詰めます。
}
『あ、エラーだ』
「エラーじゃないよ、これは警告。値が変になってますよーって教えてくれ
るだけで、コンパイルは通ってるわけ」
『ってことは、こういうのってそんなに危なくない?』
「一応ね。ビット操作っぽい理由でわざとこういうことすることもあるし。
でも直接問題ないってだけで、間接的に問題になることは多いよ」
『そりゃそうよ、変な値になっちゃうんだもの』
「というわけで、もう少し詳しく見ていきましょう。オーバーフローになる
可能性があるのは、なんと言っても四則演算」
『引き算とかはならなさそうだけど?』
「 char に入れるときに 127 - ( -127 ) だったら?」
『げ、そういえば - と - で + ね』
「四則演算するときには、オーバーフローの可能性を常に気にした方がいい
かもね。調べ方は、こんな感じ」
void CDebugDlg::OnButton1()
{
char ch1 = 100;
char ch2 = 100;
if( ( 127 - ch1 ) < ch2 )
{
TRACE( "オーバーフローします。\n" );
}
}
『んーと、 127 は char の最大数だよね。そっか、それから引けば、残り
が分かるんだ』
「そう、こうすれば〈余ってる範囲〉が分かるから、そこに入るかどうか
チェックすればいいわけ」
『……でもさ、はっきし言って計算するたびにこれするの大変だよ?』
「実際そうだね。そうなると、普通は int とかのある程度大きなサイズの
変数を使っておいて、あとは場面を絞ってチェックする感じかな」
『場面ってどんな?』
「まずはさっき言った for とかのループ系」
『 10 が 100 に変わってもかなり違うもんね』
「次に複雑な計算。でもこれはあんまないか」
『ないの?』
「普通 int じゃなく double って実数型使うからね」
『 double ってあんま使ったことない……』
「あと重要なのは入力系」
『入力って言うと、ダイアログボックスとかに?』
「そうそう。エディットボックス貼り付けて入力してもらう部分。こっちは
少ない数字を想定してても、使ってる人が誤ってかわざととかで大きな数字
入れちゃったら」
『そりゃまずいね』
「ま、それもあって MFC には自動チェックの機能が着いてるんだけど。ダ
イアログエディタでエディットボックスを貼り付けて、そのメンバ変数をク
ラスウィザードで作ってみて。変数名は適当、【変数のタイプ】は int 」
『ほい。あ、【 MFC ClassWizard 】の下の方に【最小値】と【最大値】っ
てのができた。これで上限と下限を決められるの?』
「そ。とりあえずこれで制限できるから。こういうのも使って防ぐことにな
るかな」
『オーバーフロー自体はそんな危なっかしくないね。ん? なんか罠の予
感。そういえばさっき〈この種類のオーバーフローは〉とか言ってたね』
「そう、実は他に、危険なタイプのオーバーフローがあるんです。それは、
ポインタや配列を使ったときのオーバーフロー。まずはこの例」
void CDebugDlg::OnButton1()
{
char ch[3];
TRACE( "%X\n", ch );
strcpy( ch, "123456789!" );
}
『ビルドして実行、あ、エラー』
ハンドルされていない例外は Debug.exe (MFC42D.DLL)にあります。
0xC000005: Access Violation。
『ってゆーか、 strcpy() って、文字列コピーするランタイムだよね。こ
れって 3 文字しか入らないとこに 10 文字入れてるんだから、エラーにな
るの当たり前のような……』
「そう、確保してるサイズを超えて書き込んじゃってます。アドレス表示さ
せてるのは、実際にメモリのその部分を見て欲しいから」
『あ、そっか。 Ver 4.02 ( No.052 ) とかでやったのね。ブレークポイン
トを置いてアドレスをメモリウィンドウに入れてリターン』
0064F4F4 31 32 33 34 35 36 37 38 39 21 00 00 9C 74 43 5F 9C F7 64 00
123456789!..徼C_戈d.
『あーあー、思いっきりはみ出ちゃって……あ、これがオーバーフローって
事?』
「そゆこと。実際の配列のサイズを超えて書き込んじゃうのもオーバーフ
ローの一種で〈バッファオーバーフロー〉って言います」
『バッファってどっかで聞いたような……』
「 buffer は〈緩衝材〉って意味で、とりあえず値を受け取るための変数、
って意味。でも実際は、文字列を受け取る配列だけを〈バッファ〉って言う
ことが多いかな」
『サンプルとか見ても、文字配列が buf って変数になってること多いけ
ど、それ?』
「そうそれ。もひとつ付け加えると、配列を超えた部分にアクセスすること
をオーバーランって言います」
『それはフツーな用語だね』
「ま、そういうのもね。で、このバッファオーバーフローは絶対回避しな
きゃいけない問題。特に、運悪くエラーにならないことがあって、それで
見つからない場合が多いんだよね」
『こういうエラーが出たらむしろ幸運?』
「まー幸運ってゆーか……実際にはエラーが出る前に回避しないとね。特に
バッファオーバーフローは〈わざと〉起こされることがあるから」
『わざと??』
「バッファオーバーフローの対策をしてないと、わざとながーい文を入力さ
れて、それで普通の変数とか、プログラムを動かすのに重要な変数とかが書
き換えられちゃうことがあるんだよね」
『げ、それってかなりヤバくない?』
「やばいよ。だから、バッファオーバーフローの対策は絶対しないとダメ」
『対策ってゆーと、文字列のサイズを調べればいいんだよね』
「そう。書き込み先の配列、この例だと ch だね、そのサイズが分かってれ
ば足してそのサイズに入り切るか調べればいいわけ」
『あ、分かればいいけど、ポインタとかになってて分かんないときは?』
「分かんなかったらアウト。だから、分かるようにする必要があるんです。
たとえば……そうだね、 CDebugDlg::OnButton1() の中に、ダイアログのタ
イトル char 配列で受け取って TRACE() で出力するコード書いて」
『ほいほい、 GetWindowText() だったよねウィンドウタイトル取得するの
って。 CDebugDlg は CWnd のも使えるから、そのまんま呼んで……あ、
そーか』
void CDebugDlg::OnButton1()
{
char ch[256];
GetWindowText( ch, 255 );
TRACE( "%s\n", ch );
}
『関数に渡すときは、配列のサイズ渡すんだ。そうすればはみ出ることない
もんね』
「そゆこと。文字列を返すような関数を作る時とかはこれを守るようにね」
『守ってない strcpy() とかは?』
「もちろん使う時にサイズのチェック。ってゆーか、整数のオーバーフロー
の時と同じで、別に切り捨てられたからいいってもんじゃないでしょ」
『そっか、上の GetWindowText() でも、オーバーランしてないからって切
り捨てられてたらダメだもんね』
「ま、場面場面で違ってくるけど、この例だと GetWindowTextLength()
って API があって」
『へー、これで文字列のサイズを受け取れるんだ。でも配列じゃどーしよー
もないじゃん』
「まだ教えてないけど、自由にサイズを決めて配列もどきを作ることができ
るから、それで受け取ればOK。その機能が入ってるのが CString 」
『そういえば CString なら strcpy() 使わなくても文字列のコピーできる
ね』
「さっきの文字列を返すのでも、 CString は引数から参照を通してや戻り
値として返せるから、そっちの方が安全かつ便利かもね」
『結局はそういう便利なのを使おうって話ね』
「そゆこと」
/*
Preview Next Story!
*/
『そういえばバッファオーバーフローってよく聞くよね』
「そう、この対策をしてないアプリが多くてね」
『有名なの多いよね。それがさっき言ってたわざとなんかされちゃう?』
「そゆこと」
『結構てきとーな作りなの多いのねー』
「というわけで次回」
< Version 6.15 バグをコンパイルタイムエラーで防ぐ! >
『につづく!』
「でも有名アプリがそうだからって、自分のもいいとか」
『思わないだよーだ』