コンピュータプログラムが実行されるとどうなりますか?


180

私は一般的な理論を知っていますが、詳細に合わせることができません。

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

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

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


43
基本的な質問をするための+1!
mkelley33 2011年

21
うーん...あなたは知っています、彼らはそれについての本を書きます。SOの助けを借りて、OSアーキテクチャのこの部分を本当に研究しますか?
Andrey

1
質問のメモリ関連の性質とC ++への参照に基づいていくつかのタグを追加しましたが、JavaまたはC#に詳しい人からも良い答えが得られると思います!)
mkelley33

14
賛成とお気に入り。私はいつも尋ねるのが怖いので...
Maxpm

2
「それらをレジスターに入れる」という用語は、正しくありません。ほとんどのプロセッサでは、レジスタは実行可能コードではなく中間値を保持するために使用されます。

回答:


161

これは実際にはシステムに依存しますが、仮想メモリを備えた最新の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 ++では、newおよびを使用しdeletemalloc Cのフレンドなど)。

x86アーキテクチャでは、スタックとヒープはどちらも物理的にシステムメモリ(RAM)に常駐し、仮想メモリの割り当てを通じて上記のようにプロセスアドレス空間にマップされます。

レジスタは、(まだx86の上の)CPUの指示に応じて、物理プロセッサ(RAMとは対照的に)内部に存在し、テキスト領域から、プロセッサによってロードされる(そして、メモリ又は他の場所の他の場所からロードすることができます実際に実行されます)。それらは基本的に、非常に小さく、非常に高速なオンチップメモリ​​の場所であり、さまざまな目的で使用されます。

レジスターのレイアウトはアーキテクチャーに大きく依存しているため(実際、レジスター、命令セット、およびメモリーのレイアウト/デザインは、まさに「アーキテクチャー」が意味するものです)、そのため、ここでは詳しく説明しませんが、それらをよりよく理解するためのアセンブリ言語コース。


あなたの質問:

スタックはどの時点で命令の実行に使用されますか?命令は、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の訂正を参照してください。]


4
IMO
Andrey

13
ええ、「RTFM」は常に優れています。
Sdaz MacSkibbons 2011年

56
@Andrey:そのコメントを「また、あなたの良い本の推奨事項を読みたいかもしれません」に変更する必要があるかもしれません。 .. "多分あなたは本当にモデレーターの注意のために投稿にフラグを立てるか、少なくともあなたの意見がとにかく誰にとっても重要である理由についての説明を提供することを本当に考えるべきです。
mkelley33 2011年

2
すばらしい答えです。それは確かに私のためにいくつかのことを片付けました!
Maxpm

2
@Mikael:実装によっては、必須のキャッシュが存在する場合があります。この場合、データがメモリから読み取られるたびに、キャッシュライン全体が読み取られ、キャッシュが読み込まれます。または、データが1度だけ必要になるというヒントをキャッシュマネージャーに与えることができる可能性があるため、データをキャッシュにコピーしても役に立ちません。それは読み取り用です。書き込みには、DMAコントローラがデータを読み取ることができるときに影響を与えるライトバックキャッシュとライトスルーキャッシュがあり、独自のキャッシュを持つ複数のプロセッサを処理するためのキャッシュコヒーレンシプロトコルのホスト全体があります。これは本当に、独自Q.値する
ベンフォークト

61

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フリップフロップのコレクションに一時的に格納される場合があります。しかし、これは、コンピュータサイエンティスト、ソフトウェアエンジニア、またはありふれた開発者が用語レジスタを使用する場合に意味されるものではありません。これらは上記のデータパスレジスタを意味し、コードの転送には使用されません。

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


説明をありがとう。私はそれに精通していないので、それを追加するのをためらっていましたが、他の誰かの要求でそれを行いました。
Sdaz MacSkibbons

「ARMにはデータとコードが混在していることを意味します」のs / ARM / RAM /。正しい?
Bjarke Freund-Hansen、2011

@bjarkef:最初はそうですが、2回目はそうではありません。直します。
Ben Voigt

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など)も提供します。これは、Cランタイム(つまりmalloc、フレンド)が通常実装されるWindows自体の一部として含まれています。

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

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

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


素晴らしい情報、ありがとう!「user487117」が実際に戻ってくることを願っています。:-)
Sdaz MacSkibbons 2011年

5

スタック

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

たとえば、整数の乗算:

MUL BX

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

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

DMUL

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

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

ヒープ

ヒープは、コンパイラの領域にのみ存在するもう1つの概念です。変数の背後にあるメモリの扱いに苦​​労しますが、これはCPUやOSの機能ではなく、OSが提供するメモリブロックのハウスキーピングを選択するだけです。必要に応じて、これを何度も行うことができます。

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.