ポインタとかメモリについて
int *p; // ポインタを宣言
int x = 10;
p = &x; // pにxのアドレスを割り当て
- ポインタを宣言する
- 変数を宣言する
- 変数に値を代入しただけだとその値はスタックメモリに割り当てられ、関数が終了するとメモリが開放され、値がなくなる。また大量のデータを保持するのには向いていない。
- ポインタに変数のメモリアドレスを割り当てる
- ポインタを通じてデータにアクセスする
- 乱暴にいうと、ポインタは動的メモリを使うデータかどうかを決めるものでしかない。
メモリ領域について
- 動的メモリ領域(ヒープ)
- スタックメモリ領域
- その他のメモリ領域
動的メモリ領域(ヒープ)
ヒープメモリは、プログラムが実行されている間に必要に応じて動的にメモリを確保するための領域です。メモリを確保するタイミングとサイズをプログラム側で制御できるため、実行時にサイズが決まるデータ(可変長配列やデータ構造)に適しています。
- 特徴:
- ヒープメモリは、プログラムの開始時に確保され、プログラム全体で共有されます。
- メモリの確保と解放をプログラマが手動で管理します(例えば、C言語では
malloc
やfree
を使います)。 - メモリのサイズは、プログラムの実行中に動的に決めることができます。
- 解放しないままでいると、メモリリークが発生し、システムリソースが無駄に消費されます。
- 使用例:
ヒープメモリは、例えば以下のように動的に確保されます。int *p = (int *)malloc(10 * sizeof(int)); // ヒープに10個分のint型メモリを確保
- ヒープメモリの特徴的な問題:
- メモリリーク: 確保したメモリを
free
で解放しないと、メモリがずっと占有されたままになります。 - フラグメンテーション: メモリの断片化が進むと、十分なメモリがあるにもかかわらず大きな連続したメモリが確保できないことがあります。
- メモリリーク: 確保したメモリを
スタックメモリ領域
スタックメモリは、関数の呼び出しとともに自動的にメモリを確保し、関数が終了すると自動的に解放されるメモリ領域です。主にローカル変数や関数の引数などがスタックに格納されます。スタックメモリは、関数がネストされるたびに積み上がり(LIFO:Last In, First Out)、関数が終了すると積み下げられます。
- 特徴:
- スタックメモリは関数のスコープ内でのみ有効で、関数が終了すると自動的にメモリが解放されます。
- メモリの確保と解放は自動的に管理されるため、ヒープメモリのように手動で解放する必要はありません。
- スタックは高速で効率的ですが、サイズが限られています。非常に大きなデータを格納するには向いていません。
- 使用例:
スタックメモリに格納される変数は次のように宣言されます。void myFunction() { int localVar = 10; // スタックメモリに格納されるローカル変数 }
- スタックメモリの特徴的な問題:
- スタックオーバーフロー: スタックメモリは容量が限られているため、深い再帰呼び出しや大きな配列の宣言でスタックがいっぱいになると、スタックオーバーフローが発生し、プログラムがクラッシュします。
ヒープとスタックの違いのまとめ
項目 | ヒープメモリ | スタックメモリ |
管理方法 | プログラマが手動で管理 | 自動で管理される |
メモリ確保 | 動的に確保される | 関数呼び出し時に自動で確保 |
メモリ解放 | free で明示的に解放が必要 | 関数終了時に自動で解放される |
速度 | 遅い(メモリ確保・解放に時間がかかる) | 高速 |
主な用途 | 大きなデータ構造、実行時にサイズが変わるデータ | ローカル変数、関数の引数など |
メモリの限界 | システムの物理メモリに依存 | サイズに限界がある |
問題点 | メモリリーク、フラグメンテーション | スタックオーバーフローの可能性 |
その他のメモリ領域
スタックやヒープ以外にも、プログラムが利用するメモリ領域にはいくつかの種類があります。以下に、主なメモリ領域を説明します。
データセグメント(静的領域、グローバルメモリ領域)
この領域は、静的変数やグローバル変数が格納される領域です。プログラムの実行開始時にメモリが確保され、プログラムの終了まで保持されます。データセグメントはさらに以下の2つに分けられます。
- 初期化済みデータ領域:
ここには、初期化されたグローバル変数や静的変数が格納されます。int globalVar = 100; // 初期化済みのグローバル変数
- 未初期化データ領域(BSS領域):
ここには、初期化されていない静的変数やグローバル変数が格納されます。この領域は、プログラムの開始時にゼロで初期化されます。static int uninitStaticVar; // 未初期化の静的変数(BSS領域に格納)
- 特徴:
- 静的メモリ領域はプログラムの全期間にわたってデータを保持しますが、メモリ消費が大きくなる可能性があるため、大量のデータをここに格納するのは避けるべきです。
テキスト領域(コード領域)
この領域は、プログラムの実行コード(命令)が格納される領域です。プログラムが読み込まれると、CPUがこの領域に格納された命令を実行します。この領域は通常読み取り専用であり、プログラムの命令を変更することはできません。
- 特徴:
- この領域はプログラムの命令を含むため、セキュリティ上重要です。例えば、コードインジェクションなどの攻撃は、この領域に不正なコードを書き込もうとするものです。
メモリマップトファイル領域
- メモリマップトファイル(Memory Mapped Files)は、ファイルをメモリにマップすることで、ファイルの内容に直接メモリアクセスを行えるようにする領域です。大きなファイルを効率的に扱う場合に使用されます。
共有メモリ領域
- 共有メモリ(Shared Memory)は、複数のプロセスが同じメモリ領域にアクセスできるようにする仕組みです。異なるプロセス間でデータをやり取りする際に使用されます。
いつヒープメモリを使うの・・・?
グローバル変数はデータセグメントにあるし、ローカル変数はスタックメモリにあるし、いつポインタを宣言する必要があるんだ・・・!
- 可変長のデータや動的にサイズが決まるデータを扱う場合
もし、プログラムの実行中にデータのサイズが決まらない場合や、非常に大きなデータを扱う場合は、ヒープメモリを使って動的にメモリを確保する必要があります。
たとえば、ユーザーからの入力に応じてメモリを確保したい場合や、実行時に必要なメモリサイズが分かる場合などです。これにはmalloc
やfree
を使います。
int *p = (int *)malloc(100 * sizeof(int)); // ヒープに100個分のint型のメモリを確保
if (p != NULL) {
// 確保したメモリを使用
free(p); // ヒープメモリを解放
}
このように、ヒープメモリを使って動的にメモリを確保する場合は、確保したメモリを使い終わったら解放する必要があります。これを怠ると、メモリリークが発生し、システムのメモリが不足する原因となります。
- 関数間で大きなデータをやり取りする場合
スタックメモリはサイズが限られており、大きなデータや長期間保持するデータを扱う場合には、ヒープメモリを使う必要があります。関数のスコープを超えてデータを保持したい場合や、関数間でデータをやり取りする際に大きなメモリが必要なとき、動的にメモリを確保することが一般的です。
int* createArray(int size) {
return (int *)malloc(size * sizeof(int)); // ヒープメモリに動的に配列を確保
}
int main() {
int *array = createArray(100); // 100個分の配列を動的に確保
if (array != NULL) {
// 配列の処理
free(array); // ヒープメモリを解放
}
return 0;
}
- ライフサイクルが長いデータを扱う場合
スタックメモリ上のローカル変数は、関数が終了するとメモリが解放されますが、関数が終了しても保持し続けるデータや、関数間で共有する必要があるデータはヒープメモリに格納する必要があります。
バッファオーバーフロー
バッファオーバーフローとは、バッファに対して確保された領域を超えるデータを書き込むことによって発生します。バッファは、特定のサイズ(メモリ領域)だけが確保されているにもかかわらず、そのサイズを超えるデータを書き込もうとすると、隣接するメモリ領域を上書きしてしまい、予期しない動作を引き起こします。
この現象は、メモリの破壊やシステムの不安定化、さらに悪意のある攻撃者による任意コードの実行に繋がる可能性があります。バッファオーバーフローは、長年にわたり、セキュリティ脆弱性として悪用されてきました。
リスク
- メモリ破損:
- バッファの境界を超えて書き込まれたデータが、隣接するメモリ領域のデータやコードを上書きしてしまいます。これにより、プログラムが予期しない動作を引き起こしたり、クラッシュしたりします。
- セキュリティ脆弱性:
- バッファオーバーフローは攻撃者に悪用されやすい脆弱性です。攻撃者が意図的に過剰なデータをバッファに書き込み、スタック上の制御データ(例えば、リターンアドレス)を上書きすることで、任意のコードを実行できる状態になります。これにより、システム全体が乗っ取られる可能性があります。
対策
- 入力値のサニタイズ
- バッファのサイズを常に意識する: バッファに書き込むデータのサイズがバッファの容量を超えないように注意します。
- 安全な関数を使う:
strcpy
やgets
のようなサイズ制限のない関数ではなく、strncpy
やfgets
のようにサイズ指定ができる関数を使います。 - 動的メモリの管理: 動的メモリを使う場合は、必要なサイズを適切に確保し、使い終わったら
free
でメモリを解放します。また、確保したメモリサイズを超えた操作を行わないようにします。 - スタックカナリア: 現代のコンパイラとOSでは、スタックカナリアという保護機構が導入されています。スタックカナリアは、スタックフレーム内に特定の「カナリア値」を挿入し、その値が破壊されていないかを関数が終了する際に検証します。これにより、スタック上でバッファオーバーフローが発生しても、プログラムが不正な動作を検知してクラッシュします。
- アドレス空間配置のランダム化: ASLR(Address Space Layout Randomization)は、プログラムのメモリレイアウトを実行ごとにランダム化することで、攻撃者がリターンアドレスやシェルコードの位置を予測しにくくするセキュリティ技術です。これにより、バッファオーバーフロー攻撃が成功しにくくなります。