スタックは上向きまたは下向きに成長しますか?


89

私はcにこのコードを持っています:

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

出力は次のとおりです。

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

したがって、からaまでa[2]、メモリアドレスはそれぞれ4バイトずつ増加することがわかります。ただし、からqまでs、メモリアドレスは4バイト減少します。

私は2つのことを疑問に思います:

  1. スタックは大きくなりますか、それとも小さくなりますか?(この場合、私には両方のように見えます)
  2. a[2]qメモリアドレスの間で何が起こりますか?なぜそこに大きなメモリの違いがあるのですか?(20バイト)。

注:これは宿題の質問ではありません。スタックがどのように機能するのか興味があります。助けてくれてありがとう。


順序は任意です。ギャップはおそらく&qや&sなどの中間結果を保存することです-逆アセンブリを見て、自分の目で確かめてください。
トムレイズ

同意します。アセンブリコードを読んでください。あなたがこの種の質問をしているなら、それを読むことを学ぶ時が来ました。
パーヨハンソン

回答:


74

スタックの動作(成長または成長)は、アプリケーションバイナリインターフェイス(ABI)と、呼び出しスタック(別名アクティベーションレコード)の編成方法によって異なります。

プログラムは、その存続期間を通じて、OSなどの他のプログラムと通信する必要があります。ABIは、プログラムが別のプログラムと通信する方法を決定します。

異なるアーキテクチャのスタックはどちらの方法でも拡張できますが、アーキテクチャの場合は一貫性があります。このwikiリンクを確認しください。ただし、スタックの成長は、そのアーキテクチャのABIによって決定されます。

たとえば、MIPS ABIを使用する場合、コールスタックは次のように定義されます。

関数「fn1」が「fn2」を呼び出すと考えてみましょう。これで、「fn2」で見られるスタックフレームは次のようになります。

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

これで、スタックが下向きに成長することがわかります。したがって、変数が関数のローカルフレームに割り当てられている場合、変数のアドレスは実際には下向きに大きくなります。コンパイラは、メモリ割り当ての変数の順序を決定できます。(あなたの場合、最初にスタックメモリが割り当てられるのは「q」または「s」のいずれかです。ただし、通常、コンパイラは変数の宣言の順序に従ってスタックメモリの割り当てを行います)。

ただし、配列の場合、割り当てには単一のポインタしかなく、割り当てる必要のあるメモリは実際には単一のポインタによってポイントされます。配列のメモリは連続している必要があります。したがって、スタックは下向きに成長しますが、アレイの場合、スタックは成長します。


5
さらに、スタックが上向きか下向きかを確認したい場合。main関数でローカル変数を宣言します。変数のアドレスを出力します。mainから別の関数を呼び出します。関数でローカル変数を宣言します。そのアドレスを印刷します。印刷されたアドレスに基づいて、スタックが拡大または縮小すると言えます。
Ganesh Gopalasubramanian 2009年

ガネーシュに感謝します。小さな質問があります。3番目のブロックでuが描いた図では、f1がf2を呼び出すときに、f1アドレス(戻りアドレス)を格納する必要があるため、「calleR保存レジスタがCALLERで使用されている」という意味でしたか。 f2)およびf1(calleR)レジスターの場合、f2(callee)レジスターではありません。正しい?
CSawy 2013

44

これは実際には2つの質問です。1つは、ある関数が別の関数を呼び出すとき(新しいフレームが割り当てられるとき)にスタックがどのように成長するかに関するものであり、もう1つは、特定の関数のフレームに変数がどのように配置されるかに関するものです。

どちらもC標準では指定されていませんが、答えは少し異なります。

  • 新しいフレームが割り当てられると、スタックはどちらの方向に成長しますか?関数f()が関数g()を呼び出す場合、fのフレームポインタはのフレームポインタよりも大きくなりますか、それとも小さくなりgますか? これはどちらの方向にも進むことができます-それは特定のコンパイラとアーキテクチャに依存します(「呼び出し規約」を調べてください)が、特定のプラットフォーム内で常に一貫しています(いくつかの奇妙な例外を除いて、コメントを参照してください)。下向きがより一般的です。これは、x86、PowerPC、MIPS、SPARC、EE、およびCellSPUに当てはまります。
  • 関数のローカル変数は、スタックフレーム内にどのように配置されていますか?これは特定されておらず、完全に予測不可能です。コンパイラはローカル変数を自由に配置できますが、最も効率的な結果を得るのが好きです。

