15

ネストされた関数呼び出しのデータはスタックに送られることを知っています。スタック自体は、関数が呼び出されたり戻ったりするときにスタックからデータを格納および取得するための段階的なメソッドを実装しています。これらのメソッドの名前は、プロローグおよびエピローグ。

このトピックに関する資料を検索しようとしましたが、成功しませんでした。関数プロローグとエピローグがCで一般的にどのように機能するかについてのリソース(サイト、ビデオ、記事)を知っていますか?または、説明できればさらに良いでしょう。

PS:あまり詳細ではなく、一般的な見方が欲しいだけです。

4

4 に答える 4

27

これを説明するリソースはたくさんあります。

いくつか例を挙げると。

基本的に、あなたがいくらか説明したように、「スタック」はプログラムの実行においていくつかの目的を果たします。

  1. 関数を呼び出すときに、どこに戻るかを追跡する
  2. 関数呼び出しのコンテキストでのローカル変数の格納
  3. 呼び出し元の関数から呼び出し先に引数を渡します。

prolougeは、関数の開始時に発生するものです。その責任は、呼び出された関数のスタックフレームを設定することです。エピローグは正反対です。これは関数の最後で発生することであり、その目的は呼び出し元の(親)関数のスタックフレームを復元することです。

IA-32(x86)cdeclでは、ebpレジスタは関数のスタックフレームを追跡するために言語によって使用されます。レジスタは、スタック上のesp最新の加算(最上位値)を指すためにプロセッサによって使用されます。(最適化されたコードでebpは、フレームポインターとしての使用はオプションです。例外のためにスタックを巻き戻す他の方法が可能であるため、セットアップに指示を費やす必要はありません。)

このcall命令は2つのことを行います。最初にリターンアドレスをスタックにプッシュし、次に呼び出されている関数にジャンプします。の直後はcallespスタック上のリターンアドレスを指します。ret(したがって、関数の入力時に、を実行してその差出人アドレスをEIPに戻すことができるように設定されます。プロローグはESPを別の場所にポイントします。これは、エピローグが必要な理由の一部です。)

次に、プロローグが実行されます。

push  ebp         ; Save the stack-frame base pointer (of the calling function).
mov   ebp, esp    ; Set the stack-frame base pointer to be the current
                  ; location on the stack.
sub   esp, N      ; Grow the stack by N bytes to reserve space for local variables

この時点で、次のようになります。

...
ebp + 4:    Return address
ebp + 0:    Calling function's old ebp value
ebp - 4:    (local variables)
...

エピローグ:

mov   esp, ebp    ; Put the stack pointer back where it was when this function
                  ; was called.
pop   ebp         ; Restore the calling function's stack frame.
ret               ; Return to the calling function.
于 2013-02-08T03:56:31.397 に答える
4
  1. C関数の呼び出し規約とスタックは、呼び出しスタックの概念をよく説明しています

  2. 関数プロローグは、アセンブリコードとその方法と理由を簡単に説明しています。

  3. 関数ペリローグの世代

于 2013-02-08T03:58:11.413 に答える
2

私はパーティーにかなり遅れています。質問が出されてから過去7年間で、あなたは物事をより明確に理解できるようになったと確信しています。もちろん、質問をさらに追求することを選択した場合です。しかし、私はそれでも、特にプロローグとエピローグの理由の部分を試してみようと思いました。

また、受け入れられた回答は、エピローグとプロローグの方法をエレガントかつ非常に簡単に説明しており、参考になります。私はその答えをなぜ(少なくとも論理的な理由)の部分で補足するつもりです。

受け入れられた回答から以下を引用し、説明を拡張してみます。

IA-32(x86)cdeclでは、ebpレジスタは、関数のスタックフレームを追跡するために言語によって使用されます。espレジスタは、スタック上の最新の加算(最上位値)を指すためにプロセッサによって使用されます。

呼び出し命令は2つのことを行います。最初にリターンアドレスをスタックにプッシュし、次に呼び出されている関数にジャンプします。呼び出しの直後に、espはスタック上のリターンアドレスを指します。

上記の引用の最後の行はimmediately after the call, esp points to the return address on the stack.

なんで?

したがって、現在実行されているコードには、以下の(非常にひどく描かれた)図に示すように、次のような状況があるとしましょう。

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

したがって、次に実行する命令は、たとえばアドレス2です。これがEIPが指している場所です。現在の命令には関数呼び出しがあります(これは内部でアセンブリcall命令に変換されます)。

