web-dev-qa-db-ja.com

x86では、スタック上の値のメモリアドレスはどこにありますか?

これを行う非常に単純なCプログラムがあるとします。

int i = 6;
int j = 4;
int k = 5;
int a = i + j + k;

ij、およびkはスタック上にあるため、スタックポインターに対して相対的に配置されます。コンパイラがこれらの相対的な場所を決定すると言われています。私の推測では、コンパイラがint a = i + j + kに「(スタックポインタ-3x)、(スタックポインタ-2x)、(スタックポインタ-x)にある値を追加し、結果をスタックにプッシュします。」私は正しいですか?

4
moonman239

仕様ではこれをコンパイラの実装者に任せているため、Cコンパイラに依存します。 (ただし、パラメーターのアドレスを取得する場合など、特定の要件があるため、コンパイラーの実装者をさらに制約する可能性があります。)

典型的なC関数は、「プロローグ」コード、「本体」コード、および「エピローグ」コードを生成します。プロローグはローカル変数スペースを割り当てます。一般に、コンパイラーは概説したプロセスを実行しません。それでは、16ビットx86コードとCのこの関数について話しているとしましょう。

int f( void ) {
    int i = 6;
    int j = 4;
    int k = 5;
    int a = i + j + k;
    return a;
}

前述のコンテキストでは、変数のサイズが2バイトになることに注意してください。コンパイラーは、すべてのローカル変数に必要なバイト数をカウントします。この場合、4つあり、それぞれに2バイトが必要なので、コンパイラーはこれらすべてに8バイトが必要であることを「計算」します。 (また、上記のC関数内の追加のコードブロック内に変数定義を含めた場合でも、CコンパイラはそれらすべてをカウントしますAT ONCE。これは、したがって、コンパイラはアセンブリで次のプロローグを生成する可能性があります。

Push bp
mov bp, sp
sub sp, 8

通常、BPレジスタは、関数の現在のコンテキストの「フレームポインター」として使用されます。 BPは、「アクティベーションフレーム」へのポインタとしても知られています。したがって、Cコンパイラは、古いアクティベーションフレームポインタをスタックに保存し、この関数を呼び出すために現在のポインタを指すように初期化するために、2つの命令を必要とします。

3番目の命令は単純で、すべてのローカル変数に必要な8バイトに必要なすべてのスペースを割り当てます。スタックポインタを必要な8バイトの上に移動するだけです。 BPは引き続きこのアクティベーションフレームのベースを指します。しかし、SPはその反対側にあるため、追加のプッシュと呼び出しによってローカル変数が上書きされることはありません。

Cコンパイラーは、各ローカル変数にオフセット値を内部的に割り当てます。おそらくIVAR = 0、JVAR = 2、KVAR = 4、およびAVAR = 6のようなものです。ここで、CコンパイラはいくつかのBODYコードを作成する必要があります。これらのローカル変数値を設定するため、次のようなもの(完全に最適化されていない):

mov IVAR[BP], 6
mov JVAR[BP], 4
mov KVAR[BP], 5
mov ax, IVAR[BP]
add ax, JVAR[BP]
add ax, KVAR[BP]

この16ビットのコンテキストでは、関数の戻り値がAXに収まる場合、AXに配置されることも一般的であることに注意してください。この場合はそうです。ですから、現時点では答えは正しい場所にあります。だから今エピローグが必要です:

mov sp, bp
pop bp
ret

以上です。

さて、オプティマイザはこのケースで上記のコードを改善するために素晴らしい取引を行います。ローカル変数はまったく必要ないため、おそらくすべてのローカル変数が完全に削除されます。結果全体を15として事前計算し、次の操作を実行できます。

mov ax, 15
ret

アクティベーションフレームを管理する必要はまったくありません。値を返すだけです。 (しかし、ローカル変数がどのように管理されるかについては、まったくわかりません。)

少しお役に立てば幸いです。

8
jonk

はい。それで合っています。コンパイラは実際には正確なメモリ位置を決定するのではなく、スタックポインタからのこれらの位置の相対オフセットを決定するだけです。実行時にスタックポインタが持つ値は、コンパイル時にコンパイラに認識されません。

(実際には、16ビットおよび32ビットのIntelアーキテクチャでは、これらの場所は、いわゆる「ベースポインタ」(「フレームポインタ」と呼ばれる他のアーキテクチャではレジスタBP)を基準にしています。スタックポインターから決定されますが、それは重要ではありません。64ビットで修正され、スタックポインターのみが必要になりました。)

2
Mike Nakis

I、j、およびkはスタック上にあるため、スタックポインターに対して相対的に配置されます。

まず第一に、このステートメントは現代のコンパイラの一般的なケースでは明らかに間違っています。スタック上の位置を割り当て、変数をそれらにマップし、明示的に要求された場合にのみレジスターを使用する(registerキーワードで)または一時的な値。 (MS-DOSと16ビットコンパイラで宿題はありますか?)これは非常に古い方法です。

最新のコンパイラー(GCC、Clang/LLVMなど)は [〜#〜] ssa [〜#〜] とそれに付随する手法を利用しています。 SSAでは、内部的に「変数」はありません。ほぼ変更できない値(実行パスのマージポイントを除く)があり、それらはそれぞれ、使用頻度と使用頻度に応じて、レジスタまたはスタックのいずれかで独自の場所を取得できます。いくつかの変数に「int i」という名前を付けた場合、レジスタープールからスワップアウトされたときにスタックで、関数の一部でEAXに、別の場所でEDIになる可能性があります...

コンパイラーが別の変数または定数を使用する方が簡単であると想定している場合、変数がまったく最適化されないことがあります。時々存在しますが、変更されます(たとえば、配列にx [i-1]としてアクセスする場合、iによって1からNに循環しますが、0からN-1に循環し、x [としてアクセスします。私])。などなど、最適化リストはほぼ無限です。

変数は、そのアドレスを取得した場合にのみ、スタック上に配置されていることを確認できます。それでも、このアドレスが有効な場合のみ、可変アドレスになります。別の関数をg(&i)として呼び出す場合、igが呼び出される前にスタックに配置されますが、g()終了しました。このアドレスは二度と使用されないためです。

例。 C関数は次のとおりです。

_int f(int i, int j, int k) {
  return i + j + k;
}
_

X86_64/Unix用に翻訳されています。

_    addl    %esi, %edi
    leal    (%rdi,%rdx), %eax
    ret
_

スタックはまったく使用されません。呼び出し規約は、引数と結果を配置するためのレジスタを指定し、それが実装されています。 (同じ追加がaddの次にleaであることに注意してください-これは、異なる実行ユニット間の命令フローにまたがっています。)

iの外部アクションを追加します。

_void g(int*);
int f(int i, int j, int k) {
  g(&i);
  return i + j + k;
}
_

アセンブリ出力:

_    pushq   %rbp
    movl    %esi, %ebp
    pushq   %rbx
    movl    %edx, %ebx
    subq    $24, %rsp
    movl    %edi, 12(%rsp) ; <-- storing i onto stack
    leaq    12(%rsp), %rdi ; <-- getting &i
    call    g
    addl    12(%rsp), %ebp ; <-- using possibly modified i
    addq    $24, %rsp
    leal    0(%rbp,%rbx), %eax
    popq    %rbx
    popq    %rbp
    ret
_

ここで、iはRDIで提供され、次にg(&i)の前にスタックに配置され、レジスタに戻ることなく、スタックから直接被加数として使用されます。 (また、これはリーフ関数ではなくなったため、フレームポインターが調整されます。)

タイムマシンや非常に組み込みの環境を利用していることを再度検討してください。 SSAは高価な手法です。変数セットのスタック(または以前のメモリ)へのマッピングは、コンパイラーが最初から、つまり初期のFortranバージョンから機能していた方法です。その場合、あなたは正しいです。そして、その場合の問題は、可能なアドレス変更を追跡する方法です。

S/360では、コンパイラは変数の関数本体の後にメモリチャンクを使用し、次の命令シーケンスを使用してこのチャンクのベースアドレスを取得します。

_    BALR 15,0
    USING *,15
_

レジスター15は、この目的のための従来のレジスターであり、その後、コマンドの直後にアドレスを取得します。次に、オフセットベースのメモリアクセスが次のような変数に使用されます。

_    LR 3,196(15)
_

ここで、このベースと196からの4バイト値がレジスター3にロードされます。関数の実行全体では、iは196(15)になります。

このアプローチはスタックベースではないことに注意してください。関数は再入可能ではありません。

PDP-11では、スタックベースのFortranではありませんが、これは次のように命令ポインタベースのアクセスで実行できます。

_    MOV 196(PC), R3
_

そのようなメモリアクセスのためにレジスタを割り当てる必要がないためです。ただし、オフセットは異なります(次の命令では、196ではなく192を使用する必要があります)。

スタックでは、これはSPベースのアクセスに変更され、関数本体の近くに変数領域はありません。

_    MOV 16(SP), R3
_

ただし、SPが変更された場合のオフセット変更については、以下を参照してください。

I386より前のx86では、BPベースのスタックアクセスが必須でした。 i386以降、まだ使用可能ですが、一意ではありません。関数プロローグでのBPセットアップ後、オフセットは固定されたままなので、[EBP-20](Intel)、-20(%ebp)(AT&T)などの同じ式を関数呼び出しの存続期間中の変数に使用できます。 ESP/RSPベースのアクセス(i386以降)では、プッシュ/ポップでオフセットが変更されます。 iが[ESP + 20]の場合、単一の4バイトプッシュで[ESP + 24]に「変換」します。 「変換」とは、メモリアドレスが同じであることを意味しますが、その式はESPの調整によって変更されます。たとえば、g(i,&i,&i)が呼び出された場合、コンパイラーは次のシーケンスを生成して引数リストを形成します(x86_32呼び出し規約は主にスタックベースです)。

_## assume i is now at 24(%esp)
    leal     24(%esp), %eax ; &i to eax
    pushl    %eax
    leal     28(%esp), %eax ; &i to eax
    pushl    %eax
    movl     32(%esp), %eax ; i to eax
    pushl    %eax
_

(それは明らかに次善ですが、表現しています。)

2
Netch