7
「特定のプラットフォーム内で常に一貫している」-保証されていません。スタックが動的に拡張された、仮想メモリのないプラットフォームを見てきました。新しいスタックブロックは事実上マロックされていました。つまり、あるスタックブロックをしばらく「ダウン」してから、突然別のブロックに「横向き」に移動しました。「サイドウェイズ」とは、引き分けの運次第で、より大きなまたはより小さなアドレスを意味する可能性があります。
スティーブジェソップ

2
項目2の詳細については、コンパイラーは、変数をメモリーに入れる必要がない(変数の存続期間中はレジスターに保持する)、および/または2つ以上の変数の存続期間が必要ないかどうかを判断できる場合があります。オーバーラップすると、コンパイラは複数の変数に同じメモリを使用することを決定する場合があります。
Michael Burr

2
S / 390(IBM zSeries)には、スタック上で成長するのではなく、呼び出しフレームがリンクされるABIがあると思います。
ephemient 2009年

2
S / 390で修正。呼び出しは「BALR」、分岐およびリンクレジスタです。戻り値は、スタックにプッシュされるのではなく、レジスタに入れられます。return関数は、そのレジスタの内容への分岐です。スタックが深くなると、スペースがヒープに割り当てられ、それらがチェーンされます。これは、「/ bin / true」に相当するMVSの名前が「IEFBR14」になる場所です。最初のバージョンには、「BR 14」という単一の命令があり、これは、戻りアドレスを含むレジスタ14の内容に分岐していました。
janm 2009年

1
また、PICプロセッサ上の一部のコンパイラは、プログラム全体の分析を行い、各関数の自動変数に固定位置を割り当てます。実際のスタックは小さく、ソフトウェアからはアクセスできません。差出人住所専用です。
janm 2009年

13

スタックが成長する方向は、アーキテクチャによって異なります。とは言うものの、私の理解では、成長するスタックを持つハードウェアアーキテクチャはごくわずかです。

スタックが成長する方向は、個々のオブジェクトのレイアウトとは無関係です。したがって、スタックは大きくなる可能性がありますが、配列は大きくなりません(つまり、&array [n]は常に<&array [n + 1]になります)。


4

標準には、スタック上で物事をどのように編成するかを義務付けるものはまったくありません。実際、配列要素の演算を適切に実行するスマートさがあれば、スタック上の隣接する要素に配列要素をまったく格納しない適合コンパイラを構築できます(たとえば、1がa [0]から1K離れており、それを調整できます)。

異なる結果が得られる理由は、スタックが成長して「オブジェクト」を追加する一方で、配列は単一の「オブジェクト」であり、逆の順序で配列要素が昇順である可能性があるためです。ただし、方向が変わる可能性があり、次のようなさまざまな理由で変数が入れ替わる可能性があるため、この動作に依存するのは安全ではありません。

  • 最適化。
  • アラインメント。
  • 人の気まぐれは、コンパイラのスタック管理部分です。

スタックの方向に関する私の優れた論文については、こちらをご覧ください:-)

あなたの特定の質問に答えて:

  1. スタックは大きくなりますか、それとも小さくなりますか?
    (標準の観点からは)まったく問題ではありませんが、あなたが尋ねたように、実装に応じて、メモリ内で拡大または縮小する可能性があります。
  2. a [2]とqのメモリアドレスの間で何が起こりますか?なぜそこに大きなメモリの違いがあるのですか?(20バイト)?
    それは(標準の観点からは)まったく問題ではありません。考えられる理由については、上記を参照してください。

ほとんどのCPUアーキテクチャが「成長」方法を採用しているとあなたがリンクしているのを見ましたが、そうすることの利点があるかどうか知っていますか?
Baiyan Huang 2011

わからない、本当に。それはだ可能スタックが交差する可能性を最小限にするように、HIGHMEMから下方に行く必要がありますので、誰かの思考コードが0から上向きに行くことに。ただし、一部のCPUは、ゼロ以外の場所でコードの実行を開始するため、そうではない場合があります。ほとんどのことと同じように、おそらくそれは誰かがそれをやろうと思った最初の方法だったという理由だけでそのように行われた:-)
paxdiablo 2011

