なぜスタックを後方に拡張するのですか?


46

Cコードをコンパイルしてアセンブリを見ると、スタックは次のように後方に成長します。

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)-これは、ベースポインタまたはスタックポインタが実際にメモリアドレスを上に移動する代わりに下に移動することを意味しますか?何故ですか?

に変更$5, -4(%rbp)$5, +4(%rbp)、コンパイルしてコードを実行しましたが、エラーはありませんでした。では、なぜメモリスタックをさかのぼる必要があるのでしょうか?


2
-4(%rbp)ベースポインターはまったく移動せず、動作しない+4(%rbp)可能性があることに注意してください。
マーガレットブルーム

14
なぜ我々はまだ後退しなければならないのか」-前進することの利点は何だと思いますか?最終的には、それは問題ではなく、1つを選択するだけです。
ベルギ

31
「なぜスタックを後方に拡張するのですか?」-他の誰かがmallocヒープを後方に
拡張

2
@MargaretBloom:どうやらOPのプラットフォーム上では、CRTスタートアップコードはmainRBPを破壊するかどうかは気にしません。それは確かに可能です。(そして、はい、書き込み4(%rbp)は保存されたRBP値を踏むことになります)。実際、このメインは決して実行しないmov %rsp, %rbpため、OPが実際にテストした場合、メモリアクセスは呼び出し元のRBPに相対的です!!! これが実際にコンパイラーの出力からコピーされた場合、いくつかの命令は省略されました!
ピーターコーデス

1
「後方」または「前方」(または「下」と「上」)はあなたの視点に依存しているように思えます。メモリを先頭に低いアドレスを持つ列として図式化した場合、スタックポインタをデクリメントしてスタックを拡大することは、物理スタックに似ています。
ジェームズドリン

回答:


86

これは、ベースポインターまたはスタックポインターが実際にメモリアドレスを上に移動するのではなく下に移動することを意味しますか?何故ですか?

はい、push命令はスタックポインタをデクリメントしてスタックに書き込みpop、逆の場合はスタックから読み取り、スタックポインタをインクリメントします。

これは、メモリが限られているマシンの場合、スタックが高く配置されて下方に拡大され、ヒープが低く配置されて上方に拡大されるという点で、やや歴史的です。「空きメモリ」のギャップは1つしかありません。ヒープとスタックの間にあり、このギャップは共有されます。どちらも個別に必要に応じてギャップに拡大できます。したがって、スタックとヒープが衝突して空きメモリがなくなると、プログラムはメモリ不足になります。 

スタックとヒープの両方が同じ方向に成長する場合、2つのギャップがあり、スタックは実際にヒープのギャップに成長できません(逆もまた問題です)。

元々、プロセッサには専用のスタック処理命令がありませんでした。ただし、ハードウェアにスタックサポートが追加されたため、このパターンは下方に向かって成長し、現在でもプロセッサはこのパターンに従います。

64ビットマシンでは、複数のギャップを許可するのに十分なアドレススペースがあると主張することができます。証拠として、プロセスに複数のスレッドがある場合、複数のギャップが必然的に発生します。複数のギャップシステムでは成長の方向はほぼ任意であるため、これは物事を変える十分な動機ではありませんが、伝統/互換性は規模を傾けます。


あなたは、スタックの向きを変えるか、あるいは専用のプッシュ&ポップ命令(の使用を断念するために、指示を扱うCPUスタックを変更する必要があるだろうpushpopcallret、その他)。

MIPS命令セットアーキテクチャには専用のpush&がないpopため、どちらの方向にもスタックを拡張するのが実用的です。シングルスレッドプロセス用に1ギャップのメモリレイアウトが必要な場合がありますが、スタックとヒープを上に拡張できます下向き。ただし、これを行うと、一部のC 可変引数コードで、ソースまたは内部パラメータの受け渡しの調整が必要になる場合があります。

(実際、MIPSには専用のスタック処理がないため、スタックからポップするために正確な逆を使用し、また、オペレーティングシステムは、選択されたスタック使用モデルを尊重します。実際、一部の組み込みシステムおよび一部の教育システムでは、MIPSスタックが上方に拡張されています。


32
それだけではありませんpushし、popほとんどのアーキテクチャではなく、はるかに重要な割り込み処理は、callret、および任意の他焼きにしているスタックとの相互作用。
デデュプリケーター

3
ARMは、4つすべてのスタックフレーバーを使用できます。
マーガレットブルーム

14
それが価値があることに関しては、どちらの選択も同様に良いという意味で、「成長方向はarbitrary意的ではない」と思います。成長には、バッファの終わりをオーバーフローさせることで、保存されたリターンアドレスを含む以前のスタックフレームをオーバーフローさせるという特性があります。成長には、バッファクローバのオーバーフローが同じまたはそれ以降のストレージ(バッファが最新でない場合、後のバッファがある可能性があります)の呼び出しフレームと、場合によっては未使用のスペース(すべてガードを想定)のみをオーバーフローさせる特性がありますスタックの後のページ)。だから、安全性の観点から、育っすることは非常に好ましいと思われる
R ..

6
@R ..:脆弱な関数は通常リーフ関数ではないため、成長してもバッファオーバーランの悪用が排除されるわけではありません。他の関数を呼び出し、バッファの上にリターンアドレスを配置します。呼び出し元からポインタを取得するリーフ関数は、独自のリターンアドレスを上書きする可能性があります。例えば、関数がスタックにバッファを割り当ててそれをgets()に渡したり、strcpy()インライン化されない場合、それらのライブラリ関数の戻り値は上書きされた戻りアドレスを使用します。現在、下向きに成長するスタックでは、呼び出し元が戻ってきます。
ピーターコーデス

5
@PeterCordes:実際、私のコメントでは、オーバーフローしたバッファと同じレベルまたはより最近のスタックフレームは依然として潜在的に上書き可能であると指摘しましたが、それははるかに少ないです。clobbering関数が、バッファのある関数によって直接呼び出されるリーフ関数の場合(例strcpy:)、オーバーフローする必要がない限り、リターンアドレスがレジスタに保持されているアーチでは、clobberにアクセスすることはできません住所。
R ..

8

特定のシステムでは、スタックは高いメモリアドレスから始まり、低いメモリアドレスに「下向きに」成長します。(低から高への対称的なケースも存在します)

そして、-4と+4から変更して実行したので、それが正しいという意味ではありません。実行中のプログラムのメモリレイアウトはより複雑であり、この非常に単純なプログラムで即座にクラッシュしなかったという事実に寄与する可能性のある他の多くの要因に依存しています。


1

スタックポインターは、割り当てられたスタックメモリと未割り当てのスタックメモリの境界を指します。下方向に成長するということは、割り当てられたスタックスペースの最初の構造の開始点を指し、他の割り当てられたアイテムがより大きなアドレスに続くことを意味します。ポインタが割り当てられた構造の開始点を指すことは、他の方法よりもはるかに一般的です。

現在、最近の多くのシステムには、ローカル変数ストレージが散在した状態で呼び出しチェーンを把握するために、ある程度確実に展開できるスタックフレーム用の個別のレジスタがあります。一部のアーキテクチャでこのスタックフレームレジスタを設定する方法は、のスタックポインターではなく、ローカル変数ストレージの背後を指すことを意味します。したがって、このスタックフレームレジスタを使用するには、ネガティブインデックスが必要です。

スタックフレームとそのインデックスはコンパイルされたコンピューター言語の側面であるため、貧弱なアセンブリ言語プログラマーではなく、「不自然さ」に対処する必要があるのはコンパイラーのコードジェネレーターです。

そのため、スタックを下向きに選択する歴史的な理由はありましたが(アセンブリ言語でプログラミングし、適切なスタックフレームを設定しなくてもスタックの一部は保持されます)、それらは目立たなくなりました。


2
「最近では、多くのシステムで、スタックフレーム用に個別のレジスタがあります」と、時代遅れになっています。豊富なデバッグ情報形式により、最近ではフレームポインターの必要性が大幅に排除されました。
ピーターグリーン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.