コース目次 / 第3章

条件分岐とループを読む

for文をコンパイルしたアセンブリを読み、cmp命令とjmp/jle等の条件・無条件ジャンプが、if/forをどう表現するかを追います。実際に行番号を対応づけながらループの構造を読み解きます。

第3章 / 全5章目安 約11分この章のゴール: cmpとジャンプ命令の組み合わせで、条件分岐とループを読めるようになる

C言語の if や for は、アセンブリでは専用の命令ではありません。比較(cmp)と、条件によって飛び先を変えるジャンプ(j で始まる命令)の組み合わせで表現されます。

標的 — 1からnまで足し合わせる

int sumTo(int n) {
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}
gcc -O0 -g -c loop.c -o loop.o
objdump -d -M intel loop.o
0000000000000000 <sumTo>:
   0:	f3 0f 1e fa          	endbr64
   4:	55                   	push   rbp
   5:	48 89 e5             	mov    rbp,rsp
   8:	89 7d ec             	mov    DWORD PTR [rbp-0x14],edi
   b:	c7 45 f8 00 00 00 00 	mov    DWORD PTR [rbp-0x8],0x0
  12:	c7 45 fc 01 00 00 00 	mov    DWORD PTR [rbp-0x4],0x1
  19:	eb 0a                	jmp    25 <sumTo+0x25>
  1b:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  1e:	01 45 f8             	add    DWORD PTR [rbp-0x8],eax
  21:	83 45 fc 01          	add    DWORD PTR [rbp-0x4],0x1
  25:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
  28:	3b 45 ec             	cmp    eax,DWORD PTR [rbp-0x14]
  2b:	7e ee                	jle    1b <sumTo+0x1b>
  2d:	8b 45 f8             	mov    eax,DWORD PTR [rbp-0x8]
  30:	5d                   	pop    rbp
  31:	c3                   	ret

ソースと、対応づけて読む

長く見えますが、C のソースと1行ずつ対応づければ、すっきり読めます。

[rbp-0x14] = 引数 n、[rbp-0x8] = total、[rbp-0x4] = i、と割り当てられています(コンパイラが決めた配置)。
アドレス b, 12 — total = 0; i = 1; の初期化。
アドレス 19 — jmp 25。無条件で、比較を行うアドレス25へ先にジャンプします(for は本体より先に条件を1回確かめる形が定石なので、最初の1回はここで条件チェックへ飛びます)。
アドレス 1b〜21 — ループの本体。total += i;(1b, 1e)、i++(21)。
アドレス 25, 28 — i を eax に読み、cmp eax, [rbp-0x14] で i と n を比較。
アドレス 2b — jle 1b。比較結果が「以下(less or equal)」なら、本体の先頭(アドレス1b)へ戻ってジャンプ。そうでなければ、次の行(2d)へ進み、ループを抜けます。

cmp は、実は「引き算して、結果を覚える」だけ

cmp a, b は、内部的にはa - b を計算していますが、その結果を a に書き戻しません。代わりに、結果が0だったか・負だったか、といった状態を、CPU内部の小さなフラグに記録します。直後に続く jxx 系の命令(je=等しければ、jle=以下なら、jg=より大きければ、など)は、そのフラグを見て、飛ぶか飛ばないかを決めます。cmp と jxx は、常に対で働くペアです。

読み方のコツ — ジャンプ先を、先に丸で囲む

初めて見るアセンブリを読むときのコツです。

1. まず、すべての jmp/jxx 命令の行き先アドレスに印を付ける。今回なら 19→25 と 2b→1b。
2. その印を頼りに、コードを「ブロック」に分ける。ジャンプ先・ジャンプ元は、たいてい意味のある区切り(ループの先頭、条件分岐の分かれ目)です。
3. ブロックごとに「何をしているか」を、C言語のつもりで言語化する。今回なら「初期化」「本体」「条件チェックして戻るか抜けるか」の3ブロックに分かれました。
この手順は、この先どれだけ複雑な関数を読むときも変わりません。

持ち帰る一言

if/forは、cmpとジャンプの組み合わせ。 cmp が比較してフラグを立て、jxx がそれを見て飛ぶ。ジャンプ先に印を付けてブロックに分ければ、初めて見るアセンブリでも制御の流れを再構成できます。次はまとめて、このコースを締めます。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。