@lzprgmr:特定の種類のヒープ割り当てを昇順で実行することにはいくつかのわずかな利点があり、スタックとヒープが共通のアドレス空間の両端に配置されることは歴史的に一般的でした。静的+ヒープ+スタックの合計使用量が使用可能なメモリを超えていなければ、プログラムが使用したスタックメモリの正確な量を心配する必要はありませんでした。
スーパーキャット2014年

3

x86では、スタックフレームのメモリ「割り当て」は、スタックポインタから必要なバイト数を差し引くだけで構成されます(他のアーキテクチャも同様だと思います)。この意味で、スタックは「下に」成長すると思います。スタックを深く呼び出すとアドレスが徐々に小さくなります(ただし、メモリは左上の0から始まり、移動するにつれてアドレスが大きくなると常に想定しています。右に折りたたむので、私の心のイメージではスタックが大きくなります...)。宣言されている変数の順序は、それらのアドレスとは関係がない可能性があります-副作用を引き起こさない限り、標準ではコンパイラが変数を並べ替えることができると思います(間違っている場合は誰かが私を修正してください) 。彼ら'

配列の周りのギャップはある種のパディングかもしれませんが、それは私には不思議です。


1
実際、コンパイラーがそれらを並べ替えることができることは知っています。なぜなら、それらをまったく割り当てないことも自由だからです。それらをレジスタに入れるだけで、スタックスペースをまったく使用しません。
rmeador 2009年

それらのアドレスを参照する場合、それらをレジスタに入れることはできません。
フロリン

良い点は、それを考慮していませんでした。しかし、コンパイラがそれらを並べ替えることができるという証拠としては、それでも十分です。少なくとも
いつか

1

まず、メモリ内の8バイトの未使用スペース(12ではなく、スタックが下に大きくなることを覚えておいてください。したがって、割り当てられていないスペースは604から597になります)。なぜ?。すべてのデータ型は、サイズで割り切れるアドレスから始まるメモリ内のスペースを使用するためです。この場合、3つの整数の配列は12バイトのメモリ空間を取り、604は12で割り切れません。したがって、12で割り切れるメモリアドレスに遭遇するまで空の空間を残します。それは596です。

したがって、配列に割り当てられるメモリ空間は596から584です。ただし、配列の割り当ては継続しているため、配列の最初の要素は596ではなく584アドレスから始まります。


1

コンパイラーは、ローカルスタックフレームの任意の場所にローカル(自動)変数を自由に割り当てることができます。それだけでスタックの成長方向を確実に推測することはできません。ネストされたスタックフレームのアドレスを比較することから、つまり、関数のスタックフレーム内のローカル変数のアドレスをその呼び出し先と比較することから、スタックの成長方向を推測できます。

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}

5
異なるスタックオブジェクトへのポインターを減算することは未定義の動作であると確信しています。同じオブジェクトの一部ではないポインターは比較できません。明らかに、「通常の」アーキテクチャではクラッシュしません。
スティーブジェソップ

@SteveJessopこれを修正して、プログラムでスタックの方向を取得する方法はありますか?
xxks-KKK

@ xxks-kkk:Cの実装には「スタックの方向」が必要ないため、原則としていいえ。たとえば、スタックブロックが事前に割り当てられ、その後、疑似ランダム内部メモリ割り当てルーチンがその内部をジャンプするために使用される呼び出し規約があることは、標準に違反しません。実際には、matjaが説明するように実際に機能します。
スティーブジェソップ2018

0

私はそれがそのような決定論的ではないと思います。そのメモリは連続して割り当てられる必要があるため、配列は「成長」しているように見えます。ただし、qとsは互いにまったく関係がないため、コンパイラはそれぞれをスタック内の任意の空きメモリ位置(おそらく整数サイズに最適な位置)に固定します。

a [2]とqの間で起こったことは、qの位置の周りのスペースが、3整数配列を割り当てるのに十分な大きさではなかった(つまり、12バイトより大きくなかった)ということです。


もしそうなら、なぜq、s、aは偶発的な記憶を持っていないのですか?(例:qのアドレス:2293612 sのアドレス:2293608 aのアドレス:2293604)

