関数呼び出しの詳細説明(関数状態保存パラメーターの転送と戻り値)

アセンブリ関数呼び出しの観点からプログラムを理解する

プログラムは、順次実行される命令パイプライン(PipeLine)であることはすでに知っています。分岐およびループロジックは、パイプラインで後方または前方にジャンプすることで実装できます。
実際、関数呼び出しは単なるジャンプです。関数が呼び出されると、その関数の命令ストリームの先頭にジャンプし、関数が実行された後、元に戻ります。
この関数は、外部から書き込まれたデータ(入力)を取得し、独自の固有データ(ローカル状態)を保持し、データを外部に書き込む(出力)ことができます。理論的には、プログラムが所有する関数の数は無制限ですが、レジスタの数は非常に少ないです。無限関数は、限られた数のレジスタを使用してそれぞれのデータをどのように保存できますか?

1.関数呼び出しを実現してジャンプで戻る方法

あなただけがジャンプする目的地を知る必要があります。ただし、ジャンプして戻りたい場合は、ジャンプ後に読み取れる場所に戻る前にアドレスを保存する必要があります。
アセンブリ命令呼び出しは、ジャンプバックアドレスをメモリに保存してからジャンプするために使用されますが、retは、呼び出し命令によって保存されたジャンプバックアドレスを使用して関数から戻ることができます。jmp命令と同様に、
call命令の後にはアドレスが続くことがよくありますしかし、実際のジャンプの前に、call命令の次の命令のアドレスをメモリM [%rsp]に保存しました。
関数が非常に多い場合、1%のrspで十分ですか?次の2つの事実に基づいて、私たちは言う:十分です。

  • 事実1:関数がいくつあっても、特定の時間に実行される関数は1つだけです。したがって、特定の時間に必要なのは1%のrspだけです。問題は、この%rspを他の%rspに導出する方法です。
  • 事実2:関数がいくつあっても、実行された各関数は前の関数によって呼び出されます。したがって、現在の%rspから他のすべての関数の%rspを導出する必要はありません。前の関数(呼び出し元)の%rspのみを知る必要があります。これは足し算と引き算ではないですか?

たとえば、メイン関数の%rspがxの場合、関数Aが関数Bを呼び出すと、%rsp =%rsp + Nになり、Bが戻ると、%rsp =%rsp-Nになります。
%rspはメモリのアドレス指定に使用されるため、定数Nは8です。
これは、スタックメモリの古典的なスケーリングモデルです。

そんなに多くを言ったので、コードを見てみましょう:

400540: lea     2(%rdi), %rax
400544: ret

400545: sub     5, %rdi
400549: call    400540
40054e: add     %rax, %rax
400551: ret

40055b: call    400545
400560: mov     %rax, %rdx

40055bの実行時に%rspの値が0x7fffffffe820の場合:400545を呼び出し、呼び出しが実行された後、%rspの値は8だけ減って0x7fffffffe818になり、値0x400560がメモリM [0x7fffffe818]に保存されます。コマンドmovのアドレス。その後、プログラムは0x400545にジャンプして実行を開始します。

0x400549には別の呼び出し命令があるため、同じロジックが再び出現し、%rsp-8の値は0x7fffffff10で、メモリM [0x7fffffffe810]は次の追加命令0x40054eのアドレスを格納します。その後、プログラムは0x400540にジャンプします。

ret命令が0x400544で検出されました。このとき、スタックポインターアドレスは%rspから0x7fffffffe810として読み取られ、次にM [0x7fffffffe810]の値が0x40054eから読み取られました。次に、%rsp plus 8は、最後の呼び出し命令の前の値に復元し、0x40054eにジャンプして、最後の呼び出し命令の直後に実行を開始します。

0x400551でのretプロセスは前のステップと同様であり、ここでは繰り返されません。
要約は、関数呼び出しと同期して拡張および縮小するスタックポインタを通じて関数の戻り位置を保存することであり、ジャンプおよびジャンプの両方が可能です。

第二に、レジスタを介して関数の状態を保存する方法

ほとんどの関数にはパラメーターと戻り値があります。制限されたレジスタを介してこれらのデータを保存する方法
簡単です。レジスタに明確な責任が割り当てられていることを思い出してください。%rdi、%rsi、%rdx、%rcx、%r8、%r9は、関数の6つのパラメータを保存するために使用され、%raxは関数の戻りを保存するために使用されます価値。

呼び出し関数では、呼び出し命令を実行する前にパラメーターレジスターにパラメーターを保存します。呼び出された関数では、レジスターからの値の読み取りは、使用されるパラメーターと同じです(%rdiが最初、%rsiが2 ...待つ)。

呼び出された関数retが呼び出しの次の命令位置に到達した後、呼び出された関数の戻り値は%raxに保存されます。

ただし、これはまだ問題です。通常、戻り値は1つだけですが、パラメータが6つだけではない場合があります。関数のパラメータが6つを超える場合はどうすればよいですか?
この時、再びスタックメモリに依存します。

第三に、メモリを介してより多くの関数の状態を保存する方法

