アセンブラの読み方

アセンブラは難しそうに見えますが、基本的なルールを押さえると読めるようになります。C言語と比べながら、代表的な命令を一つずつ確認しましょう。

目次

  1. 命令の基本構造
  2. 読む方向:左が行き先、右が元
  3. レジスタとは何か
  4. MOV — 値を運ぶ命令
  5. ADD / SUB — 足し算・引き算
  6. メモリアクセス — LDR/STR(ARM)と [ ](x86)
  7. CMP — 比較命令
  8. PUSH / POP — スタック操作
  9. 読むコツまとめ

1. 命令の基本構造

アセンブラの1行は ニモニック(命令名)オペランド(対象) から成ります。

MOV ニモニック
(何をするか)
 
rax 第1オペランド
(行き先)
5 第2オペランド
(元)

ニモニックは命令の略称です。MOV は MOVE(移動)、ADD は ADD(加算)、STR は STORE(格納)のように、英単語の略になっています。

ニモニックを覚えるコツ:
完全な英単語を意識すると覚えやすくなります。MOV=Move, CMP=Compare, LDR=Load Register, STR=Store Register, BL=Branch with Link(サブルーチン呼び出し)。

命令長とアドレス表示

実際の命令は1行ではなく、メモリ上では複数バイトのバイト列として格納されています。命令の種類によってバイト数が変わります。

アーキテクチャ実際の命令長
ARM Thumb / Thumb-22バイトまたは4バイトが混在
x86-641〜15バイト(命令によって可変)
AsmWalker ではすべての命令を「4バイト固定」として扱います:
アセンブラパネルのアドレスは実際の命令長ではなく、学習のしやすさを優先した仮想アドレスです。 詳しくはマシン語とアセンブラ、そしてPC — PCが進む仕組みを参照してください。

2. 読む方向:左が行き先、右が元

このツールが扱う2つのアーキテクチャはどちらも 「左 ← 右」 の順です。

アーキテクチャ読み方
x86-64(Intel構文) mov rax, 5 rax に 5 を入れる
ARM mov r0, #5 r0 に 5 を入れる
AT&T 構文(GCC デフォルト)との違い:
Linux などで gcc -S を使うと AT&T 構文が出力され、順序が逆(右 → 左)になります。 movl $5, %eax は「eax に 5 を入れる」で意味は同じですが読む方向が違います。 AsmWalker は Intel 構文(-masm=intel)を採用しているため、左が行き先で統一されています。

3. レジスタとは何か

レジスタは CPU の中にある超高速な変数です。C言語の変数はメモリに置かれますが、レジスタは CPU チップ上にあるため、メモリアクセスより桁違いに速いです。

x86-64 の主なレジスタ

64bit 名32bit 名16bit 名8bit 名主な用途
raxeaxaxal計算・関数戻り値
rbxebxbxbl汎用
rcxecxcxclカウンタ・第4引数
rdxedxdxdl乗除算・第3引数
rdiedididil第1引数
rsiesisisil第2引数
rspespスタックポインタ
rbpebpフレームポインタ
eax と rax は同じレジスタ:
rax(64bit)の下位32bitが eax、下位16bitが ax、下位8bitが al です。 int(32bit)型の値を扱うとき GCC は eax を使います。 eax に書き込むと上位32bitは自動的にゼロになります。

ARM の主なレジスタ

名前別名主な用途
r0第1引数 / 関数戻り値
r1第2引数
r2第3引数
r3第4引数
r4〜r11汎用(呼び出し先保存)
r13spスタックポインタ
r14lrリンクレジスタ(戻り先アドレス)
r15pcプログラムカウンタ

4. MOV — 値を運ぶ命令

最も頻出の命令です。レジスタ間のコピー、即値のセット、メモリからの読み書きに使います。

ARM

mov r0, #3       ; r0 = 3(即値)
mov r1, r0       ; r1 = r0(レジスタコピー)

ARM では即値に # を付ける。

x86-64

mov eax, 3       ; eax = 3(即値)
mov ebx, eax     ; ebx = eax(レジスタコピー)

x86 Intel 構文では即値に # は付けない。

C言語との対応イメージ:

int a = 3;   →   mov eax, 3        ; eax が変数 a に対応
int b = a;   →   mov ebx, eax      ; ebx が変数 b に対応
AsmWalker での確認:
MOV 命令のステップでは命令詳細パネルに rax ← 3 のような表記で値の流れが示されます。 同時にレジスタパネルで変化したレジスタが白くハイライトされます。

5. ADD / SUB — 足し算・引き算

2つの値を加減算して結果を第1オペランドに書き戻します。

ARM

add r0, r1, r2   ; r0 = r1 + r2
sub r0, r0, #1   ; r0 = r0 - 1

ARM は3オペランド形式が多い(行き先を明示)。

x86-64

add eax, edx     ; eax = eax + edx
sub eax, 1       ; eax = eax - 1

x86 は2オペランド形式(行き先 = 行き先 OP 元)。

; C: int result = a + b;
; x86-64 での展開例
mov  edx, DWORD PTR [rbp-4]   ; edx = a(メモリから読み出し)
mov  eax, DWORD PTR [rbp-8]   ; eax = b
add  eax, edx                  ; eax = a + b
AsmWalker での確認:
ADD ステップの命令詳細パネルに eax ← eax(3) + edx(5) = 8 のように 計算前後の値が表示されます。