sとaの間に「ギャップ」があります

sとaは一緒に割り当てられなかったため、連続している必要があるポインタは配列内のポインタだけです。他のメモリはどこにでも割り当てることができます。
javanix 2009年

0

私のスタックは、番号の小さいアドレスに向かって拡張しているようです。

別のコンピューターでは異なる場合があります。別のコンパイラー呼び出しを使用する場合は、自分のコンピューターでも異なる場合があります。...またはコンパイラmuigtは、スタックをまったく使用しないことを選択します(すべてをインライン化します(関数と変数のアドレスを取得しなかった場合))。

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./a.out
レベル4:xは0x7fff7781190cにあります
レベル3:xは0x7fff778118ecにあります
レベル2:xは0x7fff778118ccにあります
レベル1:xは0x7fff778118acにあります
レベル0:xは0x7fff7781188cにあります

0

スタックは大きくなります(x86上)。ただし、関数のロード時にスタックは1つのブロックに割り当てられるため、アイテムがスタック上でどのような順序になるかは保証されません。

この場合、スタック上の2つのintと3つのint配列にスペースを割り当てました。また、配列の後にさらに12バイトが割り当てられたため、次のようになります。

a [12バイト]
パディング(?)[12バイト]
s [4バイト]
q [4バイト]

何らかの理由で、コンパイラーは、この関数に32バイト、場合によってはそれ以上を割り当てる必要があると判断しました。それはCプログラマーとしてのあなたには不透明であり、その理由を知ることはできません。

理由を知りたい場合は、コードをアセンブリ言語にコンパイルしてください。gccでは-S、MSのCコンパイラでは/ Sだと思います。その関数の開始命令を見ると、古いスタックポインターが保存されてから、32(または他の何か!)が減算されていることがわかります。そこから、コードがその32バイトのメモリブロックにどのようにアクセスするかを確認し、コンパイラが何をしているかを把握できます。関数の最後で、スタックポインタが復元されているのを確認できます。


0

それはあなたのオペレーティングシステムとあなたのコンパイラに依存します。


私の答えが反対票を投じられた理由がわかりません。それは本当にあなたのOSとコンパイラに依存します。一部のシステムではスタックが下向きに成長しますが、他のシステムでは上向きに成長します。また、一部のシステムでは、実際のプッシュダウンフレームスタックはありませんが、メモリまたはレジスタセットの予約領域を使用してシミュレートされます。
David R Tribble

3
おそらく、単一文のアサーションは良い答えではないためです。
軌道上でのライトネスレース2013年

0

スタックは成長します。したがって、f(g(h()))の場合、hに割り当てられたスタックは、gよりも低いアドレスから始まり、gはfよりも低くなります。ただし、スタック内の変数はC仕様に従う必要があります。

http://c0x.coding-guidelines.com/6.5.8.html

1206ポイントされたオブジェクトが同じ集約オブジェクトのメンバーである場合、後で宣言された構造体メンバーへのポインターは、構造体で前に宣言されたメンバーへのポインターよりも大きく比較され、より大きな添え字値を持つ配列要素へのポインターは、同じ要素へのポインターよりも大きく比較されます添え字の値が低い配列。

&a [0] <&a [1]、「a」の割り当て方法に関係なく、常に真でなければなりません


ほとんどのマシンでは、スタックは上向きに成長するものを除いて、下向きに成長します。
ジョナサンレフラー

0

これは、メモリ内のデータセットに関しては、リトルエンディアンのバイトオーダー標準が原因です。

それを見ることができる1つの方法は、メモリを上から0から、下から最大まで見ると、スタックが上向きに成長することです。

スタックが下向きに成長する理由は、スタックまたはベースポインタの観点から逆参照できるようにするためです。

どのタイプの間接参照も、最小アドレスから最大アドレスへと増加することに注意してください。スタックは下に向かって(最高から最低のアドレスに)大きくなるため、これによりスタックを動的メモリのように扱うことができます。

これが、非常に多くのプログラミング言語とスクリプト言語がレジスタベースではなくスタックベースの仮想マシンを使用する理由の1つです。


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.非常に良い推論
user3405291 2017年

0

アーキテクチャによって異なります。独自のシステムを確認するには、GeeksForGeeksの次のコードを使用します。

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.