2013年4月18日木曜日

【C言語】free関数で落ちる

ちょっとハマりかけたのでメモ。

free()関数の実行で落ちる

とあるプログラムで、論理エラー終了した際に実行する関数内に、プログラム内で malloc した、あるいはする可能性のある領域を漏れなく解放する処理があって、そこの free の処理で落ちた。サンプルでソースを書くとこんな感じ。
→そんなことより答えへ急ぐ

--------------------ココカラ---------------------
#include <stdio.h> #include <memory.h> /*--------定数----------*/ #define COLUMN_MAX_LEN 50 /*--------------------------------*/ /* うっかりグローバル変数宣言 */ /*--------------------------------*/ char *p1; char *p2; /**********************************/ /* 初期処理 */ /**********************************/ int init(){ // いろいろと初期処理する // 初期化とかもする p1 = NULL; return(0); } /**********************************/ /* 終了処理 */ /**********************************/ void error_end(int code, char* msg){ // いろいろとログ吐いたりなんだかんだする // 使用していた領域解放 if(p1 != NULL){ free(p1); } if(p2 != NULL){ free(p2); } return(code); } /**********************************/ /* main */ /**********************************/ int main(){ // 初期処理 if(!init()){ // 初期処理でなんかエラー発生 return error_end(9, "init") } // 領域確保 p1 = malloc(sizeof(COLUMN_MAX_LEN)); if(p1 == NULL){ return error_end(1, "p1:alloc"); } p2 = malloc(sizeof(COLUMN_MAX_LEN)); if(p2 == NULL){ return error_end(2, "p2:alloc"); } // 主処理とかが続く hoge(); piyo(); // 終了 exit(0);
--------------------ココマデ---------------------
(ちなみにソースはこれでHTML化。一応、宣伝)
コンパイルとかしてなくて殴り書いたからテキトーだけど、とりあえずこれ、最初のinit()で何かしらエラーが起こって error_end() に飛ぶと、free()で落ちます。 なぜ落ちるか、答え。


原因はポインタ変数の初期化漏れ

当たり前っちゃ当たり前だけど、free関数はmallocした領域に対して実行しないと落ちる。なので、error_end() で free()する前に、「ポインタ変数にアドレス値が割り当てられてるか」ってのをチェック(p1 != NULL )してるんだけど、p1 についてはきちんと init() で初期化しているから良いとして、 p2 については初期化漏れ。

C言語は、その他多くの素敵な言語と違って、宣言と同時に初期化なんてしてくれないので、「char *p1;」とした段階で、p1には何か変なゴミが入っている。だから、init()でエラーが発生してerror_end()を実行する段階で、p2はアドレス値ではない何かが入っているので NULL じゃない。
NULLじゃないから、if( p2 != NULL ) の条件が true になっちゃって、p2 を free() しようとするんだけど、そもそも p2 に入ってるのはゴミだから、free()が困る。で、落ちる。

という罠でした。
アドレス操作は便利だけど、こういうことがあるから厄介ですね。デバッグもしにくいし。気を付けよ。


■おまけ
ポインタが全然わかんないけど、やっぱりきちんと理解したい…という人には個人的にこれが超お勧め。
C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

1 件のコメント:

みょ~ん さんのコメント...

はじめまして。昔、C言語を独学した者です。
free関数にNULLを入れると何もしない、ということは知っていましたが。
念のため、freeは領域を「解放」するのであって「開放」するわけではないですよね?開放と言う言葉は、情報を開示するような意味合いになりますから。

わざわざコメントさせていただいたのは、「main 関数のargv の型 char *argv[] またはchar **argv に、何故constが入っていないのか?」と疑問に思って調べたら、そもそもC言語の仕様で、argvの内容は書き換え可能でなければいけない、と明記されてる、と知って慌てたからです。

しかし、どう考えても、argvの指す領域は、プロならともかく、素人が自分勝手に書き換えていい代物ではない筈だ、getoptなどがargvを書き換えているらしいが(これもソースを良く見ないと分からないが)、それならそれでgetoptに任せるのが筋だ。などなど考えていったとき・・・。

argvが書き換え可能だとすれば、素人がargvの中身を見るだけで書き換えるつもりがないときに安全にアクセスするためには、argvの内容をコピーした配列(例えば myargvとかの名前を付けて)を作って、char const * const * const myargv という型にするとかしないと危ない。
ところが、ここでmallocなどを使ってしまうと、プログラムが異常終了した場合にfreeしそこなってしまう可能性もあって、それもまずいというジレンマが出てきたからです。
もちろん、Cではキャストしてconstを取ってしまうことは可能だから万全には程遠い(C++では、const_cast を使うことで用心できるが、可能であることには違いない)。

ということで、本来の記事の意図とは異なるかもしれませんが、素人はargvをどう扱うのが良いか?のプロの意見が得られるとすれば幸いだから、もし暇なときにでもご返事していただくとあり難いのです。もちろん、無視していただいてもかまいません。