変数はプログラムスタックにどのように格納され、そこから取得されますか?


47

この質問の素朴さに謝罪します。私は50歳のアーティストで、初めてコンピューターを正しく理解しようとしています。だからここに行きます。

私は、データ型と変数がコンパイラによってどのように処理されるかを理解しようとしています(非常に一般的な意味で、私はそれがたくさんあることを知っています)。「スタック」のストレージと値型、および「ヒープ」のストレージと参照型の関係についての理解が不足しています(引用符は、これらの用語が抽象化であって、私がこの質問を組み立てているように、そのような単純化されたコンテキストで文字通りに取られすぎます)。とにかく、私の単純なアイデアは、ブールや整数のような型は「スタック」に行くということです。なぜなら、それらはストレージスペースの観点から既知のエンティティであり、スコープはそれに応じて簡単に制御できるからです。

しかし、スタック上の変数がアプリケーションによって読み取られる方法はわかりませんx-たとえばx = 3、宣言して整数として割り当て、ストレージがスタック上に予約され、その値が3そこに格納され、次に同じ機能Iを宣言し、アサインy言う、など4そして、私はその後、使用することを、次のx別の表現では、(と言うz = 5 + xプログラムを読むことができるか)xを評価するためにz、それは以下であるときyスタックに?私は明らかに何かが欠けています。スタック上の場所は変数の有効期間/スコープにすぎず、スタック全体が常にプログラムから実際にアクセス可能であるということですか?もしそうなら、値を取得できるようにスタック上の変数のアドレスのみを保持する他のインデックスがあることを意味しますか?しかし、その後、スタックのポイントは、値が変数アドレスと同じ場所に格納されていることだと思いましたか?私のちっぽけな心では、この他のインデックスがある場合、私たちはよりヒープのようなものについて話しているようです?私は明らかに非常に混乱しており、単純な質問に対する簡単な答えがあることを望んでいます。

ここまで読んでくれてありがとう。


7
@ fade2black私は同意しません-重要なポイントを要約した合理的な長さの答えを与えることが可能であるべきです。
デビッドリチャービー

9
種類とそれが格納されている場所を混同するという非常に一般的なエラーを犯しています。boolがスタックに行くと言うのは単純に偽です。ブールは変数に入り、変数の寿命が短いことがわかっている場合はスタックに、変数の寿命が短いことがわかっていない場合はヒープに移動します。これがC#にどのように関係するかについてのいくつかの考えについては、blogs.msdn.microsoft.com / ericlippert / 2010/09/30 /…を
Eric Lippert

7
また、スタックを変数値のスタックと考えないでください。メソッドアクティベーションフレームのスタックと考えてください。メソッド内では、そのメソッドのアクティベーションの任意の変数にアクセスできますが、呼び出し元の変数にはアクセスできません。これらはスタックの最上部にあるフレーム内にないためです。
エリックリッパー

5
また、新しいことを学び、言語の実装の詳細を掘り下げるイニシアチブをとってくれたことを称賛します。ここで興味深い障害にぶつかります。抽象データ型としてのスタックが何であるかを理解していますが、アクティベーションと継続を具体化するための実装の詳細としてではありません。後者は、スタック抽象データ型の規則に従いませ。それはルールとしてよりもガイドラインとしてそれらをより扱います。プログラミング言語のポイントは、プログラミングの問題を解決するためにこれらの抽象化された詳細を理解する必要がないことを保証することです。
エリックリッパー

4
エリック、サヴァ、サムネイルに感謝します。これらのコメントと参考文献はすべて非常に役に立ちます。私のような質問に出会ったとき、あなたのような人は内心でうめき声をあげなければならないように感じますが、答えを得る際の大きな興奮と満足度を知ってください!
セリーヌアトウッド

回答:


24

スタックにローカル変数を保存することは実装の詳細であり、基本的には最適化です。このように考えることができます。関数を入力すると、すべてのローカル変数のスペースがどこかに割り当てられます。何らかの方法で変数の位置を知っているため、すべての変数にアクセスできます(これは割り当てプロセスの一部です)。関数を終了すると、スペースは割り当て解除されます(解放されます)。

