コールスタックはどの程度正確に機能しますか?


103

私はプログラミング言語の低レベルの操作がどのように機能するか、特にそれらがOS / CPUとどのように相互作用するかについて、より深く理解しようとしています。スタックオーバーフローのすべてのスタック/ヒープ関連スレッドのすべての回答を読んだことがあり、それらはすべて素晴らしいものです。しかし、まだ完全には理解していなかったことがあります。

有効なRustコードになる傾向がある疑似コードでこの関数を検討してください;-)

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(a, b);
    doAnotherThing(c, d);
}

これは、スタックが行Xで次のように見えると想定する方法です。

Stack

a +-------------+
  | 1           | 
b +-------------+     
  | 2           |  
c +-------------+
  | 3           | 
d +-------------+     
  | 4           | 
  +-------------+ 

今、私がスタックがどのように機能するかについて私が読んだすべては、それがLIFO規則に厳密に準拠しているということです(後入れ先出し)。.NET、Java、またはその他のプログラミング言語のスタックデータ型のように。

しかし、その場合、X行目以降はどうなりますか?明らかなので、次のことを我々の必要がで仕事にあるab、それはOS / CPUが(?)ポップアウトしなければならないことを意味するであろうdと、c最初に戻って取得するab。それが必要とするので、しかし、それは、足で自分自身を撮影するだろうcし、d次の行に。

では、裏で何起きているのでしょうか?

別の関連する質問。次のような他の関数の1つへの参照を渡すことを検討してください。

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(&a, &b);
    doAnotherThing(c, d);
}

私は物事を理解する方法から、これは中のパラメータがあることを意味するであろうdoSomething本質的のように同じメモリアドレスを指しているabfoo。しかし、誰があり、その後、再び、この手段は、我々がされるまでスタックをポップアップしないab起こって。

この2つのケースでは、スタックがどのように正確に機能し、どのようにLIFOの規則に厳密に従っているかを完全には把握していません。


14
LIFOは、スタック上のスペースを予約する場合にのみ重要です。他の多くの変数の下にある場合でも、少なくともスタックフレーム(関数内で宣言)にある任意の変数にいつでもアクセスできます
VoidStar

2
つまり、LIFOスタックの最後でのみ要素を追加または削除でき、いつでも要素を読み取ったり変更したりできることを意味します。
HolyBlackCat 14年

12
-O0を指定してコンパイルし、生成された命令を確認した後、単純な関数を逆アセンブルしてみませんか?それはかなり、まあ、有益です;-)。コードがRAMのR部分をうまく利用していることがわかります。自由にアドレスに直接アクセスします。変数名は、アドレスレジスタ(スタックポインタ)へのオフセットと考えることができます。他の人が言ったように、スタックはスタックに関するLIFOです(再帰などに適しています)。アクセスに関してはLIFOではありません。アクセスは完全にランダムです。
ピーター-モニカを2014年

6
配列を使用して独自のスタックデータ構造を作成し、トップエレメントのインデックスを格納するだけで、プッシュ時にインクリメントし、ポップ時にデクリメントできます。これを行った場合でも、配列の場合と同じように、プッシュしたりポップしたりすることなく、いつでも配列内の個々の要素にアクセスできます。ほぼ同じことがここで起こっています。
Crowman 2014年

3
基本的に、スタック/ヒープの命名は残念です。それらは、データ構造の用語のスタックとヒープにほとんど似ていないため、同じものを呼び出すことは非常に混乱します。
Siyuan Ren

回答:


117

呼び出しスタックは、フレームスタックとも呼ばれます。LIFOの原則の後にスタックされる
ものは、ローカル変数ではなく、呼び出される関数のスタックフレーム全体(「呼び出し」)です。ローカル変数は、いわゆる関数プロローグエピローグで、それらのフレームと一緒にそれぞれプッシュおよびポップされます。

フレーム内では、変数の順序は完全に指定されていません。コンパイラーは、フレーム内のローカル変数の位置を適切に「並べ替え」、それらのアライメントを最適化して、プロセッサーが可能な限り迅速にフェッチできるようにします。重要な事実は、いくつかの固定アドレスに対する変数のオフセットはフレームの存続期間を通じて一定であることです。そのため、アンカーアドレス、たとえばフレーム自体のアドレスを取得し、そのアドレスのオフセットを処理するだけで十分です。変数。このようなアンカーアドレスは、実際にはいわゆるベースまたはフレームポインターに含まれていますEBPレジスタに格納されます。一方、オフセットはコンパイル時に明確に認識されるため、マシンコードにハードコードされます。

