184

一般的な理論は知っていますが、詳細には入り込めません。

私は、プログラムがコンピュータの二次記憶装置に常駐していることを知っています。プログラムが実行を開始すると、全体が RAM にコピーされます。次に、プロセッサは一度にいくつかの命令を取得し (バスのサイズによって異なります)、それらをレジスタに入れ、実行します。

また、コンピューター プログラムは、スタックとヒープの 2 種類のメモリを使用することも知っています。これらは、コンピューターのプライマリ メモリの一部でもあります。スタックは非動的メモリに使用され、ヒープは動的メモリに使用されます (たとえば、newC++ の演算子に関連するすべてのもの)。

私が理解できないのは、これら 2 つのものがどのように接続されているかということです。スタックはどの時点で命令の実行に使用されますか? 命令は RAM からスタック、レジスタに移動しますか?

4

4 に答える 4

163

実際にはシステムに依存しますが、仮想メモリを備えた最新の 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 でnewand 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.cGCC にアクセスできる場合)、見てみましょう。組み立ては非常に簡単です。スタックは、関数のローカル変数、および関数の呼び出し、引数と戻り値の格納に使用されていることがわかります。これは、次のようなことをするときの理由でもあります。

f( g( h( i ) ) ); 

これらはすべて順番に呼び出されます。文字通り、関数呼び出しとその引数のスタックを構築し、それらを実行し、巻き戻す (または巻き上げる) ときにそれらをポップオフします。ただし、前述のように、スタック (x86 の場合) は実際にはプロセス メモリ空間 (仮想メモリ内) に存在するため、直接操作できます。実行中の個別のステップではありません(または少なくともプロセスに直交しています)。

参考までに、上記はCの呼び出し規約で、C++でも使用されています。他の言語/システムは、異なる順序で引数をスタックにプッシュする可能性があり、一部の言語/プラットフォームはスタックを使用することさえせず、異なる方法で処理します。

また、これらは実際に実行されている C コードの行ではないことに注意してください。コンパイラは、それらを実行可能ファイルの機械語命令に変換しました。 次に、(通常) TEXT 領域から CPU パイプラインにコピーされ、次に CPU レジスタにコピーされ、そこから実行されます。 【これは間違いでした。以下の Ben Voigt の訂正を参照してください。]

于 2011-03-02T02:03:56.750 に答える
62

Sdaz は非常に短い時間で驚くべき数の賛成票を獲得しましたが、残念なことに、命令が CPU をどのように移動するかについての誤解が根強く残っています。

質問は次のとおりです。

命令は RAM からスタック、レジスタに移動しますか?

Sdaz 氏は次のように述べています。

また、これらは実際に実行されている C コードの行ではないことに注意してください。コンパイラは、それらを実行可能ファイルの機械語命令に変換しました。次に、(通常) TEXT 領域から CPU パイプラインにコピーされ、次に CPU レジスタにコピーされ、そこから実行されます。

しかし、これは間違っています。自己変更コードの特殊なケースを除いて、命令がデータパスに入ることはありません。また、データパスから実行することはできません。

x86 CPU レジスタは次のとおりです。

  • 汎用レジスタ EAX EBX ECX EDX

  • セグメントレジスタ CS DS ES FS GS SS

  • インデックスとポインタ ESI EDI EBP EIP ESP

  • 指標EFLAGS

浮動小数点レジスタと SIMD レジスタもいくつかありますが、この説明では、これらを CPU ではなくコプロセッサの一部として分類します。CPU 内のメモリ管理ユニットにも独自のレジスタがいくつかありますが、これも別の処理ユニットとして扱います。

これらのレジスタは、実行可能コードには使用されません。 EIP命令自体ではなく、実行中の命令のアドレスが含まれます。

命令は、CPU 内でデータとはまったく異なるパスを通過します (ハーバード アーキテクチャ)。現在のすべてのマシンは、CPU 内のハーバード アーキテクチャです。最近のほとんどは、キャッシュ内のハーバード アーキテクチャでもあります。x86 (一般的なデスクトップ マシン) は、メイン メモリ内のフォン ノイマン アーキテクチャです。つまり、データとコードが RAM 内で混在しています。CPU の内部で何が起こっているかについて話しているので、それは的外れです。