スタックは、このプロセスを実装する1つの方法です。サイズが制限されているため、小さな変数にのみ適している一種の「高速ヒープ」と考えることができます。追加の最適化として、すべてのローカル変数は1つのブロックに保存されます。各ローカル変数のサイズは既知であるため、ブロック内の各変数のオフセットがわかり、それがどのようにアクセスするかです。これは、アドレスが他の変数に保存されるヒープに割り当てられた変数とは対照的です。

k

最後に、実際には、ローカル変数の一部がレジスターに保存されていることを言及します。これは、レジスタへのアクセスがスタックへのアクセスよりも速いためです。これは、ローカル変数用のスペースを実装する別の方法です。繰り返しになりますが、変数の保存場所は正確にわかります(今回はオフセットではなく、レジスタの名前を使用)。この種類の保存は小さなデータにのみ適しています。


1
「1つのブロックに割り当てられる」は、別の実装の詳細です。しかし、それは問題ではありません。コンパイラーは、ローカル変数にメモリーがどのように必要かを認識し、そのメモリーに1 つ以上のブロックを割り当ててから、そのメモリーにローカル変数を作成します。
–MSalters

ありがとう、修正しました。実際、これらの「ブロック」の一部は単なるレジスタです。
ユヴァルフィルマス

1
本当に必要なのは、戻りアドレスを保存するためのスタックだけです。ヒープの戻りアドレスにポインターを渡すことで、スタックなしで再帰を簡単に実装できます。
ユヴァルフィルマス

1
@MikeCaron Stacksは、再帰とはほとんど関係がありません。他の実装戦略で「変数を吹き飛ばす」のはなぜですか?
ガーデンヘッド

1
@gardenheadの最も明白な代替手段(および実際に使用/使用された代替手段)は、各プロシージャの変数を静的に割り当てることです。高速、シンプル、予測可能...しかし、再帰や再入は許可されていません。もちろん、それと従来のスタックは唯一の選択肢ではありません(すべてを動的に割り当てることは別です)が、通常はスタックを正当化するときに議論するものです:)
hobbs

23

持つyスタックにすることは物理的に防ぐことはできませんxあなたが指摘したように、コンピュータが他のスタックと異なるスタックになりた、アクセスされているから。

プログラムがコンパイルされると、スタック内の変数の位置も事前に決定されます(関数のコンテキスト内)。あなたの例では、スタックが含まれている場合xy、それは、その後、プログラムは知っている「の上に」事前にそれがx1つのアイテムスタックの一番下になります関数の内部しばらく。コンピューターのハードウェアはスタックの最上部の下にある1つのアイテムを明示的に要求できるため、コンピューターは存在しxyも取得できます。

スタック上の場所は変数の有効期間/スコープにすぎず、スタック全体が常にプログラムから実際にアクセス可能であるということですか?

はい。関数を終了すると、スタックポインターは以前の位置に戻り、実質的にx、およびを消去しますyが、技術的には、メモリが他の目的で使用されるまでそこに残ります。また、あなたの関数が別の関数を呼び出す場合、xそしてyまだあるだろうと意図的にスタックにすぎダウンしてアクセスすることができます。


1
これは、OPがテーブルにもたらす背景知識を超えて話をしないという点で、これまでのところ最もクリーンな答えのようです。本当にOPをターゲットにした+1!
ベンI.

1
私も同意します!すべての答えは非常に有用であり、私は非常に感謝していますが、このスタック/ヒープ全体が値/参照型の区別がどのように発生するかを理解するために絶対に不可欠であると感じているので、私の元の投稿は動機付けられましたが、 「スタック」の最上部しか表示できない場合はどうすればいいですか。だからあなたの答えは私をそこから解放します。(物理学のさまざまな逆二乗の法則がすべて球から出てくる放射の幾何学から単純に外れていることに初めて気づいたときと同じ感覚が得られます。それを見るために簡単な図を描くことができます。)
Celine Atwood