理想的には、EIPが次の命令を指しているので、それが実際に実行される次の命令になります。ただし、現在の実行フローパスからの一種の迂回があるため(これは、現在の理由で予想されcallます)、EIPの値が変更されます。なんで?これは、別の命令、たとえばアドレス1234(またはその他)にある可能性があるため、実行する必要がある場合があります。ただし、プログラマーが意図したとおりにプログラムの実行フローを完了するには、迂回アクティビティが実行された後、迂回が発生しなかった場合に次に実行されるはずのアドレス2に制御を戻す必要があります。return address作成されているコンテキストでは、このアドレスを2と呼びますcall

問題1

したがって、迂回が実際に発生する前に、差出人アドレス2を一時的にどこかに保存する必要があります。

使用可能なレジスタのいずれか、またはメモリの場所などに格納するための多くの選択肢があった可能性があります。しかし、(正当な理由があると思いますが)リターンアドレスをスタックに格納することが決定されました。

したがって、ここで行う必要があるのは、スタックの最上位がスタック上の次のアドレスを指すように、ESP(スタックポインター)をインクリメントすることです。したがって、アドレス(292など)を指していたTOS'(増分前のTOS)が増分され、アドレス293を指し始めます。ここに。を配置しますreturn address 2。だからこのようなもの:

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

これで、差出人住所を一時的にどこかに保存するという目標を達成したようです。ここで、迂回を行う必要がありcallます。そして、私たちはできました。しかし、小さな問題があります。呼び出された関数の実行中に、スタックポインタは、他のレジスタ値とともに、複数回操作される可能性があります。

問題2

したがって、私たちの差出人住所はまだスタックの293の場所に格納されていますが、呼び出された関数の実行が終了した後、実行フローは293に移動する必要があることをどのように認識し、そこに差出人住所が見つかりますか?

したがって、(正当な理由で)上記の問題を解決する方法の1つは、スタックアドレス293(リターンアドレスがある場所)をEBPと呼ばれる(指定された)レジスタに格納することです。では、EBPの内容はどうでしょうか。それは上書きされませんか?確かに、それは有効なポイントです。それでは、EBPの現在の内容をスタックに保存してから、このスタックアドレスをEBPに保存しましょう。このようなもの:

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

スタックポインタがインクリメントされます。EBPの現在の値(EBP'と表記)、つまりxxxは、スタックの最上位、つまりアドレス294に格納されます。これで、EBPの現在の内容のバックアップを取得したので、安全に配置できます。 EBPへの他の値。そのため、スタックの最上位の現在のアドレス、つまりアドレス294をEBPに配置します。

上記の戦略を実行して、上記の問題2を解決します。どのように?したがって、実行フローがどこからリターンアドレスをフェッチする必要があるかを知りたい場合は、次のようになります。

  • 最初にEBPから値を取得し、ESPにその値を指定します。この場合、これにより、TOS(スタックの最上位)がアドレス294を指すようになります(これがEBPに格納されているため)。

  • 次に、EBPの以前の値を復元します。これを行うには、単純に294(TOS)の値を取得します。これはxxx(実際にはEBPの古い値でした)であり、EBPに戻します。

  • 次に、スタックポインタをデクリメントして、スタック内の次に低いアドレス(この場合は293)に移動します。したがって、最終的に293に到達します(それが私たちの問題2であったことを参照してください)。ここで、2である差出人住所が見つかります。

  • 最終的にこの2つがEIPにポップアウトされます。これは、迂回が発生しなかった場合に理想的に実行されるはずの命令です。覚えておいてください。

そして、リターンアドレスを一時的に保存し、それを取得するために、すべてのジャグラーで実行されているのを見たばかりの手順は、関数プロローグ(関数の前call)とエピローグ(関数の前)で実行される手順とまったく同じretです。方法はすでに答えられました、私たちは理由も答えまし

最後に、簡潔にするために、スタックアドレスが逆に大きくなる可能性があるという事実には注意を払いませんでした。

于 2020-04-19T15:37:47.063 に答える
-6

すべての関数には、同一のプロローグ(関数コードの開始)とエピローグ(関数の終了)があります。

プロローグ:プロローグの構造は次のようになります:push ebp mov esp、ebp

エピローグ:プロローグの構造は次のようになります。

詳細:プロローグとエピローグとは

于 2015-03-05T13:50:56.417 に答える