C言語のメモリモデル



変数はメモリに確保される

 変数と言うとデータを入れておく箱と言う説明がよく出てきます。まったくその通りなのですが、C言語を使っていく上ではもう一歩踏み込んでコンピューター上でどうなっているのかを知っている必要があります。当たり前のことですが、変数はメモリ上に確保されます。コンピューター上のメモリはハード的には全部同じでもソフト的には領域を区切ってそれぞれに役割を割り振って動作していますのでその理解が重要です。
 きちんと理解すればC言語で起こりがちなメモリ関連の障害やバグに適切に対応できるようになります。



C言語のメモリの使い方

 C言語でメモリを理解する場合大雑把にコード(code)データ(data)ヒープ(heap)スタック(stack)の4つに分けて考えることができます(実際にはさらにいくつかに分かれる場合があります)。C言語の入門書ではこの話は滅多に見かけませんが初心者と中級者の差はここの理解にあると私は思っています。しっかり理解して下さい。
 C言語でかかれたプログラムは起動するとまずOSからプログラムのために一喝してメモリが割り当てられます。これを先ほどの4つに分けてプログラムが利用していきます。
 それでは順を追って見ていきましょう


コード (code)

 ここはプログラム自体が配置される部分です。Cプログラムはコンパイルされてマシン語のプログラムになりますが、それが配置される部分です。CPUによって順次読み込まれ実行されていきます。


データ(data)

 ここには初期化されるデータ。即ちグローバル変数やローカル変数など自動変数以外のものが割り当てられます。変数自体のアドレスは常に決まっていますし、プログラム起動時から終了時まで常に存在しています。そのためこの領域に確保される変数を「静的変数」という呼び方もします。


ヒープ(heap)

 処理系によっては存在しないのですが、mallocによって割り当てられるためのメモリ領域です。OSのメモリ管理に任せきりにすることも可能ではあるのですが、Cの言語仕様によりマッチするように、一喝してメモリを確保してその中からC言語がメモリ管理をして実行時に割り振っていく場合はそのために確保されたメモリをヒープと呼びます。
 mallocで確保したメモリは free すればその内容は保証されなくなりますし、再びmallocしても同じアドレスが確保されるとは限らないと言う点で、「動的メモリ管理」などともいいます。


スタック(stack)

 このスタックがC言語の真骨頂です。C言語がC言語たる所以ですのでしっかりと理解して下さい。
 通常のCPUにはマシン語レベルでスタックと言う機能が実装されており、スタックにメモリを割り振ってプログラムの中でデータの一時待避に利用されます。アルゴリズム的に言うスタックはFILO(First-In Last-Out)として格納したデータを格納した順とは逆順に取り出す機構です。通常何に最も威力を発揮するかと言うとそれは関数の呼び出しです。関数を呼び出すということはマシン語レベルでいうと、現在実行しているメモリ番地から関数の命令コードが格納されているメモリ番地にジャンプすることです。ただしこのままジャンプしてしまってはリターンできません。そこでどうするかというと現在実行しているメモリ番地をスタックに格納(push)しておき、リターン時にそのアドレスを取り出して(pop)元いた場所に実行を戻します。スタックの機構上関数の中から関数を呼んでもOKですね。
 さてこのような働きをするスタックですが、実はC言語ではこのスタックを自動変数のメモリ領域として利用しています。実際にどうしているかというと、関数開始時にその関数内で使う自動変数の分だけダミーのデータをスタックに押し込んで、その時スタック上に取られたメモリを自動変数用に利用します。そして関数終了時にこれらを取り除いてからリターンします。この機構により関数は再入などに関わらず常に自分だけが確実に使える変数領域を確保しているのです(再起が使えるのはスタックのおかげです)。
 しかしここで大きな問題が出てきます。通常スタックはあまり大きくありません(コンパイル時に必要とされるサイズを求めることはできないのでプログラマがサイズを管理しなければなりません)。そのため自動変数にあまり巨大な配列などを確保したり、再帰関数の終了条件の設定を間違えたりするとスタックオバーフロー(とどのつまりはスタックメモリが足りなくなってスタック自身はおろかデータやコードまで壊してしまう)を起こしてしまいます。先に書いたようにスタックには関数の戻り先と言う重要なデータが格納されていますからこれが壊れるとプログラムはかなりの確率で暴走します。自動変数にはあまり巨大なものは指定しないように心がけましょう(できればスタックサイズを把握しておきましょう、通常はリンク時にサイズ指定ができます)。大きな変数には static を付けるのが一番簡単な方法ですが、リエントラント(再入可能)にしたいなどの理由のときは関数のはじめで malloc して終わりで free するなどスタック以外のところにメモリを確保すれば対応できます。



