メモリを破壊しますが、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);
これにより、アセンブリが次のことをコンパイラに伝えます:
<オール>"+r"(...)
があります アセンブリ ステートメントの前に初期化する必要があり、アセンブリ ステートメントによって変更され、変数 bar
を関連付けます。 "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"(...)
に配置されます 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 インライン アセンブリ制約は、一種の「拡張関数プロトタイプ」と考えてください。これらは、コンパイラに、引数/戻り値の型と場所、およびその他の情報を伝えます。これらの制約を指定しない場合、インライン アセンブリは、グローバル変数/状態のみで動作する関数の類似物を作成しています。これは、おそらく誰もが同意するように、意図したとおりに動作することはめったにありません。