AsmWalker ガイド — if 文・ループがアセンブラではどう動くか
CPU には通常の汎用レジスタ(rax, r0 など)とは別に、フラグレジスタ(APSR / RFLAGS)という 特殊なレジスタがあります。 演算命令や比較命令を実行するたびに、その結果の性質を示すビットが自動的にセットされます。
条件分岐命令はこのフラグを読んで「分岐するかどうか」を決定します。
つまり if (a > b) という C の条件は、アセンブラでは
「比較命令でフラグを立て → 条件分岐命令でフラグを見て分岐」という 2 ステップで実現されます。
| フラグ | ARM 呼称 | x86 呼称 | セットされる条件 |
|---|---|---|---|
| Z(Zero) | Z | ZF | 命令の計算結果がちょうど 0 例: CMP a, b は内部で a - b を計算する。a と b が等しければ結果が 0 になり Z=1 がセットされる |
| N(Negative) | N | SF(Sign) | 演算結果の最上位ビットが 1(負の数) |
| C(Carry) | C | CF | 符号なし整数として桁あふれが発生 |
| V(oVerflow) | V | OF | 符号あり整数として桁あふれが発生 |
CMP a, b は a - b を計算し、その結果によってフラグをセットします。
計算結果自体はどこにも保存されません(比較のためだけに使う減算命令です)。
; CMP 3, 3 (a == b)
; → Z=1, N=0, C=1, V=0
; CMP 5, 3 (a > b)
; → Z=0, N=0, C=1, V=0
; CMP 3, 5 (a < b)
; → Z=0, N=1, C=0, V=0
ARM の C フラグは 「借りが発生しなかった」ときに 1。
a >= b → C=1、a < b → C=0
; CMP 3, 3 (a == b)
; → Z=1, N=0, C=0, V=0
; CMP 5, 3 (a > b)
; → Z=0, N=0, C=0, V=0
; CMP 3, 5 (a < b)
; → Z=0, N=1, C=1, V=0
x86 の CF は 「借りが発生した」ときに 1。
a < b → CF=1、a >= b → CF=0
CMP 3, 5(a < b)でも ARM は C=0、x86 は CF=1 となります。
AsmWalker でモードを切り替えながら CMP 命令のフラグ変化を比べてみてください。
TEST a, b は a AND b を計算してフラグをセットします(結果は保存しない)。
主に「あるビットが立っているか」を調べるために使われます。
; if (x != 0) の判定
test eax, eax ; eax AND eax → eax が 0 なら Z=1
jnz .nonzero ; Z=0 なら分岐(eax ≠ 0)
; if (x & 1) の判定(奇数チェック)
test eax, 1 ; eax AND 1 → ビット0 が 0 なら Z=1
jz .even ; Z=1 なら偶数
flags: Z=0, N=0, C=0, V=0 のようなフラグ変化が表示されます。
次の条件分岐命令がそのフラグを参照して分岐するかどうかを決めていることを確認してください。
CMP でフラグが立ったあと、条件分岐命令がそのフラグを読んで「指定のラベルへジャンプするか否か」を判断します。 条件が成立しない場合は次の命令に進みます(fall-through)。
| C の条件 | x86 命令 | ARM 命令 | チェックするフラグ |
|---|---|---|---|
==(等しい) | JE / JZ | BEQ | Z=1 |
!=(等しくない) | JNE / JNZ | BNE | Z=0 |
<(符号あり) | JL / JNGE | BLT | N≠V |
<=(符号あり) | JLE / JNG | BLE | Z=1 または N≠V |
>(符号あり) | JG / JNLE | BGT | Z=0 かつ N=V |
>=(符号あり) | JGE / JNL | BGE | N=V |
<(符号なし) | JB / JNAE | BCC / BLO | C=0 |
>(符号なし) | JA / JNBE | BHI | C=1 かつ Z=0 |
| 無条件分岐 | JMP | B | (常に成立) |
J は Jump、E=Equal、N=Not、L=Less、G=Greater、
B=Below(符号なし)、A=Above(符号なし)です。
ARM の B は Branch、EQ=Equal、NE=Not Equal などのサフィックスが付きます。
GCC がどのように if/else を翻訳するかを見てみましょう。
// C言語
if (a > b) {
result = a;
} else {
result = b;
}
ldr r0, [fp, #-8] ; r0 = a
ldr r1, [fp, #-12] ; r1 = b
cmp r0, r1 ; a - b でフラグをセット
ble .Lelse ; a <= b なら else へ
; if の本体(a > b)
ldr r3, [fp, #-8] ; result = a
str r3, [fp, #-16]
b .Lend ; else をスキップ
.Lelse:
ldr r3, [fp, #-12] ; result = b
str r3, [fp, #-16]
.Lend:
mov eax, DWORD PTR [rbp-4] ; eax = a
cmp eax, DWORD PTR [rbp-8] ; a - b でフラグをセット
jle .Lelse ; a <= b なら else へ
; if の本体(a > b)
mov eax, DWORD PTR [rbp-4]
mov DWORD PTR [rbp-12], eax ; result = a
jmp .Lend ; else をスキップ
.Lelse:
mov eax, DWORD PTR [rbp-8]
mov DWORD PTR [rbp-12], eax ; result = b
.Lend:
GCC は C の条件を反転させた分岐命令を生成するのが基本パターンです。
if (a > b) なら「a > b でないとき(a <= b のとき)else へ飛ぶ」という
逆向きの条件でジャンプします。条件が成立するとき(true の場合)は次の命令へ続くだけです。
if (cond) → アセンブラでは j[NOT cond] else_label が出てきます。
「else に飛ぶ条件」が書かれているので、最初は逆に読めてしまうことがあります。
「この分岐は else へのエスケープ」と考えると読みやすくなります。
ループは「条件チェック → 本体 → ループ先頭へ戻るジャンプ」の繰り返しに翻訳されます。
// C言語
int i = 0;
int sum = 0;
while (i < 5) {
sum += i;
i++;
}
mov r3, #0
str r3, [fp, #-8] ; i = 0
mov r3, #0
str r3, [fp, #-12] ; sum = 0
.Lloop:
ldr r3, [fp, #-8] ; r3 = i
cmp r3, #4 ; i - 4 でフラグ
bgt .Lend ; i > 4 なら終了
ldr r2, [fp, #-12]
ldr r3, [fp, #-8]
add r3, r2, r3 ; sum += i
str r3, [fp, #-12]
ldr r3, [fp, #-8]
add r3, r3, #1 ; i++
str r3, [fp, #-8]
b .Lloop ; 先頭に戻る
.Lend:
mov DWORD PTR [rbp-4], 0 ; i = 0
mov DWORD PTR [rbp-8], 0 ; sum = 0
.Lloop:
cmp DWORD PTR [rbp-4], 4 ; i - 4 でフラグ
jg .Lend ; i > 4 なら終了
mov eax, DWORD PTR [rbp-4]
add DWORD PTR [rbp-8], eax ; sum += i
add DWORD PTR [rbp-4], 1 ; i++
jmp .Lloop ; 先頭に戻る
.Lend:
GCC の -O0 出力では「ループ先頭でチェックしてから本体を実行」するパターンが多いです。
最適化(-O1 以上)では「先に本体を実行してからチェック」する do-while 形式に
変換されることもあります(余分なジャンプを削減できるため)。
C の比較は変数の型によって「符号あり比較」か「符号なし比較」かが決まります。 アセンブラでは異なる条件分岐命令を使い分けます。
| 型 | 比較の種類 | x86 分岐命令 | ARM 分岐命令 |
|---|---|---|---|
int(符号あり) | 符号あり比較 | JL / JG / JLE / JGE | BLT / BGT / BLE / BGE |
unsigned int(符号なし) | 符号なし比較 | JB / JA / JBE / JAE | BCC / BHI / BLS / BCS |
これは CPU がフラグを使う方法の違いです。 符号あり比較では N フラグと V フラグの関係を使い、 符号なし比較では C フラグと Z フラグを使います。
-1(符号あり)は 2 の補数表現で 0xFFFFFFFF です。
符号なしとして解釈すると約 42 億(= 4294967295)になります。
-1 < 1 は符号ありでは true ですが、符号なしでは false になります。
コンパイラは C の変数型から自動的に適切な比較命令を選択します。
&& や || を使った複合条件は、
アセンブラでは複数の CMP + 条件分岐の組み合わせに展開されます。
// C言語: if (a > 0 && b > 0)
cmp eax, 0
jle .Lfalse ; a <= 0 なら即終了(短絡評価: a が false なら b を見ない)
cmp ebx, 0
jle .Lfalse ; b <= 0 なら終了
; 両方 true の処理
...
.Lfalse:
...
C の論理 AND(&&)は左から順に評価し、最初に false になった時点で残りを評価しない
「短絡評価(short-circuit evaluation)」が保証されています。
アセンブラでは、左側の条件が偽のとき即座にラベルへジャンプすることで実現されています。
cmp r0, #0
bgt .Ltrue ; a > 0 なら即 true
cmp r1, #0
ble .Lfalse ; b <= 0 なら false
.Ltrue:
; 少なくとも一方が true
cmp eax, 0
jg .Ltrue ; a > 0 なら即 true
cmp ebx, 0
jle .Lfalse ; b <= 0 なら false
.Ltrue:
; 少なくとも一方が true
| 概念 | 内容 |
|---|---|
| フラグレジスタ | Z / N / C / V の 4 ビット。演算・比較命令が自動更新 |
| CMP a, b | a - b を計算してフラグをセット(結果は保存しない) |
| TEST a, b | a AND b を計算してフラグをセット(ビット検査に使う) |
| 条件分岐命令 | フラグを読んで分岐するかを決定(Jcc / Bcc) |
| if/else の変換 | 条件を反転した分岐で else ブロックへエスケープ |
| ループの変換 | 先頭でチェック → 本体 → 先頭へ無条件ジャンプ |
| 符号あり vs 符号なし | 変数型によって異なる分岐命令(JL/JG vs JB/JA)を使用 |
| 短絡評価 | &&/|| は複数の CMP + 分岐の連鎖で実現 |
条件分岐の仕組みを理解すると、アセンブラで書かれたコードの制御フローを 上から順に追うだけで「どの条件でどこへ飛んでいるか」が読み解けるようになります。 AsmWalker でサンプルコードをステップ実行しながら、 C の条件式とアセンブラの分岐命令を対比させてみてください。