余談

 Windows上でプログラムを組む人も多いでしょうからちょっと余談です。


386のスタック

 ご存知 Intelの x86 シリーズですが、386以降32bit化されてWindowsNTやWindows95など386ネイティブモードで動くOSも増えてきました。しかしこれらのOSは386の機能を十分に発揮していません。386にはスタック用に下方伸長型セグメントと言う機能があり、これを使えばスタックオーバーフローをOSが検知して自動的にスタックメモリを増やすことができます。しかしながら、それを実現するためにはデータセグメントとスタックセグメントを分離する必要が出てきます。ポインタ型を48bitにしてセグメントセレクタを含めればすむ問題なのですが、マイクロソフトの取った道はポインタの32bitの統一(フラットメモリモデルの採用)でした。C言語では静的変数も自動(動的)変数も同じポインタで扱いますから、データセグメントとスタックセグメントを同一のものにする必要があります。結果としてスタックサイズを事前に指定する必要があり、スタックオーバーフローするとプロセスが落ちるという今までと同じ問題を引きずることになりました。Visual-C++ や Borland-C++ の場合はデフォルトでスタックサイズは1Mのようです。使われないメモリは一応はスワップアウトの対象になるようですので、スタック関係のエラーが出るようなら多めに設定してもいいでしょう(まあ、その分OSに負荷かけてしまいますが)。

フラットモデルの限界

 上記のようにWindowsはポインタを32bitにすることで当時は無限と思えるメモリ空間をフラットモデルで実現しました。プログラマはDOS時代のセグメント管理の苦しみから開放されましたが新たな問題も出てきています。Windowsではポインタの最上位ビットをポインタかハンドルかを識別するために使っているためWindowsでは実質2Gのアドレス空間しかありません、そのため近年ではデータ自体が巨大になったのも手伝って、フラグメンテーションなどによって実メモリはあるのにマッピングする論理空間がないなどの障害が発生することもあるそうです。長時間走り続けるアプリを組む場合はこんなことまで考慮しないといけないみたいですね(メモリをロックしつづけなければOS側にガーページコレクションを行うタイミングを提供できます)。
 まあ、ポインタを48bitにしたところで 8086 時代の 64k の壁が 4G の壁になっただけでいずれは問題になったんでしょうけどね(フラットモデルなんてかっこいい名前を付けてもとどのつまりは32bit版スモールもですから)。まあ、MSの選択のおかげで少し早く問題が起こりつつあるようです。まあ元々 Intelのプロセッサはフラットモデルには向かない設計ですのでここに来て無理が出てしまったのでしょうね。そのうち 64bit のCPUに移行するでしょう(そのときOSはどう変わるんだ????)。


Windows95とかののメモリ管理

 Win95は386の上位互換CPUで動くわけですが、このメモリ管理は結構厄介です。まず386にはセグメントというメモリ管理機構がありますが、これはフラットモデルの採用によりほとんど利用されておらず、リング保護とアクセス制限に利用されているのみです。では何を使っているかというと386のもうひとつの機能ページングです。しかしながら386のページングの単位は4Kバイトという単位でしか行えません。ということは、GlobalAllocなどのAPIでメモリをFIX属性で確保していった場合OSは4Kバイト単位でしかガーベージコレクションを行えません(MOVABLE属性で確保してロックしたまましても同じこと)。また、ガーベージコレクションを行えるようにメモリハンドルを毎回アンロックするようにしてもまだ問題は残ります。Win32のSDKによるとメモリハンドルは 65,536個までしか確保できません(FIX属性で確保すれば制限は無い)。これは簡単に不足します。C言語自身がヒープを確保して自分でメモリ管理を行っても、C言語にはガーページコレクションの機能はありませんから、最終的にはフラグメンテーションから完全に逃れる術は無いのです。結局プログラマがケースバイケースで対応していくしかないというなんとも間抜けな状態になります。ガーベージコレクション機能を持つJAVAがもてはやされるのが分からなくも無いですね。ただ、C言語の名誉のために(?)補足すると、C言語ではガーベージコレクションの機能が無いがためにリアルタイム処理にも利用できるのです。結局言語というものはそれぞれ文化を持っており、得意分野があるわけですね。
 なんか話の方向がどんどんずれていますが、今回はここまでです (^^;
 ちなみに、私は386を富士ソフトウェア教育出版部の「ザ・386ブック」でお勉強しました (^^) 良い本ですよ、特に訳者のつけている注釈の的確さに感動しました。