コンピュータ アーキテクチャで教えられている古典的なシーケンスは、フェッチ - デコード - 実行です。メモリ コントローラは、アドレスに格納されている命令を検索しますEIP。命令のビットは、いくつかの組み合わせロジックを通過して、プロセッサ内のさまざまなマルチプレクサ用のすべての制御信号を作成します。そして、いくつかのサイクルの後、算術論理ユニットは結果に到達し、デスティネーションにクロックインされます。その後、次の命令がフェッチされます。

最新のプロセッサでは、動作が少し異なります。各入力命令は、一連のマイクロコード命令全体に変換されます。最初のマイクロ命令で使用されるリソースは後で必要とされないため、パイプライン処理が有効になり、次の命令から最初のマイクロ命令の処理を開始できます。

さらに言えば、レジ​​スタは D フリップフロップの集まりを表す電気工学用語であるため、用語が少し混乱しています。また、命令 (または特にマイクロ命令) は、このような D フリップフロップのコレクションに一時的に保存される可能性があります。しかし、これは、コンピューター科学者、ソフトウェア エンジニア、またはありふれた開発者がregisterという用語を使用する場合の意味ではありません。上記のデータパス レジスタを意味し、コードの転送には使用されません。

データパス レジスタの名前と数は、ARM、MIPS、Alpha、PowerPC などの他の CPU アーキテクチャでは異なりますが、それらはすべて ALU を介さずに命令を実行します。

于 2011-03-02T05:48:33.040 に答える
17

プロセス実行中のメモリの正確なレイアウトは、使用しているプラ​​ットフォームに完全に依存します。次のテスト プログラムを検討してください。

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int stackValue = 0;
    int *addressOnStack = &stackValue;
    int *addressOnHeap = malloc(sizeof(int));
    if (addressOnStack > addressOnHeap)
    {
        puts("The stack is above the heap.");
    }
    else
    {
        puts("The heap is above the stack.");
    }
}

Windows NT (およびその子) では、このプログラムは通常、次のようになります。

ヒープはスタックの上にあります

POSIXボックスでは、次のようになります。

スタックはヒープの上にあります

UNIX メモリ モデルは、@Sdaz MacSkibbons によってここで非常によく説明されているため、ここでは繰り返しません。しかし、それだけが記憶モデルではありません。POSIX がこのモデルを必要とする理由は、sbrkシステム コールです。基本的に、POSIXボックスでは、より多くのメモリを取得するために、プロセスはカーネルに「穴」と「ヒープ」の間の仕切りをさらに「穴」領域に移動するように指示するだけです。メモリをオペレーティング システムに戻す方法はなく、オペレーティング システム自体はヒープを管理しません。C ランタイム ライブラリは、(malloc を介して) それを提供する必要があります。

これは、POSIX バイナリで実際に使用されるコードの種類にも影響します。POSIX ボックスは (ほぼ普遍的に) ELF ファイル形式を使用します。この形式では、オペレーティング システムが異なる ELF ファイル内のライブラリ間の通信を担当します。したがって、すべてのライブラリは位置に依存しないコードを使用し (つまり、コード自体を別のメモリ アドレスにロードしても動作する可能性があります)、ライブラリ間のすべての呼び出しはルックアップ テーブルを介して渡され、クロスのために制御がジャンプする必要がある場所を見つけます。ライブラリ関数呼び出し。これによりオーバーヘッドが追加され、ライブラリの 1 つがルックアップ テーブルを変更した場合に悪用される可能性があります。

