「b ++」のアセンブリ

C言語では、 "b ++"の集まりは何ですか。 2つの状況がありました。

1)1命令

     addl    $0x1,-4(%rbp)

2)3つの命令

        movl    -4(%rbp), %eax
        leal    1(%rax), %edx
        movl    %edx, -4(%rbp)

この2つの状況はコンパイラによって引き起こされたのでしょうか。

私のコード:

int main()
{
    int ret = 0;
    int i = 2;

    ret = i++;
    ret = ++i;
    return ret;
}

.sファイル(++ iはaddl instrctionを使用し、i ++はotherを使用):

        .file   "main.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $0, -8(%rbp)   //ret
        movl    $2, -4(%rbp)   //i
        movl    -4(%rbp), %eax
        leal    1(%rax), %edx
        movl    %edx, -4(%rbp)
        movl    %eax, -8(%rbp)
        addl    $1, -4(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, -8(%rbp)
        movl    -8(%rbp), %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 5.3.1-14ubuntu2) 5.3.1 20160413"
        .section        .note.GNU-stack,"",@progbits
2
2)を生成するソースコードを共有してください。
追加された 著者 geza,
リストされている .s ファイルには、2)で説明した内容は含まれていません。
追加された 著者 geza,
前の2)で別のコードを表示して、私たちを完全に誤解させたことをあなたが知っていることを願っています。元の2)をどのように考え出しましたか。
追加された 著者 geza,
いつものように、これらの答えはコンパイラのバージョンと最適化設定に大きく依存しています。 「はい、これらは両方とも有効です。コンパイラは面白いものです」以外に、この質問に本当に答えることができる方法はありません。
追加された 著者 Jonathon Reinhart,
2番目のバージョンは間違っています。 [rbp-4] に格納されている値を正しく増やしますが、 [rbp-4] のアドレス(32ビットに切り捨て)を [に格納します。 rbp-4] これは意味がありません。
追加された 著者 interjay,
ご存じのとおり、あなたが投稿した2つのオプションは同等ではありません。
追加された 著者 Ajay Brahmakshatriya,
CおよびC ++言語の仕様では、アセンブラについて一切言及されていないので、保証はありません。観察された動作がThe Standardに一致する限り、さまざまなコンパイラがさまざまなケースでさまざまなことを実行できます。そのため、関心のある特定のケースで何が起きるのかを探る必要があります( godbolt.org は非常に興味深いです)。そのためのツールです。
追加された 著者 BoBTFish,
godbolt.org を試して、さまざまなコンパイラ上で起こりうるさまざまなことをすべて確認してください。
追加された 著者 doctorlove,
完全を期すために、アセンブリがどのように生成されたかを質問で説明できますか(どのコンパイラ、どのフラグ)。
追加された 著者 Olivier Sohn,
1つはリリースのコンパイル、2つ目はdebugです。
追加された 著者 Anty,

4 答え

ISO規格は、カバーの下で起こることを一切義務付けていません。 C の指示に従って特定の方法で機能する「仮想マシン」を指定します。

したがって、CコンパイラがCからDartmouthへの基本変換として実装されている場合、 b ++ 10 let b = b + 1 になりますそうでなければ:-)

一般的なアセンブラコードにコンパイルする場合は、結果を使用するかどうかによって違いが見られます。具体的には a = b ++ ではなく b ++; 前者の結果は安全に捨てることができるので。

また、最適化レベルに基づいて大きな違いが生じる可能性があります。

結論として、出力に影響を与える可能性があるすべての事項(コンパイラ、ターゲットプラットフォーム、最適化レベルを含むがこれらに限定されない)をすべて指定することはできません。

2
追加された

最初のものは ret = ++ i の一部としての ++ i の出力です。古い値を保持する必要はありません。 ++ i を実行してから res = i を実行するためです。それをコンパイルするには、メモリを増やしてからそれを再ロードするのが本当に愚かで非効率的な方法ですが、最適化を無効にしてコンパイルしたので、gccはasmの出力を良くしません。

2番目のものは ret = i ++ の一部としての i ++ の出力です。古い値の i を保持する必要があるため、レジスタにロードし、 lea を使用して別のレジスタで i + 1 を計算します。 。それは単に ret に格納してから i に格納する前にレジスタをインクリメントしたかもしれませんが、最適化を無効にしてもgccは気付かないでしょう。


ソースなし、および偽のコードを使用した、前のあいまいな質問に対する以前の回答。

The asm for a tiny expression like b++ totally depends on the surrounding code in the rest of the function (or with optimization disabled, at least the rest of the statement) and whether it's a global or local, and whether it's declared volatile.

そしてもちろん、コンパイラの最適化オプションは大きな影響を与えます。最適化を無効にすると、gccはすべてのCステートメントに対して別々のasmブロックを作成するので、GDBの jump コマンドを使用して別のソース行に移動しても、期待どおりの動作になります。 C抽象マシンから。明らかにこれはcode-genを強く制約します。文をまたいでレジスタには何も保持されません。これはソースレベルのデバッグには向いていますが、store/reloadのノイズのせいで手作業で読むのは面倒です。

incとaddの選択については、 INC命令とADD 1を参照してください。問題ありませんか? clang -O3 -mtune = bdver2 は、メモリ送り先の増分に inc を使用しますが、一般的なチューニングまたはIntel P6またはSandybridgeファミリのCPUを使用します。より良いマイクロフュージョンのために add $ 1、(mem)を使います。

