私はこのコードを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つのことを疑問に思います:
a[2]
とq
のメモリアドレスの間で何が起こりますか?なぜそこに大きなメモリの違いがあるのですか? (20バイト)。注:これは宿題の質問ではありません。スタックの仕組みに興味があります。助けてくれてありがとう。
スタックの動作(成長または成長)は、アプリケーションバイナリインターフェイス(ABI)およびコールスタック(アクティベーションレコード)の編成方法によって異なります。
プログラムはその存続期間を通じて、OSなどの他のプログラムと通信する必要があります。 ABIは、プログラムが別のプログラムと通信する方法を決定します。
異なるアーキテクチャのスタックはどちらの方法でも成長できますが、アーキテクチャの場合は一貫性があります。 this 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」のいずれかです。しかし、一般的にコンパイラは変数の宣言の順序に従ってスタックメモリの割り当てを行います)。
しかし、配列の場合、割り当てには単一のポインターしかなく、割り当てられる必要があるメモリは実際には単一のポインターによってポイントされます。メモリは配列に対して連続している必要があります。そのため、スタックは下向きに成長しますが、アレイの場合はスタックが成長します。
これは実際には2つの質問です。 1つはどちらの方法 1つの関数が別の関数を呼び出すとスタックが大きくなる (新しいフレームが割り当てられると)、もう1つは特定の関数のフレームで変数がどのようにレイアウトされるかです。
どちらもC標準では指定されていませんが、答えは少し異なります。
f
のフレームポインタが大きくなりますまたはg
のフレームポインター未満ですか?これはどちらの方法でも可能です-特定のコンパイラおよびアーキテクチャに依存します(「呼び出し規約」を参照)、しかし常に一貫性があります所定のプラットフォーム内で(いくつかの奇妙な例外はありますが、コメントを参照してください。)下向きがより一般的です; x86、PowerPC、MIPS、SPARC、EE、およびCell SPUの場合です。スタックが成長する方向は、アーキテクチャ固有です。そうは言っても、私の理解では、成長するスタックを持つハードウェアアーキテクチャはごく少数です。
スタックが成長する方向は、個々のオブジェクトのレイアウトとは無関係です。そのため、スタックが成長しても、配列は成長しません(つまり、&array [n]は常に<&array [n + 1]になります)。
標準には、スタック上で物事をどのように編成するかを義務付けるものは何もありません。実際、スタック上の連続する要素に配列要素をまったく格納しない適合コンパイラを構築できます。ただし、配列要素の計算を適切に実行するスマートがあれば(たとえば、a-- 1 はa [0]から1K離れており、そのために調整できました)。
異なる結果が得られる理由は、スタックが下降して「オブジェクト」を追加する一方で、配列が単一の「オブジェクト」であり、逆の順序で昇順の配列要素を持つ可能性があるためです。ただし、次のようなさまざまな理由で方向が変わる可能性があり、変数が交換される可能性があるため、その動作に依存するのは安全ではありません。
here を参照して、スタック方向に関する優れた論文をご覧ください:-)
特定の質問に答えて:
X86では、スタックフレームのメモリ「割り当て」は、必要なバイト数をスタックポインタから減算するだけです(他のアーキテクチャも同様だと思います)。この意味で、スタックを「ダウン」させ、スタックを深く呼び出すとアドレスが次第に小さくなっていくと思います(ただし、メモリは左上が0で始まり、移動するとアドレスが大きくなると常に想定しています)右に移動してラップダウンするので、私のイメージではスタックが大きくなります...)。宣言されている変数の順序は、アドレスに関係がない場合があります-副作用を引き起こさない限り、標準ではコンパイラが変数を並べ替えることができます(間違っている場合は誰かが私を修正してください) 。それらは、スタックポインタからバイト数を減算するときに作成される使用済みアドレスのギャップのどこかに詰まっています。
配列の周りの隙間は何らかのパディングであるかもしれませんが、私には不思議です。
まず、メモリ内の8バイトの未使用スペース(12ではなく、スタックが下向きに成長するため、割り当てられていないスペースは604から597です)。なぜ?。すべてのデータ型は、サイズで割り切れるアドレスから始まるメモリ領域を使用するためです。私たちの場合、3つの整数の配列は12バイトのメモリ空間を取り、604は12で割り切れません。したがって、12で割り切れるメモリアドレスが見つかるまで空のスペースを残します。これは596です。
したがって、配列に割り当てられるメモリ空間は596〜584です。しかし、配列の割り当てが継続しているため、配列の最初の要素は596ではなく584アドレスから始まります。
コンパイラは、ローカルスタックフレームの任意の場所にローカル(自動)変数を自由に割り当てることができます。それだけでは、スタックの成長方向を確実に推測することはできません。ネストされたスタックフレームのアドレスを比較することで、スタックの成長方向を推測できます。つまり、関数のスタックフレーム内のローカル変数のアドレスを呼び出し先と比較します。
#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;
}
それはアーキテクチャに依存します。独自のシステムを確認するには、 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;
}
これは、メモリ内のデータのセットに関しては、リトルエンディアンのバイト順標準のためです。
あなたがそれを見ることができる1つの方法は、上からメモリを0から見て、下からメモリを見ると、スタックが上向きに成長することです。
スタックが下向きに成長する理由は、スタックまたはベースポインタの観点から逆参照できるようにするためです。
任意のタイプの逆参照は、最下位アドレスから最上位アドレスに増加することに注意してください。スタックは下に向かって(最高から最低のアドレスに)成長するため、これによりスタックを動的メモリのように扱うことができます。
これが、非常に多くのプログラミングおよびスクリプト言語がレジスタベースではなくスタックベースの仮想マシンを使用する理由の1つです。
私はそれがそのような決定論的だとは思わない。そのメモリは連続して割り当てられる必要があるため、配列は「成長」しているようです。ただし、qとsはまったく関連していないため、コンパイラはそれぞれをスタック内の任意の空きメモリ位置に固定します。これはおそらく整数サイズに最も適した場所です。
A [2]とqの間で起こったことは、qの位置の周りのスペースが3整数配列を割り当てるのに十分な大きさではなかった(つまり、12バイトより大きくなかった)ことです。
オペレーティングシステムとコンパイラによって異なります。
私のスタックは、小さい番号のアドレスに向かって伸びているようです。
別のコンピューター、または別のコンパイラー呼び出しを使用する場合は自分のコンピューターでも異なる場合があります。 ...または、コンパイラ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 level 4:xは0x7fff7781190c level 3:にあります。xは0x7fff778118ec level 2:にあります。xは0x7fff778118cc level 1:にあります。 xは0x7fff778118ac level 0にあります:xは0x7fff7781188c にあります
スタックは大きくなります。したがって、f(g(h()))、hに割り当てられたスタックは、gの下のアドレスから始まり、gの値はfの値よりも低くなります。ただし、スタック内の変数はC仕様に従う必要があります。
http://c0x.coding-guidelines.com/6.5.8.html
1206指し示されているオブジェクトが同じ集合オブジェクトのメンバーである場合、後で宣言される構造体メンバーへのポインターは、構造体で以前に宣言されたメンバーへのポインターよりも大きく比較し、より大きな添字値を持つ配列要素へのポインターは、同じ要素へのポインターよりも大きい添え字の値が小さい配列。
&a [0] <&a [1]、 'a'の割り当て方法に関係なく、常にtrueでなければなりません