私はそれが大好きです。なぜなら、より高いレベルのいくつかの現象(言語など)が、抽象ツリーの少し下にあるより基本的な現象に実際に起因する方法と理由を確認できると、常に非常に役立つからです。たとえそれが非常にシンプルに保たれていても。
セリーヌアトウッド

1
@CelineAtwoodスタックから削除された変数に「強制的に」アクセスしようとすると、予測できない/未定義の動作が発生するため、実行しないでください。いくつかの言語で試してみることができるとは言えません。それでも、それはプログラミングの間違いであり、避けるべきです。
code_dredd

12

コンパイラがスタックを管理する方法とスタック上の値へのアクセス方法の具体例を提供するために、視覚的な描写に加えてGCC、i386をターゲットアーキテクチャとするLinux環境で生成されたコードを見ることができます。

1.スタックフレーム

ご存知のように、スタックは、関数またはプロシージャによって使用される実行中のプロセスのアドレス空間内の場所です。つまり、ローカルに宣言された変数と関数に渡される引数用にスタック上に空間が割り当てられます関数の外部で宣言された変数(グローバル変数など)のスペースは、仮想メモリの別の領域に割り当てられます)。関数のすべてのデータに割り当てられたスペースは、スタックフレームと呼ばれます。これは、複数のスタックフレームの視覚的な描写です(コンピューターシステム:プログラマーの視点から)。

CSAPPスタックフレーム

2.スタックフレーム管理と変数の場所

特定のスタックフレーム内のスタックに書き込まれた値がコンパイラーによって管理され、プログラムによって読み取られるようにするには、これらの値の位置を計算し、メモリーアドレスを取得する方法が必要です。これには、スタックポインターとベースポインターと呼ばれるCPU内のレジスタが役立ちます。

ebp慣例により、ベースポインターには、スタックの最下部またはベースのメモリアドレスが含まれます。スタックフレーム内のすべての値の位置は、参照としてベースポインターのアドレスを使用して計算できます。これは上の図に示されてい%ebp + 4ます。たとえば、ベースポインタに4を加えたメモリアドレスです。

3.コンパイラ生成コード

しかし、取得できないのは、スタック上の変数がアプリケーションによって読み取られる方法です-xを整数として宣言して割り当て、x = 3とし、ストレージがスタック上に予約され、その値3が格納される場合そこに、同じ関数で宣言してyを4と言って割り当て、それに続いて別の式でxを使用します(たとえばz = 5 + x)プログラムはどのようにxを読み取ってzを評価しますか?スタック上でy未満ですか?

Cで書かれた簡単なサンプルプログラムを使用して、これがどのように機能するかを見てみましょう。

int main(void)
{
        int x = 3;
        int y = 4;
        int z = 5 + x;

        return 0;
}

このCソーステキスト用にGCCによって生成されたアセンブリテキストを調べてみましょう(わかりやすくするために少し整理しました)。

main:
    pushl   %ebp              # save previous frame's base address on stack
    movl    %esp, %ebp        # use current address of stack pointer as new frame base address
    subl    $16, %esp         # allocate 16 bytes of space on stack for function data
    movl    $3, -12(%ebp)     # variable x at address %ebp - 12
    movl    $4, -8(%ebp)      # variable y at address %ebp - 8
    movl    -12(%ebp), %eax   # write x to register %eax
    addl    $5, %eax          # x + 5 = 9
    movl    %eax, -4(%ebp)    # write 9 to address %ebp - 4 - this is z
    movl    $0, %eax
    leave

観察されるのは、変数x、y、およびzがそれぞれアドレス%ebp - 12%ebp -8および%ebp - 4にあることです。言い換えれば、スタックフレーム内の変数の位置はmain()、CPUレジスタに保存されたメモリアドレスを使用して計算され%ebpます。

4.スタックポインターを超えたメモリ内のデータは範囲外です

私は明らかに何かが欠けています。スタック上の場所は変数の有効期間/スコープにすぎず、スタック全体が常にプログラムから実際にアクセス可能であるということですか?もしそうなら、値を取得できるようにスタック上の変数のアドレスのみを保持する他のインデックスがあることを意味しますか?しかし、その後、スタックのポイントは、値が変数アドレスと同じ場所に格納されていることだと思いましたか?

