15

%rbpインライン asm 内でベース ポインタ レジスタ ( ) を使用できるようにしたいと考えています。このおもちゃの例は次のようになります。

void Foo(int &x)
{
    asm volatile ("pushq %%rbp;"         // 'prologue'
                  "movq %%rsp, %%rbp;"   // 'prologue'
                  "subq $12, %%rsp;"     // make room

                  "movl $5, -12(%%rbp);" // some asm instruction

                  "movq %%rbp, %%rsp;"  // 'epilogue'
                  "popq %%rbp;"         // 'epilogue'
                  : : : );
    x = 5;
}

int main() 
{
    int x;
    Foo(x);
    return 0;
}

古い をプッシュしてポップする通常のプロローグ/エピローグ関数呼び出し方法を使用しているので、これで問題ないことを願っていました%rbpxただし、インライン asm の後にアクセスしようとすると、seg fault が発生します。

GCC で生成されたアセンブリ コード (少し簡略化) は次のとおりです。

_Foo:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)

    # INLINEASM
    pushq %rbp;          // prologue
    movq %rsp, %rbp;     // prologue
    subq $12, %rsp;      // make room
    movl $5, -12(%rbp);  // some asm instruction
    movq %rbp, %rsp;     // epilogue
    popq %rbp;           // epilogue
    # /INLINEASM

    movq    -8(%rbp), %rax
    movl    $5, (%rax)      // x=5;
    popq    %rbp
    ret

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    leaq    -4(%rbp), %rax
    movq    %rax, %rdi
    call    _Foo
    movl    $0, %eax
    leave
    ret

このセグメントが失敗する理由を誰か教えてもらえますか? どういうわけか破損しているようです%rbpが、方法がわかりません。前もって感謝します。

64 ビットの Ubuntu 14.04 で GCC 4.8.4 を実行しています。

4

2 に答える 2

26

他の inline-asm Q&A へのリンクのコレクションについては、この回答の下部を参照してください。

pushGCCが値を保持していたRSPの下のレッドゾーン( )を踏んだため、コードが壊れています。


インライン asm で達成するために何を学びたいですか? インライン asm を学びたい場合は、このようなひどいものではなく、それを使用して効率的なコードを作成する方法を学んでください。関数のプロローグとプッシュ/ポップを記述してレジスタを保存/復元する場合は、関数全体を asm に記述する必要があります。(そうすれば、あまり好まれない AT&T 構文を GNU アセンブラー ディレクティブで使用するのではなく、nasm または yasm を簡単に使用できます1。)

GNU インライン asm は使いにくいですが、カスタム asm フラグメントを C および C++ に混在させることができ、必要に応じてレジスタ割り当てと保存/復元をコンパイラに処理させることができます。場合によっては、コンパイラーは、上書きが許可されているレジスターを提供することで、保存と復元を回避できる場合があります。がなければvolatile、入力が同じであっても、ループから asm ステートメントを巻き上げることができます。(つまり、 を使用しない限りvolatile、出力は入力の「純粋な」関数であると見なされます。)

そもそも asm を学ぼうとしているだけなら、GNU インライン asm はひどい選択です。 asm で行われていることのほとんどすべてを完全に理解し、コンパイラが知る必要があることを理解して、正しい入出力制約を記述し、すべてを正しく処理する必要があります。間違いは、物を壊したり、デバッグが困難な破損につながります。関数呼び出し ABI は、コードとコンパイラのコードの間の境界を追跡するのがはるかに単純で簡単です。


これが壊れる理由

でコンパイルし-O0たため、gcc のコードは関数パラメーターを%rdiスタック上の場所にスピルします。(これは、 であっても自明でない関数で発生する可能性があります-O3)。

ターゲット ABI はx86-64 SysV ABIであるため、スペースを予約するためにスタック ポインターをデクリメントする命令を無駄にする代わりに、「レッド ゾーン」%rsp (非同期シグナル ハンドラーでさえクロバーが許可されていない128 バイトより下) を使用します。

8B ポインタ関数 arg を に格納し-8(rsp_at_function_entry)ます。次に、インライン asmが push します。%rbpこれにより、%rsp が 8 減分され、そこに書き込まれ、&x(ポインター) の下位 32b が上書きされます。

インライン asm が完了したら、

  • gcc-8(%rbp)は (で上書きされている%rbp) をリロードし、それを 4B ストアのアドレスとして使用します。
  • Foomainwith に戻ります%rbp = (upper32)|5 (low 32 が に設定された元の値5)。
  • main走るleave%rsp = (upper32)|5
  • mainで実行retされ、コメントからの%rsp = (upper32)|5仮想アドレスからリターンアドレスを読み取ります。(void*)(upper32|5)0x7fff0000000d

デバッガーで確認しませんでした。これらのステップの 1 つが少しずれている可能性がありますが、問題は間違いなく、レッド ゾーンを壊してしまい、 gcc のコードがスタックを破壊することです。