GCC/clangアセンブリの出力から「ノイズ」を取り除く方法は? 、特にMatt GodboltのCppCon2017へのリンクは、コンパイラのasm出力を見て意味を理解することについての話です。


元の質問の2番目のバージョンは、この奇妙なソースに対するほとんど最適化されていないコンパイラ出力のように見えます。

//inside some function
 int b;

                  //leaq  -4(%rbp), %rax  //rax = &b
 b++;             //incl   (%rax)
 b = (int)&b;     //mov    %eax, -4(%rbp)

質問は別のコードに編集されています。元のコードは、1行目のオペコードと別の行のオペランドを手作業で混在させることによって入力ミスされたようです。。更新されたコードについては、私の答えの前半を参照してください:それは周囲のコードと最適化を無効にすることによります。 res = b ++ を使うには<の古い値が必要ですcode> b 、増分値ではないため、asmが異なります。

それがあなたの情報源がしていることではないのであれば、あなたはいくつかの介入の指示か何かを省いたにちがいありません。それ以外の場合、コンパイラはそのスタックスロットを他のものに再利用しています。

gccとclangは通常、計算したばかりの結果を使用したくないため、どのコンパイラから入手したのか興味がありません。 incl -4(%rbp)を期待していました。

mov%eax、-4(%rbp)についても説明していません。コンパイラは既に inc %rax のアドレスを使用しているため、コンパイラは movではなく1バイト長いRBP相対アドレッシングモードに戻るのはなぜでしょうか。 %eax、(%rax)最近書き込まれていない、より少ない数の異なるレジスタを参照することは、レジスタ読み取りストールを減らすために、Intel P6ファミリーCPU(最大Nehalem)にとって良いことです。 (それ以外の場合は無関係です。)

RBPをフレームポインタとして使用すること(および単純な変数をレジスタに保持する代わりにメモリを増分すること)は、最適化されていないコードのように見えます。しかし、それは gcc -O0 からのものではあり得ません、なぜならそれは増分の前にアドレスを計算し、それらは2つの別々のCステートメントからでなければならないからです。

b++ = &b; isn't valid because b++ isn't an lvalue. Well actually the comma operator lets you do b++, b = &b; in one statement, but gcc -O0 still evaluates it in order, rather than computing the address early.

もちろん、最適化を有効にした場合、上書きする直前にメモリの増分を説明するために b volatile にする必要があります。

clang is similar, but actually does compute that address early. For b++; b = &b;, notice that clang6.0 -O0 does an LEA and keeps RAX around across the increment. I guess clang's code-gen doesn't support consistent debugging with GDB's jump the way gcc does.

    leaq    -4(%rbp), %rax
    movl    -4(%rbp), %ecx
    addl    $1, %ecx
    movl    %ecx, -4(%rbp)
    movl    %eax, %ecx          # copy the LEA result
    movl    %ecx, -4(%rbp)

I wasn't able to get gcc or clang to emit the sequence of instructions you show in the question with unoptimized or optimized + volatile, on the Godbolt compiler explorer. I didn't try ICC or MSVC, though. (Although unless that's disassembly, it can't be MSVC because it doesn't have an option to emit AT&T syntax.)

2
追加された
@ Jams.Liu:あなたが示したgcc5.3 -O0からの実際のasm出力には leaq -4(%rbp)、%rax はありません。これはLEAを使用して ret = b ++ (値をインクリメントする前に値を保持する必要がある)を実装するためにレジスタをコピーおよびインクリメントするために使用されています。 。 -O0 asmを見ることは、間違った /遅い方法を学ぶための良い方法です。速く走るつもりはありません。
追加された 著者 Peter Cordes,
@ Jams.Liu:あなたの最新の質問はついに理にかなっています。私はそれに答える私の答えの上部に新しいセクションを追加しました。
追加された 著者 Peter Cordes,
質問にコメントを追加しました。もう一度見てください。
追加された 著者 Jams.Liu,

式の結果が破棄された場合、優れたコンパイラであれば b ++ ++ b に最適化します。特に、 for ループの増分でこれがわかります。

それがあなたの「一命令」の場合に起こっていることです。

1
追加された

通常、値(変数)はload-modify-storeパラダイムを使用して更新されるため、最適化されていないコンパイラ出力を調べることは意味がありません。これは最初はアセンブリに慣れるときに役立つかもしれませんが、頻繁に使用するために値やポインタなどをレジスタに保持している最適化コンパイラに期待する出力ではありません。 (地域参照をご覧ください)

/* un-optimized logic: */

int i = 2;
ret = i++; /* assign ret <- i, and post-increment i (ret = i; i++ (i = 3)) */
ret = ++i; /* pre-increment i, and assign ret <- i  (++i (i = 4); ret = i) */

つまり、最新の最適化コンパイラであれば、 ret の最終値が(4)であると簡単に判断できます。

OS Xでgcc-7.3.0を使用すると、余分なディレクティブなどがすべて削除されます。

_main:  /* Darwin x86-64 ABI adds leading underscores to symbols... */
        movl    $4, %eax
        ret

AppleのネイティブのclangとMacPortsのclang-6.0は基本的なスタックフレームをセットアップしますが、それでも ret 算術を最適化します。

_main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $4, %eax
        popq    %rbp
        retq

Mach-O(OS X)ABIは、ユーザースペースコードのELF ABIと非常によく似ています。 「本物の」(本番の)コードを感じるには、少なくとも -O2 でコンパイルしてみてください。

1
追加された