スタックは仮想メモリ内の領域であり、その使用はコンパイラによって管理されます。コンパイラは、スタックポインタを超える値(スタックの最上部を超える値)が参照されないようにコードを生成します。関数が呼び出されると、スタックポインターの位置が変化して、いわば「境界外」ではないと見なされるスタック上のスペースが作成されます。

関数が呼び出されて返されると、スタックポインターはデクリメントおよびインクリメントされます。スタックに書き込まれたデータは、スコープ外になっても消えませんが、コンパイラは%ebpまたはを使用してこれらのデータのアドレスを計算する方法がないため、このデータを参照する命令を生成しません%esp

5.まとめ

CPUによって直接実行できるコードは、コンパイラーによって生成されます。コンパイラは、スタック、関数のスタックフレーム、およびCPUレジスタを管理します。GCCがi386アーキテクチャで実行するコード内のスタックフレーム内の変数の場所を追跡するために使用する戦略の1つ%ebpは、スタックフレームの場所への参照および変数値の書き込みとして、スタックフレームベースポインターのメモリアドレスを使用することですのアドレスへのオフセットで%ebp


その画像がどこから来たのか尋ねると それは疑わしく馴染みのあるように見える... :-)それは過去の教科書にあったかもしれない
グレートダック

1
nvmd。リンクを見ました。それは私が考えていたものでした。その本を共有するための+1。
グレートダック

1
gccアセンブリデモの+1 :)
flow2k

9

ESP(スタックポインター)とEBP(ベースポインター)の2つの特殊レジスターがあります。プロシージャが呼び出されると、通常、最初の2つの操作は

push        ebp  
mov         ebp,esp 

最初の操作はEBPの値をスタックに保存し、2番目の操作はスタックポインターの値をベースポインターにロードします(ローカル変数にアクセスするため)。したがって、EBPはESPと同じ場所を指します。

アセンブラーは、変数名をEBPオフセットに変換します。たとえば、ローカル変数が2つありx,y、次のようなものがある場合

  x = 1;
  y = 2;
  return x + y;

それは次のようなものに翻訳されるかもしれません

   push        ebp  
   mov         ebp,esp
   mov  DWORD PTR [ ebp + 6],  1   ;x = 1
   mov  DWORD PTR [ ebp + 14], 2   ;y = 2
   mov  eax, [ ebp + 6 ]
   add  [ ebp + 14 ], eax          ; x + y 
   mov  eax, [ ebp + 14 ] 
   ...  

オフセット値6および14は、コンパイル時に計算されます。

これがおおよその仕組みです。詳細については、コンパイラの本を参照してください。


14
これは、Intel x86に固有です。ARMでは、レジスターSP(R13)とFP(R11)が使用されます。また、x86では、レジスタがないため、積極的なコンパイラはESPから派生できるためEBPを使用しません。これは、他の変更を必要とせずに、すべてのEBP相対アドレス指定をESP相対に変換できる最後の例では明らかです。
–MSalters

そもそもx、yのスペースを空けるために、ESPのSUBを逃していませんか?
ハーゲンフォンアイゼン

@HagenvonEitzen、おそらく。スタックに割り当てられた変数がハードウェアレジスタを使用してどのようにアクセスされるかを考えたいと思いました。
fade2black

Downvoters、コメントしてください!!!
-fade2black

8

スタックに格納されたローカル変数は、スタックのアクセスルールであるFirst In Last Outまたは単にFILOでアクセスされないため、混乱します。

問題は、ローカル変数ではなく、FILOルールが関数呼び出しシーケンスとスタックフレームに適用されることです。

スタックフレームとは何ですか?

関数を入力すると、スタックフレームと呼ばれるスタック上のメモリが割り当てられます。関数のローカル変数はスタックフレームに格納されます。各関数のローカル変数の数とサイズは異なるため、スタックフレームのサイズは関数ごとに異なることが想像できます。