ウィキペディアのこの図は、典型的なコールスタックの構造を示しています1

スタックの画像

アクセスしたい変数のオフセットをフレームポインターに含まれるアドレスに追加すると、変数のアドレスが取得されます。つまり、コードは、ベースポインターからのコンパイル時の一定のオフセットを介して直接アクセスするだけです。これは単純なポインター演算です。

#include <iostream>

int main()
{
    char c = std::cin.get();
    std::cout << c;
}

gcc.godbolt.orgが提供する

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp

    movl    std::cin, %edi
    call    std::basic_istream<char, std::char_traits<char> >::get()
    movb    %al, -1(%rbp)
    movsbl  -1(%rbp), %eax
    movl    %eax, %esi
    movl    std::cout, %edi
    call    [... the insertion operator for char, long thing... ]

    movl    $0, %eax
    leave
    ret

...のためにmain。コードを3つのサブセクションに分割しました。関数プロローグは、最初の3つの操作で構成されています。

  • ベースポインタがスタックにプッシュされます。
  • スタックポインターはベースポインターに保存されます
  • スタックポインターは、ローカル変数のためのスペースを作るために差し引かれます。

そして、cinEDIレジスタに移動され2get呼ばれています。戻り値はEAXです。

ここまでは順調ですね。今面白いことが起こります:

8ビットレジスタALで指定されたEAXの下位バイトが取得され、ベースポインタの直後のバイトに格納されます。つまり、ベースポインタ-1(%rbp)のオフセットは-1です。このバイトは変数ですc。スタックはx86で下向きに成長するため、オフセットは負です。次の操作cはEAXに格納されます。EAXはESIにcout移動され、EDIに移動されてcoutからc、引数とともに挿入演算子が呼び出されます。

最後に、

  • の戻り値はmainEAX:0に格納されます。これは、暗黙のreturnステートメントが原因です。のxorl rax rax代わりに表示されることもありますmovl
  • 呼び出しサイトを離れて戻ります。leaveこのエピローグを省略し、暗黙的に
    • スタックポインターをベースポインターに置き換え、
    • ベースポインターをポップします。

この操作retが実行された後、フレームは実質的にポップされましたが、cdecl呼び出し規約を使用しているため、呼び出し元は引数をクリーンアップする必要があります。stdcallなどの他の規則では、バイト数をに渡すなどして呼び出し先を片付ける必要がありますret

フレームポインターの省略

ベース/フレームポインターからのオフセットではなく、スタックポインター(ESB)からのオフセットを使用することもできます。これにより、フレームポインタ値を含むEBPレジスタが任意の用途に使用できるようになります。ただし、一部のマシンではデバッグが不可能になる可能性があり、一部の機能では暗黙的にオフになります。これは、x86など、レジスタがほとんどないプロセッサ用にコンパイルする場合に特に便利です。

この最適化はFPO(フレームポインターの省略)と呼ばれ-fomit-frame-pointer、GCCと-OyClang で設定されます。それ以外のコストがないため、デバッグがまだ可能な場合に限り、0より大きいすべての最適化レベルによって暗黙的にトリガーされることに注意してください。詳細については、ここここを参照ください


1コメントで指摘されているように、フレームポインターはおそらく戻りアドレスの後にあるアドレスを指すようになっています。

2 Rで始まるレジスタは、Eで始まるレジスタに対応する64ビットのレジスタであることに注意してください。EAXは、RAXの下位4バイトを指定します。わかりやすくするために、32ビットレジスタの名前を使用しました。


1
すばらしい答えです。オフセットでデータをアドレス指定することは、私にとって欠けていたビットでした:)
Christoph

1
絵にはちょっとした間違いがあると思います。フレームポインタは、戻りアドレスの反対側にある必要があります。関数の終了は、通常、次のように行われます。スタックポインターをフレームポインターに移動し、呼び出し元のフレームポインターをスタックからポップし、戻ります(つまり、呼び出し元のプログラムカウンター/命令ポインターをスタックからポップします)
kasperd

