アセンブラを使用して、Cをより深く理解します。 パート2(条件)

これは、サイクルの2番目の部分です。 その中で条件を分析します。 今回は、他の最適化レベルを試し、これがコードに与える影響を確認します。



誤解がないように、これらの記事の目的を示す価値があります。 各Cコンパイラを個別に解析しません。 それは長くて退屈です。 代わりに、Cコードの興味深い解釈の分析で読者を魅了し、人々が自分のコードをプロセッサーによってどのように変更して実行できるかを理解したいと思います。 また、初心者プログラマーの間で起こっているいくつかの神話を払拭します。 たとえば、ループに数値を追加すると、ある数値に別の数値を乗算するよりも高速になると考えている人がいます。 この記事では、-m32 -O0を使用してgccを具体的に分析していません。一部の人はこの考えをよく理解していません。 本当の意味があれば、コンパイラとキーの両方を変更します。



つまり、私は何を言いたいのですか? 古い例を2つ考えてみましょう。



int main(void) { register int a = 1; //    1 return a; //     }
      
      





そして



 int a = 1; int b = a * 2;
      
      





確かに、最初のケースのclangはスタック上の変数を定義しますが、私たちにとってそれはどれほど興味深いか、または重要ですか? レジスタ指定子に精通している人は、これが単なる推奨事項であることを読んだか知っています。 したがって、コンパイラは指定子を単に無視できます。 さらに、この例の目的は、最も単純な例を取り上げて、読者にレジスターを紹介することでした。 このgccにぴったり。 2番目の例はさらに単純です。この例では、clangはすぐにシフトを行い、3を掛けるとすでに降伏してimulを出します。 正直なところ、私はこの例で何が好奇心が強いのか本当に理解していないので、gccのコードも引用しました。これは数字22に変換されています。 また、コンパイラの開発者は、標準に違反しない場合に限り、自由に実装できます。 したがって、コンパイラに応じてコードの解釈が異なります。 しかし、すみません、それぞれを分解しますか? この資料の実用性は何ですか? みんなの頭を悩ます? 正しく指摘されているように、特定のコンパイラーに興味がある場合は、デバッガーと一緒に座ってください。 そして、これらの記事を読んでいる人たちにとってそれほど怖くないでしょう。



それでは、続けましょう。



最も単純な状態



まず、変数と数値を比較します。



 int a = 0; if (a < 5) { return 1; } return 0;
      
      





AFM(gcc 7.2):

  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 4 jg .L2 mov eax, 1 jmp .L3 .L2: mov eax, 0 .L3: leave ret
      
      





最初の行では、コンパイラーは変数「a」の値をスタックにプッシュします。 2番目には、新しいcmp命令があります。 この命令が2つの値を比較することを推測するのは難しくありません。 私たちの場合:スタックからの値と4。



しかし、それはどのように機能しますか? 単純に第1オペランドから第2オペランドを減算します。 sub命令も同様に機能しますが、 cmpの場合結果は保存されません。 ただし、EFLAGS / RFLAGSレジスタのフラグは、この結果に従って設定されます。 詳細に進むことなく、否定的な結果またはゼロの肯定的な結果があったかどうかを確認できます。 結果が正の場合、次の条件付きジャンプコマンドjgのみがトリガーされます(ジャンプがg reaterの場合)。



これを理解した場合、公正な質問が発生する可能性があります。サインが小さければもっと多くなるのはなぜですか? 実際、 <5の場合は記述しましたが、> 4の場合は何かをするようになりました。しかし、プログラムのロジックは違反されませんでした。 実際、a> 4の場合は0を返します。その後、別の公正な質問が発生する可能性がありますが、条件を記述した場合:(a> 4)0を返す場合、コード自体はどのように変化しますか?



  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 4 jle .L2 mov eax, 0 jmp .L3 .L2: mov eax, 1 .L3: leave ret
      
      





そして再び条件の反転を取得します: jle 、あなたはそれを推測しました、より小さいか等しい(essまたはe qualの場合ジャンプ)



問題は、returnがプログラムを完了するため、最後の2つの指示に従う必要があるため、両方の例でjmp .L3変更されないためです。 これは無条件のジャンプ命令です。 私たちの場合:eaxレジスタに完全に異なる数値を書き込む必要がある条件に続く行をスキップします。



つまり、コンパイラは反対の条件をチェックして、元のfalseでコードを送信しますが、元の条件がtrueの場合、cmpの直後のコードと条件付きジャンプが実行されます。 明確にするために、条件分岐に番号を付けてみましょう。



 int a = 0; if (a > 5) { //#0 return 1; } //#1 return 0;
      
      





  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 4 jle .L2 ;#0 mov eax, 0 jmp .L3 .L2: mov eax, 1 ;#1 .L3: leave ret
      
      





ご覧のとおり、プログラムの構造には違反していませんが、条件付き遷移を置き換えると、次のようになります。



  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 5 jg .L2 ;#1 mov eax, 1 jmp .L3 .L2: mov eax, 0 ;#0 .L3: leave ret
      
      





つまり、条件の内側の部分はプログラムに落ちますが、これはあまり良くありません:たとえば、条件(セクション#1)の後にまだ行の束があるので、セクション#0を見るために、リストを非常に下にスクロールします。 (ifの後、コードの実行を継続するために再び戻る必要があることを明確にするのを忘れました。つまり、もう1つのラベルともう1つの遷移です。)



署名なし



符号付きの数値を比較するだけでしたが、符号なしの数値を比較するとどうなりますか?



 unsigned int a = 0; if (a > 5) {    return 1; } return 0;
      
      





  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 5 jbe .L2 mov eax, 1 jmp .L3 .L2: mov eax, 0 .L3: leave ret
      
      





条件付きジャンプ命令以外は何も変更されていません。jleの代わりにjbe( j ump b elowまたはe qual)になりました。 符号付き数値と符号なし数値を比較するための2つの異なる指示があるのはなぜですか?



 int a = 01; //-1 unsigned int a = 0 – 1; //4294967295
      
      





実際、メモリ内では、いずれにしても4294967295になります。これは単なる表示方法であり、Cで書くことができます。



 unsigned int a = 0 - 1; printf("%i", a); //-1
      
      





しかし、cmp命令では、1つのフラグが設定されるのではなく、複数のフラグが設定されます。 jbe命令は、減算時にオーバーフローフラグをチェックし、jleは結果の上位ビットの値に等しいフラグをチェックします(つまり、結果が負の場合は1です)。 現実には、すべてがもう少し複雑です。JBE(CF = 1またはZF = 1)、JLE(ZF = 1またはSF <> OF)ですが、これに集中することはできません。 さらに興味深いことに移りましょう:



 unsigned int a = 0; if (a < 0) {    return 1; } return 0;
      
      





以下に変換されます:



  mov DWORD PTR [ebp-4], 0 mov eax, 0 leave ret
      
      





いいですね コードのロジックでは、変数「a」がゼロより小さくなることはないため、条件は単純に破棄されます。



そしてこれはどうですか:



 unsigned int a = 0; if (a > 0) {    return 1; } return 0;
      
      





AFM:



  mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-4], 0 je .L2 mov eax, 1 jmp .L3 .L2: mov eax, 0 .L3: leave ret
      
      





比較の結果がゼロの場合、 je命令はジャンプします( eが等しい場合はジャンプします)。



「<」は「<=」よりも高速ですか? または” <||より =”?



jle、jbe、jg、jeのいくつかの条件分岐命令についてはすでに説明しました。 そのような命令は、すべての場合でわずかに大きく、逆の命令もあります。たとえば、jneがゼロまたは等しくない、またはjnbeが低くなく等しくないなどです。 つまり、数値を比較すると、cmp(またはtest)とjcc(条件付きジャンプ)の2つの命令が得られます。 したがって、たとえば、<と<=の命令の数に違いはないと結論付けることができます。



しかし、



 if (a < 0 || a == 0)
      
      





違いはありますが、-O0のみです。



次のプログラムを見てみましょう。



 #include <stdio.h> int main() { int a = 0; scanf("%d", &a); if (a < 0 || a == 0) {    return 10; } return 20; }
      
      





今回はclang 5.0.0 -O3 -m32を使用します。これは、生成されるasmコードが少ないためです。この例では、何が起こっているのかを簡単に説明できます。



  sub esp, 12 mov dword ptr [esp + 8], 0 ;scanf sub esp, 8 lea eax, [esp + 16] push eax push .L.str call scanf add esp, 16 ;end scanf ;   cmp dword ptr [esp + 8], 0 ;#1 mov ecx, 10 ;#2 mov eax, 20 ;#3 cmovle eax, ecx ;#4 ;  add esp, 12 ret .L.str: .asciz "%d"
      
      





#1:変数「a」とゼロの比較

#2:ecxは10

#3:eaxは20

#4:cmovleはjleに似ていますが、指定された値を移動するだけです。 したがって、a <= 0の場合、ecx(10)の値はeaxに分類され、そうでない場合は単に20のままになります。



Cコードで<= 0に置き換えられても何も変わらないことは既に理解していますが、希望があるかどうかを確認できます。



条件が条件でなくなるとき



状況を想像してください。コードには条件がありますが、デバッグ時には条件付き命令を見つけることができません。 面白い?



次のコードを見てください。



 int x = 10; scanf("%d", &x); if (x < 0) { return 3; } return 2;
      
      





cmpとラベルを期待でき、setxのようなさらに重要なものを期待できますが、次のようになりました(clang 5.0.0 -O3 -m32):



  mov eax, dword ptr [esp + 8] shr eax, 31 or eax, 2 add esp, 12 ret
      
      





さて、それは何ですか? それを理解しましょう。 最初の行ですべてが明確になっています。eaxでは、変数xの値が転送されました。



最後の記事の次の行を覚えておく必要があります。 これは右に31ビットシフトします。 つまり、実際には、整数の最初のビットしかありません。



次はビット演算「or」です。 つまり、結果として10または11(バイナリ表記)が得られます。 それだけです。次の行は関数のエピローグに関連しています。



興味深いことに、そのようなコードを書くことを推測することは特に難しくありません。 変数xの数値の符号をデュースに追加するだけです。



同じロジックですが、わずかに異なります(gcc 4.8.5など)。



  sar eax, 31 not eax add eax, 3
      
      





sarは右へのシフトでもありますが、わずかに異なる方法で動作します。最上位ビット、つまり符号であり、シフトしません。



[1000] shr [0100] shr [0010] shr [0001]



[1000] sar [1100] sar [1110] sar [1111]



つまり、すべてのユニットがある場合、数値は負であり、すべてのビットを反転し、ゼロを取得し、3を追加するだけで、まさに望みどおりになりました。 そして、数値が正の場合、すべてゼロになり、反転後はゼロになります。 実際、これは-1で、それに3を追加すると、2になります。



この場合、MSVC -O2がより期待されます。



  cmp DWORD PTR _x$[ebp], eax ; x < 0 setl al ; less ? mov al, 1 add eax, 2 ; eax + 2
      
      





簡単に言うと、比較結果が負の場合、eaxレジスタの下位バイトが1に設定され、その後2.が追加されます。 コンパイラーはこの手法を本当に気に入っています。また会えることを願っています。



他のステートメント



反対の条件はチェックされないことを誰もが理解していると思います。 コードには別のラベルが表示されますが、それがすべての機能です。 これを確認して、次のコードを考えてみましょう。



 int x = 10; int c = 0; if (x < 4) { c = 3; } else { c = 2; } return c;
      
      





AFM:



  mov DWORD PTR [ebp-8], 10 mov DWORD PTR [ebp-4], 0 cmp DWORD PTR [ebp-8], 3 jg .L2 mov DWORD PTR [ebp-4], 3 jmp .L3 .L2: mov DWORD PTR [ebp-4], 2 .L3: mov eax, DWORD PTR [ebp-4]
      
      





ご覧のとおり、唯一の違いは、if内でコードを実行した後、elseブロック(ラベル.L2)の内部をジャンプすることです。 詳細な分析は不要であり、すべてが明らかであるように思えます。



ifの論理演算



少し非標準的な例を見てみましょう:



 int main(void) { int a = -1; if (a++ < 0 || a++ > 5) { a++; } else { a+=2; } return 0; }
      
      





最初に答えを出そうとします。変数「a」にはどのような値が含まれますか? これが難しくない場合は、asmコードがどのように見えるかについてすでにある程度の考えがあります。



  mov DWORD PTR [ebp-4], -1 mov eax, DWORD PTR [ebp-4] ;#1 lea edx, [eax+1] ;#1 mov DWORD PTR [ebp-4], edx ;#1 test eax, eax ;#1 js .L2 ;#1 mov eax, DWORD PTR [ebp-4] ;#2 lea edx, [eax+1] ;#2 mov DWORD PTR [ebp-4], edx ;#2 cmp eax, 5 ;#2 jle .L3 ;#2 .L2: mov eax, 1 ;#3 jmp .L4 ;#3 .L3: mov eax, 0 ;#4 .L4: test al, al ;#5 je .L5 ;#5 add DWORD PTR [ebp-4], 1 ;#6 jmp .L6 ;#6 .L5: add DWORD PTR [ebp-4], 2 ;#7 .L6: mov eax, 0 ;#8
      
      





だから、私たちはそれを解明しようとしています。 関連するものを区別しやすくするために、操作に番号を割り当てました。



#1:a ++ <0。この場合、値はインクリメントの前にeaxに書き込まれ、これはゼロと比較する必要があるものです。 テスト命令はとの原理で動作しますが、オペランド自体は変更せず、フラグのみを変更します。 私たちの場合、 テスト後、数値の符号をチェックし、1である場合、遷移を実行します。 また、1増加した値がスタックに返されます。 増加は、 lea edx [eax + 1]で行われます。 lea命令は、実効アドレスをロードするために使用されます。 この例では、 mov edx、eaxadd edx、1の 2つの命令を一度に置き換えます。

#2:a ++> 5.実際、同じことが起こります。<= 5の場合、.L3ラベルへの遷移のみです。つまり、最初の条件または2番目の条件が満たされると、.L2ラベルに到達します。 最初の条件が満たされた場合、2番目の条件は計算されないことに注意してください。 しかし、あなたはすでにそれを知っているはずです。

#3:eaxレジスタでは1

#4:eaxレジスタで0

#5:eaxレジスタの下位バイトのゼロをチェックし、等しい場合は、ラベル.L5に移動します

#6:それ以外の場合は、変数「a」に追加します1.プログラムの最後に移動します。

#7:eaxの最下位バイトがゼロの場合、変数「a」を追加します2



つまり、#3は両方の条件が満たされ、「フラグ」が設定されていることを確認し、「フラグ」がオンの場合は#5でチェックが実行され、そうでない場合は1が追加されます。



また、(a <0 && a <-5)の場合、最初の条件が満たされない場合、2番目の条件も計算されないことに注意してください。



おわりに



Cの条件を簡単に確認しました。 コンパイラーがコードをわずかに変更し、最適化中に認識できないほどに変更できることを確認しました。



残念ながら、この記事は長すぎるため、switchステートメントを検討することはできなかったため、必要に応じて次の記事でこれを行うことができます。 同時に、最適化されたプログラムを他のものと一緒に検討することもできます。



前の記事

次の記事



All Articles