スタックフレームにローカル変数を保存する方法は、FILOとは関係ありません。(ソースコードでのローカル変数の出現順序でさえ、ローカル変数がその順序で保存されることを保証しません。)あなたの質問で適切に推論したように、「変数のアドレスのみを保持する他のインデックスがありますスタック上で値を取得できるようにします」。ローカル変数のアドレスは、通常、スタックフレームの境界アドレスなどのベースアドレスと、各ローカル変数に固有のオフセット値を使用して計算されます。

では、このFILOの動作はいつ表示されますか?

さて、別の関数を呼び出すとどうなりますか?呼び出し先関数には独自のスタックフレームが必要であり、スタックにプッシュれるのはこのスタックフレームです。つまり、呼び出し先関数のスタックフレームは、呼び出し元関数のスタックフレームの上に配置されます。この呼び出し先関数が別の関数を呼び出すと、そのスタックフレームが再びスタックの一番上にプッシュされます。

関数が戻るとどうなりますか?呼び出し先関数が呼び出し元関数に戻ると、呼び出し先関数のスタックフレームがスタックからポップアウトされ、将来の使用のためにスペースが解放されます。

あなたの質問から:

スタック上の場所は変数の有効期間/スコープにすぎず、スタック全体がプログラムから常に実際にアクセス可能であるということですか?

スタックフレームのローカル変数値は、関数が戻ったときに実際には消去されないため、ここで非常に正しいです。値はそこにとどまりますが、保存されているメモリの場所は関数のスタックフレームに属していません。他の関数がその場所を含むスタックフレームを取得し、他の値をそのメモリの場所に上書きすると、値は消去されます。

それでは、スタックとヒープの違いは何ですか?

スタックとヒープは、どちらもメモリ上のスペースを参照する名前であるという意味で同じです。アドレスを使用してメモリ上の任意の場所にアクセスできるため、スタックまたはヒープ内の任意の場所にアクセスできます。

違いは、コンピューターシステムがそれらをどのように使用するかについての約束から生じます。あなたが言ったように、ヒープは参照型用です。ヒープ内の値は特定のスタックフレームとは関係がないため、値のスコープは関数に関連付けられていません。ただし、ローカル変数のスコープは関数内にあり、現在の関数のスタックフレームの外にある任意のローカル変数値にアクセスできます、システムはこのような動作が発生しないことを確認しようとします。スタックフレーム。これは、ローカル変数が特定の関数にスコープされているという幻想を与えます。


4

言語ランタイムシステムによってローカル変数を実装するには、多くの方法があります。スタックの使用は、多くの実際的なケースで使用される一般的な効率的なソリューションです。

直観的には、スタックポインタspは実行時に保持されます(固定アドレスまたはレジスタに格納されます-本当に重要です)。すべての「プッシュ」がスタックポインターをインクリメントすると仮定します。

コンパイル時に、コンパイラは、各変数のアドレスを決定するsp - K場合にKのみ(したがって、コンパイル時に計算することができる)変数のスコープに依存する定数です。

ここでは、「スタック」という単語を大まかな意味で使用していることに注意してください。このスタックには、プッシュ/ポップ/トップ操作のみでアクセスするのではなく、を使用してアクセスしsp - Kます。

たとえば、次の擬似コードを検討してください。

procedure f(int x, int y) {
  print(x,y);    // (1)
  if (...) {
    int z=x+y; // (2)
    print(x,y,z);  // (3)
  }
  print(x,y); // (4)
  return;
}

プロシージャが呼び出されると、引数x,yをスタックに渡すことができます。簡単にするために、呼び出し元がx最初にプッシュし、次にが慣例であると想定しyます。

次に、ポイント(1)のコンパイラが見つけることができるxsp - 2ysp - 1

ポイント(2)で、新しい変数がスコープに追加されます。コンパイラは、sum x+y、つまりsp - 2andが指すものを生成するコードを生成しsp - 1、合計の結果をスタックにプッシュします。

ポイント(3)でz印刷されます。コンパイラは、それがスコープ内の最後の変数であることを認識しているため、が指し示していsp - 1ます。変更されたためy、これはもうありませんsp。それでも、yコンパイラを印刷するには、このスコープ内でを見つけることができることがわかっていますsp - 2。同様に、xはにありsp - 3ます。

