GNU/Linux >> Linux の 問題 >  >> Linux

このインライン アセンブリが、命令ごとに個別の asm volatile ステートメントで機能しないのはなぜですか?

メモリを破壊しますが、GCC に通知しないため、GCC は値を buf にキャッシュできます アセンブリ呼び出し間。入力と出力を使用したい場合は、GCC にすべてを伝えてください。

__asm__ (
    "movq %1, 0(%0)\n\t"
    "movq %2, 8(%0)"
    :                                /* Outputs (none) */
    : "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
    : "memory");                     /* Clobbered */

また、通常、GCC に mov のほとんどを処理させたいと考えています。 、レジスタの選択など - 明示的にレジスタを制約しても (rrax はまだ %rax です) ) 情報が GCC を通過するようにしないと、予期しない結果が得られます。

__volatile__

理由 __volatile__ 存在するのは、コンパイラがコードをその場所に正確に配置することを保証できるようにするためです...これは完全に不要です このコードの保証。メモリ バリアなどの高度な機能を実装するために必要ですが、メモリとレジスタのみを変更する場合はほとんど意味がありません。

GCC は、printf 以降はこのアセンブリを移動できないことを既に認識しています。 なぜなら printf 呼び出しは buf にアクセスします 、および buf アセンブリによって破壊される可能性があります。 GCC は、rrax=0x39; より前にアセンブリを移動できないことを既に認識しています。 なぜなら rax アセンブリ コードへの入力です。 __volatile__ とは 分かりますか?

__volatile__ がないとコードが機能しない場合 修正する必要があるコードにエラーがあります __volatile__ を追加する代わりに そしてそれがすべてをより良くすることを願っています。 __volatile__ キーワードは魔法ではないため、そのように扱うべきではありません。

別の修正:

__volatile__ です 元のコードに必要ですか?いいえ。入力と clobber の値を正しくマークするだけです。

/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
   The inputs and clobbered values are specified.  There is no output
   so that section is blank.  */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");

__volatile__ の理由 ここでは役に立ちません:

rrax = 0x34; /* Dead code */

上記の質問のコードは rrax を使用しないと主張しているため、GCC には上記の行を完全に削除する権利があります。 .

わかりやすい例

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)");
}

-O0 での逆アセンブリは、多かれ少なかれ予想どおりです。 、

movl $5, %rax
movq %rax, (global)

しかし、最適化をオフにすると、アセンブリに関してかなりずさんになる可能性があります。 -O2 を試してみましょう :

movq %rax, (global)

おっと! rax = 5; はどこに行った 行く? %rax 以来、デッド コードです。 関数で使用されることはありません — 少なくとも GCC が知る限り。 GCC はアセンブリ内を覗きません。 __volatile__ を削除するとどうなるか ?

; empty

__volatile__ と思うかもしれません。 GCC が貴重なアセンブリを破棄しないようにすることでサービスを提供していますが、GCC があなたのアセンブリが行っていないと考えているという事実を隠しているだけです。 なんでも。 GCC は、アセンブリが入力を受け取らず、出力を生成せず、メモリを破壊しないと見なします。あなたはそれをまっすぐにしたほうがいいです:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}

これで、次の出力が得られます:

movq %rax, (global)

より良い。しかし、入力について GCC に伝えると、%rax が確実に行われます。 最初に適切に初期化されます:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}

最適化された出力:

movl $5, %eax
movq %rax, (global)

正しい! __volatile__ を使用する必要さえありません。 .

なぜ __volatile__ は 存在しますか?

__volatile__ の主な正しい使い方 アセンブリコードが入力、出力、またはメモリの破壊以外の何かを行う場合です。おそらく、GCC が認識していない、または IO に影響を与える特殊なレジスタをいじっています。 Linux カーネルではよく見かけますが、ユーザー空間ではよく誤用されています。

__volatile__ キーワードは非常に魅力的です。なぜなら、私たち C プログラマーは、ほぼ アセンブリ言語でのプログラミングはすでに完了しています。そうではなかった。 C コンパイラは多くのデータ フロー分析を行うため、アセンブリ コードのデータ フローをコンパイラに説明する必要があります。そうすれば、コンパイラは、生成したアセンブリを操作するのと同じように、アセンブリのチャンクを安全に操作できます。

__volatile__ を使用している場合 代わりに、関数またはモジュール全体をアセンブリ ファイルに記述することもできます。


コンパイラはレジスタを使用し、レジスタに入力した値を上書きする場合があります。