6. メモリアクセス — LDR/STR(ARM)と [ ](x86)

レジスタはCPU内の一時置き場なので、変数の値を永続的に保持するにはメモリへの読み書きが必要です。

読み込み(Load)

ARM — LDR(Load Register)

ldr r0, [r7, #4]  ; r0 = メモリ[r7+4]

[ ] の中がメモリアドレス。ベースレジスタ+オフセット。

x86 — MOV + [ ]

mov eax, DWORD PTR [rbp-4]
; eax = メモリ[rbp-4](4バイト読み出し)

DWORD PTR は4バイト幅を指定。

書き込み(Store)

ARM — STR(Store Register)

str r0, [r7, #4]  ; メモリ[r7+4] = r0

LDR と逆。r0 の値をメモリに保存する。

x86 — MOV + [ ] (行き先がメモリ側)

mov DWORD PTR [rbp-4], eax
; メモリ[rbp-4] = eax(4バイト書き込み)

x86 は MOV1つでロードもストアも表現する。

[ ] の中の読み方:
[rbp-4][r7, #8] はアドレスの計算式です。 rbp-4 なら「rbp の値から 4 を引いたアドレス」を意味します。 これはローカル変数がフレームポインタからの固定オフセットに配置されているためです。 変数が増えるごとにオフセット(-4, -8, -12…)が増えていきます。

メモリサイズの指定(x86)

指定子サイズ対応するC型
BYTE PTR1バイトchar
WORD PTR2バイトshort
DWORD PTR4バイトint, float
QWORD PTR8バイトlong, double, ポインタ
AsmWalker での確認:
LDR/STR(ARM)や mov [...], reg(x86)のステップでは スタックパネルの該当アドレスのセルがハイライトされ、値の変化が確認できます。

7. CMP — 比較命令

CMP は引き算を行いますが、結果をレジスタに書かず、フラグだけを更新します。

cmp eax, 5    ; eax - 5 を計算してフラグを更新(eax は変わらない)
je  .L1       ; Zero Flag が立っていれば .L1 へジャンプ(eax == 5)

フラグレジスタは演算結果の性質を記録する1bitのフラグ群です。

フラグ意味立つ条件
Zero Flag (ZF)結果がゼロcmp eax, eax(同値比較)
Negative Flag (NF)結果が負cmp eax, 10 で eax < 10
Carry Flag (CF)桁あふれ(符号なし)符号なし演算のオーバーフロー
Overflow Flag (OF)桁あふれ(符号あり)符号あり演算のオーバーフロー

CMP の後に続く条件ジャンプ命令がこのフラグを参照します。

x86ARM条件フラグ
jebeq等しい(==)ZF=1
jnebne等しくない(!=)ZF=0
jlblt小さい(<)NF≠OF
jgbgt大きい(>)ZF=0 かつ NF=OF
jleble以下(<=)ZF=1 または NF≠OF
jgebge以上(>=)NF=OF
; C: if (a > 0) { ... }
cmp eax, 0    ; a - 0 のフラグを計算
jle .L_else   ; a <= 0 なら else へジャンプ
; ↑ if の中身
.L_else:
読み解くコツ:
コンパイラは if (a > 0) の条件を 反転 して jle(以下ならスキップ)を生成することが多いです。 「条件が成立しないときにジャンプ」という発想で読むと理解しやすくなります。
AsmWalker での確認:
CMP ステップの命令詳細パネルでは更新されるフラグ(ZF / NF など)が表示されます。 続く条件ジャンプのステップで、ジャンプが発生するかどうかを確認できます。

8. PUSH / POP — スタック操作

PUSH はスタックに値を積み、POP は取り出します。スタックポインタ(SP / RSP)が自動的に動きます。

ARM

push {r7, lr}  ; r7 と lr をスタックに保存
               ; sp -= 8

pop  {r7, pc}  ; r7 と pc を復元
               ; sp += 8

複数レジスタを一度に保存・復元できる。

x86-64

push rbp       ; [rsp-8] = rbp, rsp -= 8

pop  rbp       ; rbp = [rsp], rsp += 8

1レジスタずつ操作する。

PUSH/POP は関数のプロローグ・エピローグで必ず登場します。「呼び出し元のレジスタ値を壊さないように保存して、帰りに復元する」という用途です。

AsmWalker での確認:
PUSH のステップではスタックパネルに新しいセルが追加され、SP が下方向に動くのが見えます。 POP では逆にセルが消えて SP が上に戻ります。

9. 読むコツまとめ

  1. 左 → 右 の順で「どこに何を入れるか」を読む(Intel 構文 / ARM 共通)
  2. [ ] はメモリ参照の印。カッコの中はアドレスの計算式
  3. 命令名は英単語の略。知らない命令は略を展開して考える(MOV=Move, LDR=Load Register)
  4. CMP の後に条件ジャンプがセットで登場する。条件は反転していることが多い
  5. 関数の入り口と出口は決まったパターン(プロローグ: push rbp / mov rbp, rsp、エピローグ: pop rbp / ret
  6. 即値に # が付くのは ARM だけ。x86 Intel 構文では付かない
  7. 全部読まなくてよい。着目している変数が入っているレジスタだけ追うと理解が速い
AsmWalker での使い方:
サンプルを選んでコンパイルし、ステップ実行しながらこのページを横に置いておくと、 命令を1つずつ確認しながら読み進めることができます。 命令詳細パネルの「フル命令名」と「構文フォーマット」も参考にしてください。