ポイント(4)で、スコープを終了します。zポップされ、そしてy再びアドレスで発見されsp - 1、かつxですsp - 2

戻るとf、呼び出し元がx,yスタックからポップします。

そのKため、コンパイラの計算は、大まかにスコープ内の変数の数を数える問題です。実際の世界では、すべての変数が同じサイズであるとは限らないため、これは実際にKはより複雑です。したがって、の計算は少し複雑になります。スタックにはのリターンアドレスも含まれている場合があるためf、それもK「スキップ」する必要があります。しかし、これらは技術です。

一部のプログラミング言語では、より複雑な機能を処理する必要がある場合、事態はさらに複雑になる可能性があることに注意してください。たとえばK、ネストされたプロシージャは、特にネストされたプロシージャが再帰的である場合、多くのリターンアドレスを「スキップ」する必要があるため、非常に慎重な分析が必要です。クロージャ/ラムダ/匿名関数も、「キャプチャされた」変数を処理するためにある程度の注意が必要です。それでも、上記の例は基本的な考え方を示しているはずです。


3

最も簡単なアイデアは、変数をメモリ内のアドレスの修正と考えることです。実際、一部のアセンブラーはそのようにマシンコードを表示します(「値5をアドレスに保存i」、ここiで変数名)。

これらのアドレスの一部は、グローバル変数のように「絶対」であり、ローカル変数のように「相対」です。関数内の変数(アドレス)は、関数呼び出しごとに異なる「スタック」上のある場所に関連しています。そのように、同じ名前は異なる実際のオブジェクトを参照でき、同じ関数の循環呼び出しは、独立したメモリで動作する独立した呼び出しです。


2

スタックに配置できるデータ項目はスタックに配置されます-はい!プレミアムスペースです。また、一度xスタックにプッシュしてからスタックにプッシュするとy、理想的にはそこにアクセスするxまでアクセスできませんyyアクセスするにはポップする必要がありますx。正解です。

スタックは変数ではなく、 frames

間違っているのは、スタックそのものです。スタックでは、直接プッシュされるのはデータ項目ではありません。むしろ、スタック上に何かstack-frameがプッシュされます。このスタックフレームはデータ項目が含まれています。スタック内の深いフレームにはアクセスできませんが、最上位のフレームとその中に含まれるすべてのデータ項目にアクセスできます。

2つのスタックフレームframe-xとにバンドルされたデータ項目があるとしましょうframe-y。それらを次々にプッシュしました。今限りframe-yの上に座ってframe-x、あなたが理想的に任意のデータ項目の内部にアクセスすることはできませんframe-x。のみframe-y表示されます。ただしframe-y、表示されている場合は、バンドルされているすべてのデータ項目にアクセスできます。フレーム全体が表示され、その中に含まれるすべてのデータ項目が公開されます。

答えの終わり。これらのフレームの詳細(暴言)

コンパイル中に、プログラム内のすべての関数のリストが作成されます。次に、関数ごとに、スタック可能なデータ項目のリストが作成されます。次に、各関数に対してa stack-frame-templateが作成されます。このテンプレートは、選択されたすべての変数、関数の入力データ用のスペース、出力データなどを含むデータ構造です。実行中、関数が呼び出されるたびに、このコピーがtemplateすべての入力変数と中間変数とともにスタックに置かれます。この関数が他の関数を呼び出すと、その関数の新しいコピーがstack-frameスタックに配置されます。この関数が実行されいる限り、この関数のデータ項目は保持されます。一度その機能が終了すると、そのスタック・フレームが飛び出しています。いまこのスタックフレームはアクティブであり、この関数はそのすべての変数にアクセスできます。

スタックフレームの構造と構成は、プログラミング言語ごとに異なることに注意してください。言語内であっても、実装によって微妙な違いが生じる可能性があります。


CSをご検討いただきありがとうございます。私は今、ピアノのレッスンを受けているプログラマーです:)

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