この場合、コンパイラはおそらく rbx を使用します。 rrbx の後に登録します 割り当てとインライン アセンブリ セクションの前。

一般に、レジスタがインライン アセンブリ コード シーケンスの前後で値を保持するとは考えないでください。


少し本題から外れますが、gcc インライン アセンブリについて少し補足したいと思います。

__volatile__ の (非) 必要性 GCC が最適化するという事実から来ています。 インラインアセンブリ。 GCC はアセンブリ ステートメントの副作用/前提条件を検査し、それらが存在しないことが判明した場合は、アセンブリ命令を移動するか、削除 することさえ選択できます。 それ。すべて __volatile__

これは通常、あなたが本当に望んでいるものではありません.

ここで制約が必要になります 名前はオーバーロードされ、実際には GCC インライン アセンブリのさまざまな目的で使用されます:

  • 制約は、asm() で使用される入力/出力オペランドを指定します ブロック
  • 制約は、asm() によって影響を受ける「状態」(レジスタ、条件コード、メモリ) を詳述する「破壊リスト」を指定します。 .
  • 制約は、オペランドのクラス (レジスタ、アドレス、オフセット、定数など) を指定します
  • 制約は、アセンブラ エンティティと C/C++ 変数 / 式の間の関連付け / バインディングを宣言します

多くの場合、開発者は悪用 __volatile__ 彼らは、自分たちのコードが動かされたり、コードなしで消えたりしていることに気付いたからです。これが発生した場合、それは通常、開発者が試みしなかったことを示しています。 アセンブリの副作用/前提条件についてGCCに伝えます。たとえば、このバグのあるコード:

register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;

asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);

いくつかのバグがあります:

  • 1 つは、gcc のバグ (!) が原因でコンパイルされるだけです。通常、インライン アセンブルでレジスタ名を記述するには、%% を 2 倍にします。 が必要ですが、上記で実際に指定すると、コンパイラ/アセンブラ エラー /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax' が発生します。 .
  • 第二に、いつ、どこで変数を必要/使用するかをコンパイラに伝えていません。代わりに、想定 コンパイラは asm() を尊重します 文字通り。 Microsoft Visual C++ ではそうかもしれませんが、そうではありません gcc

なしでコンパイルした場合 最適化、作成:

0000000000400524 <main>:
[ ... ]
  400534:       b8 d2 04 00 00          mov    $0x4d2,%eax
  400539:       bb e1 10 00 00          mov    $0x10e1,%ebx
  40053e:       48 01 c3                add    %rax,%rbx
  400541:       48 89 da                mov    %rbx,%rdx
  400544:       b8 5c 06 40 00          mov    $0x40065c,%eax
  400549:       48 89 d6                mov    %rdx,%rsi
  40054c:       48 89 c7                mov    %rax,%rdi
  40054f:       b8 00 00 00 00          mov    $0x0,%eax
  400554:       e8 d7 fe ff ff          callq  400430 <[email protected]>
[...]
add を見つけることができます 命令、および2つのレジスタの初期化が行われ、期待どおりに出力されます。一方、最適化を上げた場合、別のことが起こります:
0000000000400530 <main>:
  400530:       48 83 ec 08             sub    $0x8,%rsp
  400534:       48 01 c3                add    %rax,%rbx
  400537:       be e1 10 00 00          mov    $0x10e1,%esi
  40053c:       bf 3c 06 40 00          mov    $0x40063c,%edi
  400541:       31 c0                   xor    %eax,%eax
  400543:       e8 e8 fe ff ff          callq  400430 <[email protected]>
[ ... ]
両方の「使用済み」レジスタの初期化はもうありません。コンパイラはそれらを使用していないことがわかり、それらを破棄し、アセンブリ命令を保持しながらに配置しました 2 つの変数の使用。そこにありますが、何もしません (幸いなことに、実際には ... rax の場合 / rbx 使用されていた 何が起こったのか誰が知ることができますか...)。

その理由は、あなたが実際に言っていないからです アセンブリがこれらのレジスタ / これらのオペランド値を使用していることを示す GCC。 これは volatile とは何の関係もありません しかし、制約のない asm() を使用しているという事実はすべて

これを行う方法正しく 制約によるものです。つまり、次を使用します:

int foo = 1234;
int bar = 4321;

asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);

これにより、アセンブリが次のことをコンパイラに伝えます:

<オール>
  • レジスタに 1 つの引数 "+r"(...) があります アセンブリ ステートメントの前に初期化する必要があり、アセンブリ ステートメントによって変更され、変数 bar を関連付けます。
  • レジスタに 2 番目の引数 "r"(...) があります アセンブリステートメントの前に初期化する必要があり、読み取り専用として扱われ、ステートメントによって変更されません。ここで、foo を関連付けます
  • レジスタの割り当てが指定されていないことに注意してください。コンパイラは、コンパイルの変数/状態に応じてそれを選択します。上記の (最適化された) 出力:

    0000000000400530 <main>:
      400530:       48 83 ec 08             sub    $0x8,%rsp
      400534:       b8 d2 04 00 00          mov    $0x4d2,%eax
      400539:       be e1 10 00 00          mov    $0x10e1,%esi
      40053e:       bf 4c 06 40 00          mov    $0x40064c,%edi
      400543:       01 c6                   add    %eax,%esi
      400545:       31 c0                   xor    %eax,%eax
      400547:       e8 e4 fe ff ff          callq  400430 <[email protected]>
    [ ... ]
    GCC インライン アセンブリの制約はほぼ常に必要 ただし、同じ要件をコンパイラーに記述する方法は複数あります。上記の代わりに、次のように書くこともできます。

    asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
    

    これは gcc に次のように伝えます:

    <オール>
  • ステートメントには出力オペランド、変数 bar があります 、ステートメントがレジスター "=r"(...) で見つかった後
  • ステートメントには入力オペランド、変数 foo があります 、レジスタ "r"(...) に配置されます
  • オペランド 0 も入力オペランドであり、bar で初期化されます
  • または、別の方法:

    asm("add %1, %0" : "+r"(bar) : "g"(foo));
    

    これは gcc に次のように伝えます:

    <オール>
  • ブラ (あくび - 前と同じ bar 入力と出力の両方)
  • ステートメントには入力オペランド、変数 foo があります 、ステートメントは、レジスタ、メモリ、またはコンパイル時の定数 ("g"(...) です) にあるかどうかを気にしません 制約)
  • 結果は前者とは異なります:

    0000000000400530 <main>:
      400530:       48 83 ec 08             sub    $0x8,%rsp
      400534:       bf 4c 06 40 00          mov    $0x40064c,%edi
      400539:       31 c0                   xor    %eax,%eax
      40053b:       be e1 10 00 00          mov    $0x10e1,%esi
      400540:       81 c6 d2 04 00 00       add    $0x4d2,%esi
      400546:       e8 e5 fe ff ff          callq  400430 <[email protected]>
    [ ... ]
    なぜなら今、GCC は 実際に理解した foo はコンパイル時の定数で、 add 指示 !それはきちんとしていませんか?

    確かに、これは複雑で、慣れが必要です。利点は、コンパイラに選択させることです コード全体を最適化できるオペランドにどのレジスタを使用するか。たとえば、マクロおよび/または static inline でインライン アセンブリ ステートメントが使用されている場合 関数の場合、コンパイラは、呼び出しコンテキストに応じて、コードのさまざまなインスタンス化でさまざまなレジスタを選択できます。または、特定の値がコンパイル時に評価可能/一定であるが別の場所ではそうでない場合、コンパイラは作成されたアセンブリをそれに合わせて調整できます。

    GCC インライン アセンブリ制約は、一種の「拡張関数プロトタイプ」と考えてください。これらは、コンパイラに、引数/戻り値の型と場所、およびその他の情報を伝えます。これらの制約を指定しない場合、インライン アセンブリは、グローバル変数/状態のみで動作する関数の類似物を作成しています。これは、おそらく誰もが同意するように、意図したとおりに動作することはめったにありません。


    Linux
    1. 一部のコマンドでBashプロセス置換が機能しないのはなぜですか?

    2. なぜBashはスペースで始まるコマンドを保存しないのですか?

    3. Linux – Uefi / biosで動作するのにUsbがLinuxで動作しないのはなぜですか?

    1. 「ls」を実行するために別のプロセスが必要なのはなぜですか?

    2. &を使用してバックグラウンドで実行すると、Python スクリプトの Nohup が機能しない

    3. /tmp 用に別のパーティションを作成する必要があるのはなぜですか?

    1. Linux で SOCK_SEQPACKET の MSG_EOR が表示されないのはなぜですか?

    2. PYTHONPATH が GNU/Linux の sudo で機能しない (root で機能)

    3. Tomcat がポート 8080 で動作するのに 80 で動作しないのはなぜですか?