kasperdは完全に正しいです。フレームポインターをまったく使用しないか(有効な最適化、特にx86などのレジスタ不足のアーキテクチャでは非常に便利です)、それを使用して以前のものをスタックに保存します(通常はリターンアドレスの直後)。フレームの設定と削除の方法は、アーキテクチャとABIに大きく依存します。全体がもっと興味深いアーキテクチャがいくつかあります(こんにちはItanium)。もっと興味深い(そして可変サイズの引数リストのようなものがあります!)
Voo

3
@Christoph概念的な観点からこれに取り組んでいると思います。これはうまくいくと思いますが、RTS(RunTime Stack)は他のスタックとは少し異なります。「ダーティスタック」です。一番上に。図では、緑のメソッドの「返送先住所」が青のメソッドで必要なことに注意してください。パラメータの後です。前のフレームがポップされた後、blueメソッドはどのように戻り値を取得しますか?まあ、それは汚いスタックなので、それに到達してそれをつかむことができます。
2014年

1
代わりに常にスタックポインターからのオフセットを使用できるため、フレームポインターは実際には必要ありません。デフォルトでx64アーキテクチャをターゲットとするGCCはスタックポインターを使用rbpし、他の作業を行うために解放されます。
Siyuan Ren

27

明らかに、次に必要なのはaとbを操作することですが、これはOS / CPU(?)がaとbに戻るために最初にdとcをポップアウトする必要があることを意味します。しかし、次の行でcとdが必要なため、足で自分自身を撃ちます。

要するに:

引数をポップする必要はありません。呼び出し側fooから関数に渡される引数doSomethingとローカル変数doSomething はすべて、ベースポインタからのオフセットとして参照できます
そう、

  • 関数呼び出しが行われると、関数の引数がスタックにプッシュされます。これらの引数は、ベースポインターによってさらに参照されます。
  • 関数が呼び出し元に戻ると、戻り関数の引数はLIFOメソッドを使用してスタックからPOPされます。

詳細に:

ルールは、各関数呼び出しの結果、スタックフレームが作成されることです(最小値は戻るアドレスです)。したがって、funcAcalls funcBfuncBCallsの場合funcC、3つのスタックフレームが順番に設定されます。関数が戻ると、そのフレームは無効になります。正常に機能する関数は、独自のスタックフレームのみに作用し、他のスタックフレームには侵入しません。つまり、(関数から戻ったときに)一番上のスタックフレームに対してPOPが実行されます。

ここに画像の説明を入力してください

あなたの質問のスタックは、呼び出し元によって設定されますfoo。ときdoSomethingdoAnotherThing、その後、彼らは、セットアップ、独自のスタックと呼ばれています。この図は、これを理解するのに役立ちます。

ここに画像の説明を入力してください

引数にアクセスするには、関数本体が戻りアドレスが格納されている場所から下方向(上位アドレス)をトラバースする必要があり、ローカル変数にアクセスするには、関数本体がスタック(下アドレス)を上方向に横断する必要があることに注意してください。)差出人住所が格納されている場所を基準に。実際、関数用の典型的なコンパイラー生成コードはまさにこれを行います。コンパイラーは、このためにEBPと呼ばれるレジスター(ベース・ポインター)を専用にします。同じの別の名前はフレームポインターです。コンパイラーは通常、関数本体の最初のものとして、現在のEBP値をスタックにプッシュし、EBPを現在のESPに設定します。つまり、これが完了すると、関数コードのどの部分でも、引数1はEBP + 8離れて(呼び出し元のEBPと戻りアドレスのそれぞれに4バイト)、引数2はEBP + 12(10進数)離れて、ローカル変数になります。 EBP-4n離れている。

.
.
.
[ebp - 4]  (1st local variable)
[ebp]      (old ebp value)
[ebp + 4]  (return address)
[ebp + 8]  (1st argument)
[ebp + 12] (2nd argument)
[ebp + 16] (3rd function argument) 

関数のスタックフレームの形成について、次のCコードを見てください。

void MyFunction(int x, int y, int z)
{
     int a, int b, int c;
     ...
}

発信者がそれを呼び出すとき

MyFunction(10, 5, 2);  

次のコードが生成されます

^
| call _MyFunction  ; Equivalent to: 
|                   ; push eip + 2
|                   ; jmp _MyFunction
| push 2            ; Push first argument  
| push 5            ; Push second argument  
| push 10           ; Push third argument  

関数のアセンブリコードは次のようになります(戻る前に呼び出し先によってセットアップされます)

