
ポインタのお話をする前に、C言語で作るプログラムの中でのメモリの利用について最低限のお話をまとめておきたいと思います。
前回お話した通り、C言語ではローカル変数、グローバル変数、static変数などの変数があります。 これらの変数を宣言して利用するとき、プログラムに割り当てられたメモリ上にデータを格納する場所を確保することになりますが、利用する変数の種類によって、C言語のプログラムが確保するメモリの領域が異なります。
また、プログラムを実行するとき、ハードディスクなどの補助記憶装置からより高速のメモリにプログラム本体がロード(読み込み)されてから動作します。
このようにプログラムを実行する上で様々な情報がメモリ上に存在することになりますが、大きく、プログラムはメモリを3つの領域(セグメント)に分けて管理しています。 3つのセグメントとは、スタックセグメント、テキストセグメント、データセグメントと呼ばれています。
スタックセグメントはプログラムを実行していく中で、ローカル変数や関数の引数、関数を呼び出したときの戻り値などが収められます領域のことです。
ここで、スタックとは、日本語で「積み重ねる」という意味があります。関数を呼び出すとき、プログラムは今実行している関数の場所と、いずれ関数が終了したときに戻る場所(関数を呼び出した場所)を知っておく必要があります。
メモリの中を図にしてみます。だいたいこんな感じ。関数mainから関数Aを呼び、関数Aの中から関数Bを呼んだときのメモリの中をイメージしています。
メモリの中でのスタックセグメント
「関数main」や「関数A」と書いてありますが、それは関数が作った変数のメモリ、とわかるように記載しています。 関数mainやAのプログラム本体は別のセグメント(テキストセグメント)に格納されています。
また、関数の中を実行しているとき、その関数のローカル変数や引数は、自身の関数を抜け出すまでは記録されていないといけません。
そのようなプログラムを進めるうえで必要な情報は、メモリのスタックセグメントという場所に、連続で一つのデータのカタマリとして積み上げるように記録していく仕組みとしています。 自身の関数だけで有効な変数について、スタックセグメントに最後に積み上げたカタマリを見ればよいのです。
関数が終了したら、スタックセグメントの最後に積み上げられているカタマリ(スタックフレームと呼ばれます)を除くことで、関数の呼び出し元の位置からプログラムを続けることができる、という仕組みです。
スタックセグメントはプロセスに割り当てられたメモリ領域のうち、大きいアドレスから小さいアドレスに向けて ”積まれて” いきます。
関数をどんどん呼び出していくと、スタックセグメントはどんどん拡張されていきます。 例えば下のように、関数を大量に呼び出して、スタックセグメントを大量に使った時、プログラムは異常終了します。 例えば下の要なプログラムを実行します。
#include <stdio.h>
#define CALL_LIMIT 1000
int a = 1;
void myfunc() {
int i[1024 * 64];
printf("%d 回目で呼び出された myfunc 関数の", a);
printf("ローカル変数 i のアドレス=%d\n", (long)&i);
a++; // グローバル変数の a を 1 加算
if (a < CALL_LIMIT) myfunc();
}
int main(void) {
myfunc();
printf("EXIT");
}
Visual Studio Community 2017 上でデフォルトオプションで実行したとき、下のように異常終了しました。
1 回目で呼び出された myfunc 関数のローカル変数 i のアドレス=2620652
2 回目で呼び出された myfunc 関数のローカル変数 i のアドレス=2356940
3 回目で呼び出された myfunc 関数のローカル変数 i のアドレス=2093228
正常終了であれば、1000 回 myfunc を呼び出したのち、”EXIT”というメッセージを表示して終了する作りとなっています。 ですが、実際には、3回ほど呼び出して異常終了しています。
仕組みは、main 関数の中から、 myfunc 関数を呼び出し、myfunc 関数の中から、自分の関数(myfunc関数)を再度呼び出ししています。 myfunc 関数を呼び出すたびに専用のローカル変数を格納する領域がスタックセグメントに作られます。
スタックセグメントが拡張可能なぶんを超えると、スタックオーバーフローが発生します。 オーバーフローは「溢れる」という意味ですが、その意味の通り、予めスタックとして利用可能なサイズを超えてしまうことを指します。
このとき、他のメモリ領域を破壊してしまわないように、保護措置として強制的にプログラムが終了されています。正しく終了していれば、main関数のなかの printf 関数で EXIT という文字が表示されるはずですが、実行結果の通り、表示されずにプログラムは終了してしまっています。
プログラムを実行するとき、プログラムのコードはハードディスクやSDカードなどから、最初にメモリ上に読み込み(ロード)されてから実行されます。 ハードディスクやSDカードは読み込み速度が遅いため、より早く読み書きができるメモリ上にコピーされて実行しています。
このメモリ上の領域はテキストセグメント、または実行コードが格納されることから、コードセグメントなどと呼ばれます。 またプログラムが実行されるときは、このコードセグメントの上を順番に読み取り実行されていきます。 関数を呼び出すときは、その関数が格納されているコードセグメントのアドレスにジャンプして読み取り、命令を実行していくという仕組みです。
この領域はスタックセグメントの拡張やプログラマが誤ってポインタを操作して破壊しないよう、読み込み専用で保護された領域となっています宇。
プログラムの中で使うグローバル変数やスタティック変数、そのほか動的に取得した大きなメモリ領域が格納されます。 この動的に取得した大きなメモリはヒープ領域と呼ばれるところに格納されます。 動的に大きなメモリを確保する関数で malloc 関数と呼ばれる関数が使われます。利用方法は別途お話したいと思います。
スタックセグメント、テキストセグメント、データセグメントの3つはプロセスに割り当てられた仮想アドレス空間の中で、下のように利用されます。
スタックセグメント、テキストセグメント、データセグメントのイメージ
データセグメントのヒープ領域はアドレスの小さいほうから大きいほうに向けて確保されていきます。
今までお話したセグメントの領域外にポインタ操作をしてアクセスをすることで、OSが異常なアクセスと見なして強制終了することがあります。 これはセグメンテーションフォルトと呼ばれます。 セキュリティ上、プロセスに割り当てられたメモリ外へのアクセスができてしまうと非常に危険なので、最近のOSでは自動的に範囲外のアクセスを検知してプログラムを強制終了させています。
プログラムを実行していて、強制終了した場合、ポインタの操作に失敗して想定外のメモリアドレスへのアクセスが行われたことで、このセグメンテーションフォルトが発生している可能性があります。
今回は具体的なポインタ操作よりも、その前提となるセグメントなどの基礎知識部分のみをまとめました。 ポインタによりメモリアドレスを操作するとき、メモリの仕組みがわかっていることで、ある程度処理が見通しやすくなるかと思います。
更新日 | 更新内容 |
---|---|
更新なし |
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: