C でいくつかのトレーニング資料を準備しており、サンプルを典型的なスタック モデルに適合させたいと考えています。
Linux、Windows、Mac OSX (PPC および x86)、Solaris、および最新の Unix では、C スタックはどの方向に成長しますか?
C でいくつかのトレーニング資料を準備しており、サンプルを典型的なスタック モデルに適合させたいと考えています。
Linux、Windows、Mac OSX (PPC および x86)、Solaris、および最新の Unix では、C スタックはどの方向に成長しますか?
スタックの増加は、通常、オペレーティング システム自体には依存しませんが、それが実行されているプロセッサに依存します。たとえば、Solaris は x86 と SPARC で動作します。Mac OSX (おっしゃる通り) は PPC と x86 で動作します。Linux は、仕事中の私の大きな System z からちっぽけな小さな腕時計まで、あらゆるもので動作します。
CPU が何らかの選択肢を提供する場合、OS で使用される ABI / 呼び出し規則は、自分のコードで他のすべてのコードを呼び出す場合に必要な選択を指定します。
プロセッサとその方向は次のとおりです。
これらの最後のいくつかで私の年齢を示しているのは、1802 が初期のシャトルを制御するために使用されたチップ (処理能力に基づいて、ドアが開いているかどうかを感知していると思われます :-) と、私の 2 番目のコンピューターであるCOMX-35 (私のZX80に続いて)。
PDP11 の詳細は here から収集され、8051 の詳細はhereから収集されました。
SPARC アーキテクチャは、スライディング ウィンドウ レジスタ モデルを使用します。構造的に見える詳細には、有効で内部にキャッシュされたレジスタ ウィンドウの循環バッファも含まれており、オーバー/アンダーフロー時のトラップも含まれています。詳しくはこちらをご覧ください。SPARCv8のマニュアルで説明されているように、SAVE および RESTORE 命令は、ADD 命令とレジスタ ウィンドウのローテーションのようなものです。通常の負の定数の代わりに正の定数を使用すると、上向きに成長するスタックが得られます。
前述の SCRT 手法は別のものです。1802 では、SCRT (標準の呼び出しと戻りの手法) 用に 16 ビット レジスタがいくつか使用されていました。SEP Rn
1 つはプログラム カウンターで、任意のレジスターを命令で PC として使用できます。1 つはスタック ポインターで、2 つは常に SCRT コード アドレスを指すように設定されていました。1 つは呼び出し用、もう 1 つはリターン用です。レジスタは特別な方法で処理されませんでした。これらの詳細は記憶によるものであり、完全に正しいとは限りません。
たとえば、R3 が PC、R4 が SCRT 呼び出しアドレス、R5 が SCRT 戻りアドレス、R2 が「スタック」(ソフトウェアで実装されているので引用) の場合、SEP R4
R4 を PC に設定し、SCRT の実行を開始します。コードを呼び出します。
次に、R2「スタック」にR3を格納し(R6は一時ストレージに使用されたと思います)、上下に調整し、R3に続く2バイトを取得してR3にロードし、新しいSEP R3
アドレスで実行します。
戻るにはSEP R5
、R2 スタックから古いアドレスを取り出し、それに 2 つ追加して (呼び出しのアドレス バイトをスキップするため)、それを R3 にロードしSEP R3
て、前のコードの実行を開始します。
すべての 6502/6809/z80 スタックベースのコードの後、最初は頭を包み込むのは非常に困難ですが、頭を壁にぶつけるような方法ではまだエレガントです。また、チップの大きな売りの機能の 1 つは、16 個の 16 ビット レジスタの完全なスイートでしたが、そのうち 7 個 (SCRT 用に 5 個、DMA 用に 2 個、およびメモリからの割り込み用) をすぐに失いました。ああ、現実に対するマーケティングの勝利 :-)
System z は実際には非常に似ており、R14 および R15 レジスタを呼び出し/復帰に使用します。
C++ の場合 (C に適合) stack.cc :
static int
find_stack_direction ()
{
static char *addr = 0;
auto char dummy;
if (addr == 0)
{
addr = &dummy;
return find_stack_direction ();
}
else
{
return ((&dummy > addr) ? 1 : -1);
}
}
下方に拡張することの利点は、古いシステムでは、通常、スタックがメモリの最上位にあることです。プログラムは通常、メモリを一番下から埋めていくため、この種のメモリ管理により、スタックの一番下を測定して適切な場所に配置する必要性が最小限に抑えられました。
私が見る限り、この点に触れていない他の回答へのほんの少しの追加:
スタックを下方に拡張すると、スタック内のすべてのアドレスがスタック ポインターに対して正のオフセットになります。未使用のスタック領域のみを指すため、負のオフセットは必要ありません。これにより、プロセッサがスタックポインタ相対アドレッシングをサポートしている場合に、スタック位置へのアクセスが簡素化されます。
多くのプロセッサには、一部のレジスタに相対的な正のオフセットのみを使用したアクセスを許可する命令があります。それらには、多くの近代的な建築物だけでなく、いくつかの古い建築物も含まれます。たとえば、ARM Thumb ABI は、単一の 16 ビット命令ワード内にエンコードされた正のオフセットを使用して、スタックポインタ相対アクセスを提供します。
スタックが上向きに成長した場合、スタックポインタに関連するすべての有用なオフセットは負になり、直感的でなく便利ではなくなります。また、構造体のフィールドへのアクセスなど、レジスタ相対アドレス指定の他のアプリケーションとは相容れません。
MIPS および多くの最新のRISC アーキテクチャpush
(PowerPC、RISC-V、SPARC など) には、 andpop
命令はありません。これらの操作は、スタック ポインターを手動で調整し、調整されたポインターに相対的に値をロード/ストアすることによって明示的に行われます。すべてのレジスタ (ゼロ レジスタを除く) は汎用であるため、理論的にはどのレジスタもスタック ポインタにすることができ、スタックはプログラマが望む任意の方向に拡張できます。
とはいえ、スタックは通常、ほとんどのアーキテクチャで成長します。おそらく、スタックとプログラム データまたはヒープ データが成長して互いに衝突するケースを回避するためです。sh-'s answer に記載されている大きなアドレス指定の理由もあります。いくつかの例: MIPS ABI は下向きに成長し、スタック ポインターとして$29
(AKA $sp
) を使用します。RISC-V ABI も下向きに成長し、スタック ポインターとして x2 を使用します。
Intel 8051 では、おそらくメモリ空間が非常に小さい (元のバージョンでは 128 バイト) ため、ヒープがなく、スタックを上に置く必要がないため、スタックが大きくなります。下から
https://en.wikipedia.org/wiki/Calling_conventionで、さまざまなアーキテクチャでのスタックの使用に関する詳細情報を見つけることができます。
こちらもご覧ください
ほとんどのシステムでは、スタックが減少します。https: //gist.github.com/cpq/8598782 にある私の記事では、スタックが減少する理由について説明しています。簡単です: メモリの固定チャンクに 2 つの成長するメモリ ブロック (ヒープとスタック) を配置する方法は? 最善の解決策は、それらを反対側に置き、お互いに向かって成長させることです.
プログラムに割り当てられたメモリには「永続データ」、つまりプログラム自体のコードが下部にあり、ヒープが中央にあるため、サイズが小さくなります。スタックを参照する別の固定ポイントが必要なので、トップのままです。これは、ヒープ上のオブジェクトに潜在的に隣接するまで、スタックが成長することを意味します。
このマクロは、UB なしで実行時にそれを検出する必要があります。
#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);
__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) {
return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}