^
| _MyFunction:
|  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
|  ;x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
|  ;a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] =   [esp]
|  mov ebp, esp
|  push ebp
 

参照:


1
お返事ありがとうございます。また、リンクは本当にクールで、コンピュータが実際にどのように機能するかという、終わりのない質問にさらに光を当てるのに役立ちます:)
Christoph

「現在のEBP値をスタックにプッシュする」とはどういう意味か、またスタックポインターがレジスターに格納されているか、スタック内の位置を占めすぎている...少し混乱している
Suraj Jain

そして、それは[ebp + 8]ではなく* [ebp + 8]であるべきではありません。
Suraj Jain

@Suraj Jain; あなたが何であるかを知っていますEBPESP
2016

espはスタックポインター、ebpはベースポインターです。見落としがある場合は、修正してください。
Suraj Jain

19

他の人が指摘したように、スコープから外れるまで、パラメーターをポップする必要はありません。

Nick Parlanteによる「Pointers and Memory」の例をいくつか貼り付けます。状況はあなたが想像していたよりも少し単純だと思います。

ここにコードがあります:

void X() 
{
  int a = 1;
  int b = 2;

  // T1
  Y(a);

  // T3
  Y(b);

  // T5
}

void Y(int p) 
{
  int q;
  q = p + 2;
  // T2 (first time through), T4 (second time through)
}

時間のポイントT1, T2, etc。コードにマークが付けられ、そのときのメモリの状態が図面に表示されます。

ここに画像の説明を入力してください


2
素晴らしい視覚的説明。私はグーグルで見つけて、ここに紙を見つけました:cslibrary.stanford.edu/102/PointersAndMemory.pdf 本当に役立つ紙!
Christoph

7

異なるプロセッサと言語は、いくつかの異なるスタック設計を使用しています。8x86と68000の両方の2つの従来のパターンは、Pascal呼び出し規約とC呼び出し規約と呼ばれます。各規則は、レジスタの名前を除いて、両方のプロセッサで同じように処理されます。それぞれが2つのレジスターを使用して、スタックポインター(SPまたはA7)およびフレームポインター(BPまたはA6)と呼ばれるスタックおよび関連する変数を管理します。

いずれかの規則を使用してサブルーチンを呼び出す場合、ルーチンを呼び出す前に、パラメータがスタックにプッシュされます。次に、ルーチンのコードは、フレームポインターの現在の値をスタックにプッシュし、スタックポインターの現在の値をフレームポインターにコピーし、スタック変数から、ローカル変数(ある場合)によって使用されるバイト数を差し引きます。これが完了すると、追加のデータがスタックにプッシュされた場合でも、すべてのローカル変数はスタックポインターから一定の負の変位で変数に格納され、呼び出し元によってスタックにプッシュされたすべてのパラメーターには、フレームポインターからの一定の正の変位。

2つの規則の違いは、サブルーチンからの出口を処理する方法にあります。Cの規則では、戻り関数はフレームポインターをスタックポインターにコピーし(古いフレームポインターがプッシュされた直後の値に復元し)、古いフレームポインターの値をポップして戻ります。呼び出しがスタックに残る前に、呼び出し元がスタックにプッシュしたパラメータ。Pascal規則では、古いフレームポインターをポップした後、プロセッサは関数の戻りアドレスをポップし、呼び出し元によってプッシュされたパラメーターのバイト数をスタックポインターに追加してから、ポップされた戻りアドレスに移動します。元の68000では、呼び出し元のパラメーターを削除するために3つの命令シーケンスを使用する必要がありました。オリジナルの後の8x86およびすべての680x0プロセッサには「ret N」が含まれていた

Pascal規約には、呼び出し元が関数呼び出しの後にスタックポインターを更新する必要がないため、呼び出し元側のコードを少し節約できるという利点があります。ただし、呼び出された関数は、呼び出し元がスタックに何バイトのパラメーターを置くかを正確に知っている必要があります。Pascal規則を使用する関数を呼び出す前に、適切な数のパラメーターをスタックにプッシュしないと、クラッシュが発生することがほぼ保証されています。ただし、呼び出される各メソッド内の少し余分なコードが、メソッドが呼び出される場所でコードを保存するという事実により、これは相殺されます。そのため、元のMacintoshツールボックスルーチンのほとんどはPascal呼び出し規約を使用していました。

C呼び出し規約には、ルーチンが可変数のパラメーターを受け入れることができ、ルーチンが渡されたすべてのパラメーターを使用しなくても堅牢であるという利点があります(呼び出し元は、プッシュしたパラメーターのバイト数を知っています。したがって、それらをクリーンアップすることができます)。さらに、関数を呼び出すたびにスタックのクリーンアップを実行する必要はありません。ルーチンが4つの関数を順番に呼び出し、それぞれが4バイト相当のパラメーターを使用した場合はADD SP,4、各呼び出しの後にafter を使用する代わりにADD SP,16、最後の呼び出しの後に1つ使用して、4つすべての呼び出しからパラメーターをクリーンアップできます。

現在、説明されている呼び出し規約は、いくぶん時代遅れであると考えられています。コンパイラーはレジスターの使用でより効率的になったので、すべてのパラメーターをスタックにプッシュすることを要求するのではなく、レジスター内のいくつかのパラメーターをメソッドが受け入れるようにするのが一般的です。メソッドがレジスタを使用してすべてのパラメータとローカル変数を保持できる場合は、フレームポインタを使用する必要がないため、古いポインタを保存して復元する必要はありません。それでも、リンクを使用するようにリンクされたライブラリを呼び出すときに、古い呼び出し規約を使用する必要がある場合があります。


1
うわー!一週間ほど脳を借りてもいいですか。骨の折れるものを抽出する必要があります!正解です。
Christoph

フレームとスタックポインターは、スタック自体または他のどこに格納されていますか?
Suraj Jain

@SurajJain:通常、フレームポインターの保存された各コピーは、新しいフレームポインターの値に対して固定された変位で格納されます。
スーパーキャット2016

サー、私は長い間この疑問を抱いています。私の関数にif ifを記述し、(g==4)その後int d = 3、別の変数を定義してg入力を受け取りscanfますint h = 5。さて、コンパイラはどのようにしd = 3てスタック内のスペースを確保するのでしょうか。どのようにしている場合ので、行ってオフセットgではありません4し、スタック内のDのためのメモリはないだろうと単純に与えられるオフセットhとあればg == 4、最初グラムのために、その後のためになりますオフセットh。コンパイラはコンパイル時にそれをどのように行うのか、それは私たちの入力を知らないg
Suraj Jain

@SurajJain:Cの初期のバージョンでは、関数内のすべての自動変数は、実行可能なステートメントの前になければなりませんでした。その複雑なコンパイルを少し緩和しますが、1つのアプローチは、SPから前方宣言されたラベルの値を減算する関数の開始時にコードを生成することです。関数内では、コンパイラーはコードの各ポイントで、まだローカルにあるバイト数に相当する範囲を追跡し、ローカルに存在する最大バイト数を追跡​​することができます。関数の最後に、以前の値を提供できます...
supercat

5

ここには本当に良い答えがいくつかあります。ただし、スタックのLIFO動作について引き続き懸念がある場合は、変数のスタックではなく、フレームのスタックと考えてください。私が示唆していることは、関数はスタックの最上位にない変数にアクセスする可能性がありますが、それでもアイテムでのみ動作しているということです単一のスタックフレーム)に対してです。

もちろん、これには例外があります。コールチェーン全体のローカル変数は引き続き割り当てられ、使用可能です。ただし、直接アクセスすることはできません。代わりに、それらは参照(または、意味的にのみ異なるポインタ)によって渡されます。この場合、さらに下のスタックフレームのローカル変数にアクセスできます。ただし、この場合でも、現在実行中の関数は、それ自体のローカルデータでのみ動作しています。独自のスタックフレームに格納されている参照にアクセスしています。これは、ヒープ、静的メモリ、またはスタックのさらに下の何かへの参照である可能性があります。

これは、関数を任意の順序で呼び出し可能にし、再帰を可能にするスタック抽象化の一部です。一番上のスタックフレームは、コードによって直接アクセスされる唯一のオブジェクトです。それ以外のものは間接的にアクセスされます(最上位のスタックフレームにあるポインターを介して)。

特に最適化なしでコンパイルする場合は、小さなプログラムのアセンブリを確認することは有益かもしれません。関数内のすべてのメモリアクセスは、スタックフレームポインターからのオフセットを介して行われることがわかります。これは、コンパイラーによって関数のコードが記述される方法です。参照渡しの場合、スタックフレームポインターからのオフセットに格納されているポインターを介した間接メモリアクセス命令が表示されます。


4

呼び出しスタックは、実際にはスタックデータ構造ではありません。舞台裏では、私たちが使用するコンピューターは、ランダムアクセスマシンアーキテクチャの実装です。したがって、aとbに直接アクセスできます。

機械は裏側で次のことを行います。

  • get "a"は、スタックトップの下にある4番目の要素の値を読み取ることと同じです。
  • get "b"は、スタックの一番下の3番目の要素の値を読み取ることと同じです。

http://en.wikipedia.org/wiki/Random-access_machine


1

これは、Cのコールスタック用に作成した図です。Google画像バージョンよりも正確で現代的です

ここに画像の説明を入力してください

上記の図の正確な構造に対応して、Windows 7でのnotepad.exe x64のデバッグを次に示します。

ここに画像の説明を入力してください

この図では、スタックが上向きに上昇しているため、下位アドレスと上位アドレスが入れ替えられています。赤は、最初の図とまったく同じようにフレームを示します(赤と黒を使用していましたが、現在は黒が転用されています)。黒はホームスペースです。青は戻りアドレスであり、呼び出し後の命令への呼び出し元関数へのオフセットです。オレンジは配置で、ピンクは呼び出しの直後で最初の命令の前を指している場所です。homespace + return値は、Windowsで許可されている最小のフレームです。呼び出された関数の先頭の16バイトのrspアラインメントを維持する必要があるため、これには常に8バイトのアラインメントも含まれます。BaseThreadInitThunk 等々。

赤い関数フレームは、呼び出し先の関数が論理的に「所有」している+読み取り/変更の概要を示しています(スタック上で渡されたパラメーターが大きすぎて、-Ofastでレジスターに渡すことができない場合があります)。緑の線は、関数が関数の最初から最後まで自分自身を割り当てるスペースを示しています。


RDIと他のレジスタ引数は、デバッグモードでコンパイルした場合にのみスタックにスピルされ、コンパイルがその順序を選択する保証はありません。また、最も古い関数呼び出しの図の上部にスタック引数が表示されないのはなぜですか?どのフレームがどのデータを「所有」しているのか、図には明確な境界がありません。(呼び出し先はスタック引数を所有します)。ダイアグラムの上部からスタック引数を省略すると、「レジスターで渡すことができないパラメーター」が常にすべての関数の戻りアドレスの真上にあることがわかりにくくなります。
Peter Cordes

@PeterCordes goldbolt asmの出力は、clangおよびgccの呼び出し先が、レジスターに渡されたパラメーターをデフォルトの動作としてスタックにプッシュすることを示しているため、アドレスを持っています。gccでregisterパラメータの後ろを使用すると、これが最適化されますが、関数内でアドレスが取得されることはないため、とにかく最適化されると思います。上のフレームを修正します。確かに私は省略記号を別の空白のフレームに入れるべきだった。「呼び出し先はそのスタック引数を所有しています」、レジスタで渡すことができない場合に呼び出し元がプッシュするものを含めて何ですか?
ルイスケルシー

ええ、最適化を無効にしてコンパイルすると、呼び出し先はそれをどこかにこぼしてしまいます。しかし、スタック引数(そしておそらく保存されたRBP)の位置とは異なり、どこについても標準化されていません。Re:呼び出し先はそのスタック引数を所有しています。はい、関数は入力引数を変更できます。自分自身がこぼしたreg引数は、スタック引数ではありません。コンパイラーはこれを行う場合がありますが、IIRCは引数を再度読み取らなくても、リターンアドレスの下のスペースを使用してスタックスペースを浪費することがよくあります。発信者が同じ引数を使用して別の呼び出しを行いたい場合、安全のために、繰り返しを行う前に別のコピーを保存する必要がありますcall
Peter Cordes

@PeterCordesまあ、私はrbpが指す場所に基づいてスタックフレームの境界を定めていたので、引数を呼び出し元スタックの一部にしました。一部の図は、これを呼び出し先スタックの一部として示し(この質問の最初の図が示すように)、一部を呼び出し元スタックの一部として示していますが、パラメータースコープとして見て、呼び出し先スタックの一部にすることは意味があるかもしれません上位レベルのコードでは、呼び出し元はアクセスできません。はい、それはそうですregisterし、const最適化は唯一-O0の違いを生みます。
ルイスケルシー

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