実際にはシステムに依存しますが、仮想メモリを備えた最新の OS は、プロセス イメージをロードして、次のようにメモリを割り当てる傾向があります。
+---------+
| stack | function-local variables, return addresses, return values, etc.
| | often grows downward, commonly accessed via "push" and "pop" (but can be
| | accessed randomly, as well; disassemble a program to see)
+---------+
| shared | mapped shared libraries (C libraries, math libs, etc.)
| libs |
+---------+
| hole | unused memory allocated between the heap and stack "chunks", spans the
| | difference between your max and min memory, minus the other totals
+---------+
| heap | dynamic, random-access storage, allocated with 'malloc' and the like.
+---------+
| bss | Uninitialized global variables; must be in read-write memory area
+---------+
| data | data segment, for globals and static variables that are initialized
| | (can further be split up into read-only and read-write areas, with
| | read-only areas being stored elsewhere in ROM on some systems)
+---------+
| text | program code, this is the actual executable code that is running.
+---------+
これは、多くの一般的な仮想メモリ システムの一般的なプロセス アドレス空間です。「穴」は、メモリの合計サイズから、他のすべての領域が占有するスペースを差し引いたものです。これにより、ヒープが成長するための大量のスペースが提供されます。これは「仮想」でもあります。つまり、変換テーブルを介して実際のメモリにマップされ、実際のメモリの任意の場所に実際に格納される可能性があります。この方法は、あるプロセスが別のプロセスのメモリにアクセスするのを防ぎ、各プロセスが完全なシステム上で実行されていると認識できるようにするために行われます。
たとえば、スタックとヒープの位置は、一部のシステムでは異なる順序になる場合があることに注意してください ( Win32 の詳細については、以下 の Billy O'Neal の回答を参照してください)。
他のシステムは非常に異なる場合があります。たとえば、DOS はリアル モードで実行され、プログラム実行時のメモリ割り当ては大きく異なります。
+-----------+ top of memory
| extended | above the high memory area, and up to your total memory; needed drivers to
| | be able to access it.
+-----------+ 0x110000
| high | just over 1MB->1MB+64KB, used by 286s and above.
+-----------+ 0x100000
| upper | upper memory area, from 640kb->1MB, had mapped memory for video devices, the
| | DOS "transient" area, etc. some was often free, and could be used for drivers
+-----------+ 0xA0000
| USER PROC | user process address space, from the end of DOS up to 640KB
+-----------+
|command.com| DOS command interpreter
+-----------+
| DOS | DOS permanent area, kept as small as possible, provided routines for display,
| kernel | *basic* hardware access, etc.
+-----------+ 0x600
| BIOS data | BIOS data area, contained simple hardware descriptions, etc.
+-----------+ 0x400
| interrupt | the interrupt vector table, starting from 0 and going to 1k, contained
| vector | the addresses of routines called when interrupts occurred. e.g.
| table | interrupt 0x21 checked the address at 0x21*4 and far-jumped to that
| | location to service the interrupt.
+-----------+ 0x0
DOS が保護なしでオペレーティング システム メモリへの直接アクセスを許可していたことがわかります。つまり、ユーザー空間プログラムは通常、好きなものに直接アクセスしたり上書きしたりできました。
ただし、プロセスのアドレス空間では、コード セグメント、データ セグメント、ヒープ、スタック セグメントなどと記述されているだけで、プログラムは同じように見える傾向があり、マッピングが少し異なります。しかし、一般的な領域のほとんどはまだそこにありました。
プログラムと必要な共有ライブラリをメモリにロードし、プログラムの一部を適切な領域に配布すると、OS はメイン メソッドがある場所でプロセスの実行を開始し、プログラムはそこから引き継ぎ、必要に応じてシステム コールを作成します。それらが必要です。
スタックレス システム、ハーバード アーキテクチャ システム (コードとデータが別々の物理メモリに保持される)、実際に BSS を読み取り専用メモリに保持するシステム (最初にプログラマー) などですが、これは一般的な要点です。
あなたが言った:
また、コンピューター プログラムは、スタックとヒープの 2 種類のメモリを使用することも知っています。これらは、コンピューターのプライマリ メモリの一部でもあります。
「スタック」と「ヒープ」は単なる抽象的な概念であり、(必然的に) 物理的に異なる「種類」のメモリではありません。
スタックは、単なる後入れ先出しのデータ構造です。x86 アーキテクチャでは、実際には末尾からのオフセットを使用してランダムにアドレス指定できますが、最も一般的な機能は、アイテムを追加および削除する PUSH と POP です。これは一般的に、関数ローカル変数 (いわゆる「自動ストレージ」)、関数の引数、戻りアドレスなどに使用されます (詳細は後述)。
「ヒープ」は、オンデマンドで割り当てることができるメモリのチャンクの単なるニックネームであり、ランダムにアドレス指定されます (つまり、その中の任意の場所に直接アクセスできます)。これは一般的に、実行時に割り当てるデータ構造に使用されます (C++ では、C でnew
and delete
、 andmalloc
と friends を使用するなど)。
x86 アーキテクチャのスタックとヒープは、どちらもシステム メモリ (RAM) に物理的に存在し、前述のように、仮想メモリ割り当てによってプロセス アドレス空間にマップされます。
レジスター(まだ x86 上)は(RAM ではなく) プロセッサー内に物理的に存在し、TEXT 領域からプロセッサーによってロードされます (また、CPU 命令に応じて、メモリー内の他の場所または他の場所からロードすることもできます)。実際に実行されます)。それらは基本的に、さまざまな目的に使用される非常に小さく、非常に高速なオンチップ メモリ ロケーションです。
レジスタ レイアウトはアーキテクチャに大きく依存します (実際、レジスタ、命令セット、およびメモリ レイアウト/設計は、まさに「アーキテクチャ」が意味するものです)。それらをよりよく理解するためのアセンブリ言語コース。
あなたの質問:
スタックはどの時点で命令の実行に使用されますか? 命令は RAM からスタック、レジスタに移動しますか?
スタック (それらを使用しているシステム/言語) は、次のように使用されることが最も多いです。
int mul( int x, int y ) {
return x * y; // this stores the result of MULtiplying the two variables
// from the stack into the return value address previously
// allocated, then issues a RET, which resets the stack frame
// based on the arg list, and returns to the address set by
// the CALLer.
}
int main() {
int x = 2, y = 3; // these variables are stored on the stack
mul( x, y ); // this pushes y onto the stack, then x, then a return address,
// allocates space on the stack for a return value,
// then issues an assembly CALL instruction.
}
このような単純なプログラムを作成し、それをアセンブリにコンパイルして ( gcc -S foo.c
GCC にアクセスできる場合)、見てみましょう。組み立ては非常に簡単です。スタックは、関数のローカル変数、および関数の呼び出し、引数と戻り値の格納に使用されていることがわかります。これは、次のようなことをするときの理由でもあります。
f( g( h( i ) ) );
これらはすべて順番に呼び出されます。文字通り、関数呼び出しとその引数のスタックを構築し、それらを実行し、巻き戻す (または巻き上げる) ときにそれらをポップオフします。ただし、前述のように、スタック (x86 の場合) は実際にはプロセス メモリ空間 (仮想メモリ内) に存在するため、直接操作できます。実行中の個別のステップではありません(または少なくともプロセスに直交しています)。
参考までに、上記はCの呼び出し規約で、C++でも使用されています。他の言語/システムは、異なる順序で引数をスタックにプッシュする可能性があり、一部の言語/プラットフォームはスタックを使用することさえせず、異なる方法で処理します。
また、これらは実際に実行されている C コードの行ではないことに注意してください。コンパイラは、それらを実行可能ファイルの機械語命令に変換しました。 次に、(通常) TEXT 領域から CPU パイプラインにコピーされ、次に CPU レジスタにコピーされ、そこから実行されます。 【これは間違いでした。以下の Ben Voigt の訂正を参照してください。]