Windows のメモリ モデルは、使用するコードの種類が異なるため異なります。Windows は PE ファイル形式を使用するため、コードは位置に依存する形式のままになります。つまり、コードは、コードが読み込まれる仮想メモリ内の正確な場所に依存します。PE仕様には、プログラムの実行時にライブラリまたは実行可能ファイルがメモリ内の正確にどこにマップされるかをOSに伝えるフラグがあります。プログラムまたはライブラリを優先アドレスにロードできない場合、Windows ローダーはリベースする必要がありますライブラリ/実行可能ファイル-基本的に、位置依存コードを新しい位置を指すように移動します-ルックアップテーブルを必要とせず、上書きするルックアップテーブルがないため悪用できません。残念ながら、これには Windows ローダーでの非常に複雑な実装が必要であり、イメージをリベースする必要がある場合、かなりの起動時間のオーバーヘッドが発生します。大規模な商用ソフトウェア パッケージでは、リベースを回避するために、ライブラリを意図的に別のアドレスで開始するように変更することがよくあります。Windows 自体が独自のライブラリでこれを行います (例: ntdll.dll、kernel32.dll、psapi.dll など - デフォルトではすべて異なる開始アドレスを持っています)。

Windows では、仮想メモリはVirtualAllocへの呼び出しを介してシステムから取得され、 VirtualFreeを介してシステムに返されます(技術的には VirtualAlloc は NtAllocateVirtualMemory にファームアウトしますが、これは実装の詳細です) (これを POSIX と対比してください。回収されます)。このプロセスは低速です (IIRC では、物理ページ サイズのチャンク (通常は 4kb 以上) を割り当てる必要があります)。Windows は、RtlHeap と呼ばれるライブラリの一部として独自のヒープ関数 (HeapAlloc、HeapFree など) も提供します。これは、Windows 自体の一部として含まれており、C ランタイム (つまり、mallocその仲間) が通常実装されています。

また、Windows には、古い 80386 を処理しなければならなかった時代からのレガシー メモリ割り当て API がかなりあります。これらの関数は現在、RtlHeap の上に構築されています。Windows でメモリ管理を制御するさまざまな API の詳細については、MSDN の記事http://msdn.microsoft.com/en-us/library/ms810627を参照してください。

これは、Windows では単一のプロセスが (そして通常は) 複数のヒープを持つことを意味することにも注意してください。(通常、各共有ライブラリは独自のヒープを作成します。)

(この情報のほとんどは、Robert Seacord による「C および C++ でのセキュアなコーディング」からのものです)

于 2011-03-02T06:06:39.683 に答える
5

スタック

X86 アーキテクチャでは、CPU はレジスタを使用して操作を実行します。スタックは便宜上の理由でのみ使用されます。サブルーチンまたはシステム関数を呼び出す前に、レジスターの内容をスタックに保存し、それらをロードして元の場所から操作を続行できます。(スタックなしで手動で行うこともできますが、頻繁に使用される機能であるため、CPU サポートがあります)。しかし、PC のスタックがなくても、ほとんど何でもできます。

たとえば、整数の乗算:

MUL BX

AX レジスタと BX レジスタを乗算します。(結果は、上位ビットを含む DX および AX、DX になります)。

スタックベースのマシン (JAVA VM など) は、基本的な操作にスタックを使用します。上記の乗算:

DMUL

これにより、スタックの一番上から 2 つの値がポップされ、TEM が乗算され、結果がスタックにプッシュされます。この種のマシンにはスタックが不可欠です。

一部の高水準プログラミング言語 (C や Pascal など) は、この後者の方法を使用してパラメーターを関数に渡します。パラメーターは左から右の順序でスタックにプッシュされ、関数本体によってポップされ、戻り値がプッシュバックされます。(これは、コンパイラの製造元が行う選択であり、X86 がスタックを使用する方法を悪用しています)。

ヒープ

ヒープは、コンパイラの領域にのみ存在する別の概念です。変数の背後にあるメモリを処理する手間がかかりますが、これは CPU や OS の機能ではなく、OS によって提供されるメモリ ブロックのハウスキーピングの選択にすぎません。必要に応じて、これを何度も行うことができます。

システム リソースへのアクセス

オペレーティング システムには、その機能にアクセスするためのパブリック インターフェイスがあります。DOS では、パラメータは CPU のレジスタに渡されます。Windows は、OS 関数 (Windows API) のパラメーターを渡すためにスタックを使用します。

于 2011-03-02T22:32:59.207 に答える