関数の状態をメモリに保存する状況はいくつかあります。

  • 呼び出された関数のパラメーター数が6を超える場合
  • 関数のローカルデータがレジスタの数を超えています
  • 関数local dataは配列を使用するため、データにアクセスするには配列を使用する必要があります
  • 関数はローカルデータの&演算を使用してメモリアドレスを取得するため、データをメモリに保存する必要があります

どちらの場合も、データを格納するためにメモリを使用する必要があります。これらのデータは、スタックメモリと総称される関数スタックポインタに基づいて特定のオフセット位置に格納されます(スケーリング方法がスタックデータ構造に類似しているため)。
通常は、プッシュおよびポップ命令を使用します。push命令は、現在のスタックポインタ値R [%rsp]を8だけ減らし、メモリM [R [%rsp]]に保存される値を保存します。ポップポインターは、メモリ値M [R [%rsp]]を指定されたレジスターに取り、R [%rsp]に8を加算してスタックポインターの位置を復元します。
コンパイル時間は、R [%rsp]が関数が戻る前に関数呼び出しの開始時の値に確実に復元できるように、各プッシュがポップに対応する必要があることを保証します。同時に、コンパイル時に自動的に維持されるため、開発者はスタックスペースのメモリを解放することを心配する必要がありません。

3.1スタックメモリを使用してより多くの関数パラメーターを保存する

PがQを呼び出すと、パラメーターは最初に対応するレジスターに保存されます。レジスタの数が十分でない場合、残りのパラメータは、プッシュ命令を使用してスタックメモリに保存されます。これらのパラメータを準備した後、呼び出し命令を呼び出します。
Qを実行すると、最初の6つのパラメーターが通常どおりレジスターから読み取られ、書き込まれます。その後のパラメーターは、Mのオフセットに従って読み取られ、書き込まれます[R [%rsp] + 8n]。パラメータは、%rsp以上(+ 8n)からアドレス指定されるため、Pのスタックスペースに格納されることに注意してください。

3.2スタックメモリを使用して関数のローカルデータを保存する

レジスターの数は制限されており、呼び出し元の保存と呼び出し先の保存の2つのカテゴリーに分類されます。P実行中、ローカルデータが多すぎる場合は、データを使用する前にスタックの呼び出し先保存レジスタにデータを保存し、スタックから元の値を取得してから戻る必要があります。

呼び出し先の保存レジスタを追加した後でも、その数が十分でない場合は、データをスタックにプッシュする必要があります。
前述のように、配列内のデータにアクセスする必要がある場合は、データをメモリに保存してから、互いに関連しないレジスタを使用する代わりに、最初のアドレスとオフセットを使用してデータにアクセスする必要があります。現時点では、pushを使用して配列の要素を1つずつメモリに保存できますが、pushを使用してデータを保存するには、2つの欠点があります。各push命令にはpop命令が必要であり、pushの数は事前にわかっている必要があります。

プッシュポップの消費を減らし、配列の長さを動的に指定できるようにするために、コンパイラーはスタックフレームモードを使用します。
スタックフレームモードは、別のregister%rbpに依存します。その使用プロセスは次のとおりです。

  • (push%rbp)%rbpの元の値を保存します
  • (movq%rsp、%rbp)%rbpを使用して、%rspの値をスタックフレームの開始点として保存します(ベースポインター)
  • (subq 8n、%rsp)%rsp-8n-ローカル変数を格納するためにn個のアドレスを開く
  • (addq 8n、%rsp)%rsp + 8n割り当てられたn個のアドレスを再利用する
  • (movq%rbp、%rsp)%rspを%rbpから復元
  • (popq%rbp)%rspから%rbpの元の値を復元します

もちろん、n個の要素に必要なスペースの動的割り当てをサポートしますが、nを無限に大きくすることはできず、特定の数を構成できます。このサイズを超えてエラーが発生した場合は、StackOverflowに移動して解決方法を見つけてください。

4.まとめ

関数呼び出しで解決すべき主な問題が2つあります。

  • どうして
  • 関数の状態を保存する方法

最初の問題の解決策は次のとおりです。

  • 関数と同期して拡張および縮小するスタックポインターを使用して、戻りアドレスを保存する

2番目の問題の解決策は次のとおりです。

  • レジスタは、関数の独立した状態を維持するために、呼び出し元の保存と呼び出し先の保存に分類されます。
  • スタックポインタに従ってレジスタの数を超えるデータをメモリにプッシュし、popを​​使用してアドレスを回復し、スタックポインタの状態を維持します。
  • アレイの状態データをフレームに保存し、プッシュポップの消費を減らし、動的スタックサイズをサポートします

元のアドレス:https://www.imhuwq.com/2019/03/10/%E4%BB%8E%E6%B1%87%E7%BC%96%E7%9A%84%E8%A7%92% E5%BA%A6%E7%90%86%E8%A7%A3%E7%A8%8B%E5%BA%8F%EF%BC%88%E4%B8%89%EF%BC%89%E2% 80%94%E2%80%94%20%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8 /

8件のオリジナル記事を公開 いいね4 訪問数290

おすすめ

転載: blog.csdn.net/qq_45521281/article/details/105408267