プログラムがメモリをどう使うかを理解する(1)
この記事の狙い
この記事は、端的に言えば
この図が言わんとしていることを理解できるようになるための解説を目指しています。昨今のプログラミング環境において、メモリの管理方法やその実態は、詳細を知らずとも目的を達成できるようになっています。といっても、実際にはメモリは無尽蔵に使えません。制約が厳しい環境下で動かさねばならないプログラムもありますし、多少潤沢に使える環境であっても、無駄に浪費するよりは、必要最低限のメモリで効率よく動作するプログラムの方が、多くの場面においては良いプログラムと言えるでしょう。
メモリのことなど知らなくてもプログラムを書けるのは一つの理想ではありますが、現実的にはその裏に隠されている(抽象化されている)仕組みを知っておいたほうが有利です。また、昨今のレトロゲームにおけるタイムアタックで駆使されるメモリ書き換えのテクニックなども、何故そういったことが可能なのかを知ることで、現代のアーキテクチャに通じる知見が得られるかもしれません。
この連載記事では、C++で記述したプログラムにデバッガをアタッチして、どのような挙動をしているのかを探りつつ、仕組みを明らかにしていきたいと思います。Visual Studioを使った手順や画面を載せていきますので、デバッガの使い方としても参考になるかもしれません。
メモリとは何か
物理的には、これです。
これや、これに類するもの、相当するものが、あらゆるコンピュータに搭載されています。
そして、この中身はこうなっています。
どんなプログラムにも、このような16進数の羅列と、個々の値に対するアドレスからなるブロックが割り当てられており、これをちまちまと、整合性を保ちつついじくり、目的を達成することがプログラミングであると言えます。
メモリの覗き方
既製のアプリケーションのメモリを覗く手段もあるにはありますが、規約的にNGな製品も多いので、ここでは自分で書いてビルドしたアプリケーションを対象にします。
- Visual Studio で C++ のコンソールアプリケーションを開発するプロジェクトを作成
- 生成されたコードのmain関数内にある "Hello World" を出力する行にブレークポイントを置く
- 行番号の左隣の列をクリックすると赤丸が付く
- F5キーを押して実行すると表示処理の前に実行が停止する
- メニューから「デバッグ→ウィンドウ→メモリ→メモリ1」を開く
これで覗けます。覗けますが、ここに表示される内容は恐らく多くの人にとって意味不明な情報でしょう。なので、変数を1つ定義して、そこを足がかりに挙動を明らかにしていきます。
int main()
{
int a = 0xDEADBEEF;
std::cout << "Hello World!\n";
}
int型の変数aを追加します。値は16進数で分かりやすい方が良いので、A~Fの文字で作れる意味のある文字列になる値を代入しましょう。この DEADBEEF という値は、デバッグ時や初期化漏れなどを分かりやすく検出するために、よく使われます。
今度は変数aの定義行にブレークポイントを置き、再度F5で実行します。動作が停止したら、変数aのために確保されている領域を探してみましょう。メニューから「デバッグ→ウィンドウ→イミディエイト」を開きます。
イミディエイトウィンドウは、値として評価できるプログラムコードを入力すると、その評価結果を表示してくれる超便利ツールです。(ただし、C++だと思った通りに動作してくれないこともあります。C#だとかなりの精度で動作します)ここで、変数aのアドレスを得るために &a
と入力してEnterキーを押下してみましょう。
このようにアドレスが表示されますが、恐らく実行環境毎に異なる値になるはずです。ここでは 0x00d3f950
という値が得られたので、このアドレスをメモリウィンドウのアドレス欄に貼り付けてみましょう。
フフフフという文字列が不気味ですが、ここが変数aのために確保された4バイトです。信じられませんか?では停止しているプログラムをステップ実行してみましょう。F10を押してみてください。プログラムが1行だけ実行されます。すると……
はい、書き換わりました!今、代入処理が実行されたことにより、0x00d3f950
から4バイトの値が書き換わりましたね?まさにメモリを操作する瞬間を目の当たりにできたわけです。おめでとうございます。
メモリウィンドウでの表示上は、DEADBEEF が入れ替わって EFBEADDE になっています。これは、私が使っている Intel のプロセッサがリトルエンディアンを採用しているからですね。小さい桁の1バイトから順番に、4バイトに渡って値が記録されるため、このようになります。
今ここで明らかにしたことは、プログラム中で定義したあらゆる変数は、コンパイラやOSの助けを借りて、メモリ上の特定の領域を与えられる、ということです。大昔の原始的な開発環境においては、メモリ上の領域をどのように使うかをプログラマがいちいち定めて管理する必要がありました。それと比べれば、現代のプログラミング言語は考えるべきことが大きく削減され、達成したい目的に集中しやすくなっていると言えます。
今回のまとめ
- すべてのプログラムにはメモリブロックが与えられ、それを操作することで目的を達成する
- 変数を定義することで、メモリ上の特定の領域がその変数用のスペースとして与えられる
今回はC++を使用しましたが、原則的に他の言語においても、同じような仕組みが上位レイヤーで実現されているか、より高度な概念で記述されたコードが、最終的にはC/C++で記述されたプログラムに落とし込まれることによって、メモリを利用していることには変わりありません。
今後、より複雑な挙動や仕組みについても、メモリを覗きつつ暴いていきたいとおもいます。お楽しみに。
続き
Discussion
シリーズの"続き"のリンクが壊れています
ご連絡ありがとうございます。
昨夜はzennの記事全体でそういう現象が起きていたようです。
今は直っていると思いますので、ご確認いただければ幸いです。