web-dev-qa-db-ja.com

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

私は一般的な理論を知っていますが、詳細には収まりません。

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

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

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

176
gaijinco

本当にシステムに依存しますが、 仮想メモリ を備えた最新の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.
+---------+

これは、多くの一般的な仮想メモリシステムの一般的なプロセスアドレス空間です。 「穴」は、合計メモリのサイズから、他のすべての領域が占めるスペースを引いたものです。これは、ヒープが成長するための大量のスペースを提供します。これは「仮想」でもあります。つまり、変換テーブルを介してactualメモリにマップされ、実際のメモリの任意の場所に実際に格納されます。これは、あるプロセスが別のプロセスのメモリにアクセスするのを防ぎ、各プロセスが完全なシステムで実行されているとみなすために、この方法で行われます。

一部のシステムでは、スタックとヒープなどの位置が異なる順序になる場合があることに注意してください(Win32の詳細については、下記の Billy O'Nealの回答 を参照してください)。

他のシステムは、非常に異なる場合があります。たとえば、DOSは real mode で実行され、プログラムの実行時のメモリ割り当ては大きく異なります。

+-----------+ 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種類のメモリを使用することも知っています。スタックとヒープは、コンピューターのプライマリメモリの一部でもあります。

「スタック」と「ヒープ」は、(必ずしも)物理的に異なる「種類」のメモリではなく、単なる抽象的な概念です。

stack は、単なる後入れ先出しのデータ構造です。 x86アーキテクチャでは、実際には末尾からのオフセットを使用してランダムにアドレス指定できますが、最も一般的な機能は、それぞれアイテムの追加と削除を行うプッシュとPOPです。通常、関数ローカル変数(いわゆる「自動ストレージ」)、関数引数、戻りアドレスなどに使用されます(以下で詳しく説明します)。

"heap" は、オンデマンドで割り当てることができるメモリチャンクの単なるニックネームであり、ランダムにアドレス指定されます(つまり、その場所に直接アクセスできます)。一般に、実行時に割り当てるデータ構造に使用されます(C++では、newdelete、およびmallocとCのフレンドなどを使用)。

X86アーキテクチャ上のスタックとヒープは、両方とも物理的にシステムメモリ(RAM)に存在し、前述のように仮想メモリの割り当てを通じてプロセスアドレス空間にマップされます。

registers (まだ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の修正 以下を参照してください。]

158

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(一般的なデスクトップマシン)は、メインメモリ内のVon Neumannアーキテクチャです。つまり、データとコードがRAMに混在しています。 CPUの内部で何が起こるかについて話しているので、それは重要ではありません。

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

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

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

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

59
Ben Voigt

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

#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ローダーはライブラリ/実行可能ファイルをrebaseする必要があります-基本的に、位置依存を移動します新しい位置を指すコード-ルックアップテーブルを必要とせず、上書きするルックアップテーブルがないため、悪用できません。残念ながら、これにはWindowsローダーでの非常に複雑な実装が必要であり、イメージのリベースが必要な場合、起動時間のオーバーヘッドがかなり大きくなります。大規模な商用ソフトウェアパッケージは、多くの場合、リベースを回避するために異なるアドレスで意図的に開始するようにライブラリを変更します。 Windows自体が独自のライブラリ(たとえば、ntdll.dll、kernel32.dll、psapi.dllなど)でこれを実行します。すべてデフォルトで異なる開始アドレスを持っています)

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

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

また、これはWindowsでは単一のプロセスに複数のヒープがあることを意味します(通常はそうなります)。 (通常、各共有ライブラリは独自のヒープを作成します。)

(この情報の大部分は、Robert Seacordによる「CおよびC++でのセキュアコーディング」から得られています)

17
Billy ONeal

スタック

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)のパラメーターを渡すためにスタックを使用します。

5
vbence