「メモリ」クロバーを追加しても、gcc はレッド ゾーンの使用を回避できません。そのため、インライン asm から独自のスタック メモリを割り当てるのは悪い考えのようです。(メモリ クロバーとは、書き込みが許可されているメモリ、たとえば、グローバル変数またはグローバルによってポイントされる何かを書き込んだ可能性があることを意味し、想定されていないものを上書きした可能性があることを意味します。)

インライン asm からスクラッチ スペースを使用する場合は、おそらく配列をローカル変数として宣言し、それを出力専用オペランド (決して読み取らない) として使用する必要があります。

私の知る限り、レッドゾーンを変更することを宣言するための構文はないため、唯一のオプションは次のとおりです。

  • "=m"スクラッチ スペースに出力オペランド (おそらく配列) を使用します。コンパイラはおそらく、そのオペランドに RBP または RSP に相対的なアドレッシング モードを入力します。などの定数を使用してインデックスを作成できます4 + %[tmp]。アセンブラーの警告4 + (%rsp)が表示される場合がありますが、エラーにはなりません。
  • コードの周りのadd $-128, %rsp/でレッドゾーンをスキップします。sub $-128, %rsp(ループ内でのプッシュや関数呼び出しなど、不明な量の余分なスタック スペースを使用する場合に必要です。インライン asm ではなく、純粋な C で関数ポインターを逆参照するもう 1 つの理由です。)
  • でコンパイルします-mno-red-zone(関数ごとに有効にすることはできず、ファイルごとにのみ有効にできると思います)
  • そもそもスクラッチ スペースを使用しないでください。どのレジスターを上書きするかをコンパイラーに伝え、それらを保存させます。

これがあなたがすべきことです

void Bar(int &x)
{
    int tmp;
    long tmplong;
    asm ("lea  -16 + %[mem1], %%rbp\n\t"
         "imul $10, %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
         "add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
         "movl $5, %[mem1]\n\t" // some asm instruction writing to mem
           : [mem1] "=m" (tmp), [reg1] "=r" (tmplong)  // tmp vars -> tmp regs / mem for use inside asm
           :
           : "%rbp" // tell compiler it needs to save/restore %rbp.
  // gcc refuses to let you clobber %rbp with -fno-omit-frame-pointer (the default at -O0)
  // clang lets you, but memory operands still use an offset from %rbp, which will crash!
  // gcc memory operands still reference %rsp, so don't modify it.  Declaring a clobber on %rsp does nothing
         );
    x = 5;
}

/セクション%rbpの外側のコードで、gcc によって発行されたプッシュ/ポップに注意してください。また、それが与えるスクラッチ メモリはレッド ゾーンにあることに注意してください。でコンパイルすると、こぼれた場所とは異なる位置にあることがわかります。#APP#NO_APP-O0&x

より多くのスクラッチ reg を取得するには、周囲の非 asm コードで決して使用されない出力オペランドを宣言することをお勧めします。これにより、レジスタの割り当てはコンパイラに任せられるため、別の場所にインライン化すると異なる可能性があります。事前に選択して clobber を宣言することは、特定のレジスタ (例: のシフト カウント%cl) を使用する必要がある場合にのみ意味があります。もちろん、"c" (count)gets gcc のような入力制約により、カウントが rcx/ecx/cx/cl に格納されるため、潜在的に冗長なmov %[count], %%ecx.

これが複雑すぎると思われる場合は、 inline asm を使用しないでください。最適な asm のような C で必要な asm にコンパイラを導くか、asm で関数全体を記述します。

インライン asm を使用する場合は、可能な限り小さくしてください。理想的には、gcc が独自に発行しない 1 つまたは 2 つの命令だけで、asm ステートメントにデータを出し入れする方法を指示するための入出力制約があります。これが設計されたものです。

経験則: GNU C インライン asm が で開始または終了するmov場合、通常は間違っているため、代わりに制約を使用する必要がありました。


脚注:

  1. inline-asm で GAS の intel-syntax を使用するには-masm=intel(この場合、コードはそのオプションでのみ動作します)、または方言の代替を使用して、Intel または AT&T asm 出力構文のコンパイラーで動作します。しかし、それはディレクティブを変更しません.GASのIntel構文は十分に文書化されていません. (ただし、NASM ではなく、MASM のようなものです。) AT&T 構文が本当に嫌いでない限り、あまりお勧めしません。

インライン asm リンク:

それらのいくつかは、ここで説明したのと同じことを繰り返しています。冗長性を避けるためにそれらを再読しませんでした、申し訳ありません。

于 2015-12-30T04:06:57.607 に答える
3

x86-64 では、スタック ポインターを 8 バイトに揃える必要があります。

これ:

subq $12, %rsp;      // make room

次のようにする必要があります。

subq $16, %rsp;      // make room
于 2015-12-29T22:30:04.963 に答える