アセンブラは難しそうに見えますが、基本的なルールを押さえると読めるようになります。C言語と比べながら、代表的な命令を一つずつ確認しましょう。
アセンブラの1行は ニモニック(命令名) と オペランド(対象) から成ります。
ニモニックは命令の略称です。MOV は MOVE(移動)、ADD は ADD(加算)、STR は STORE(格納)のように、英単語の略になっています。
MOV=Move, CMP=Compare, LDR=Load Register, STR=Store Register, BL=Branch with Link(サブルーチン呼び出し)。
実際の命令は1行ではなく、メモリ上では複数バイトのバイト列として格納されています。命令の種類によってバイト数が変わります。
| アーキテクチャ | 実際の命令長 |
|---|---|
| ARM Thumb / Thumb-2 | 2バイトまたは4バイトが混在 |
| x86-64 | 1〜15バイト(命令によって可変) |
このツールが扱う2つのアーキテクチャはどちらも 「左 ← 右」 の順です。
| アーキテクチャ | 例 | 読み方 |
|---|---|---|
| x86-64(Intel構文) | mov rax, 5 |
rax に 5 を入れる |
| ARM | mov r0, #5 |
r0 に 5 を入れる |
gcc -S を使うと AT&T 構文が出力され、順序が逆(右 → 左)になります。
movl $5, %eax は「eax に 5 を入れる」で意味は同じですが読む方向が違います。
AsmWalker は Intel 構文(-masm=intel)を採用しているため、左が行き先で統一されています。
レジスタは CPU の中にある超高速な変数です。C言語の変数はメモリに置かれますが、レジスタは CPU チップ上にあるため、メモリアクセスより桁違いに速いです。
| 64bit 名 | 32bit 名 | 16bit 名 | 8bit 名 | 主な用途 |
|---|---|---|---|---|
rax | eax | ax | al | 計算・関数戻り値 |
rbx | ebx | bx | bl | 汎用 |
rcx | ecx | cx | cl | カウンタ・第4引数 |
rdx | edx | dx | dl | 乗除算・第3引数 |
rdi | edi | di | dil | 第1引数 |
rsi | esi | si | sil | 第2引数 |
rsp | esp | — | — | スタックポインタ |
rbp | ebp | — | — | フレームポインタ |
rax(64bit)の下位32bitが eax、下位16bitが ax、下位8bitが al です。
int(32bit)型の値を扱うとき GCC は eax を使います。
eax に書き込むと上位32bitは自動的にゼロになります。
| 名前 | 別名 | 主な用途 |
|---|---|---|
r0 | — | 第1引数 / 関数戻り値 |
r1 | — | 第2引数 |
r2 | — | 第3引数 |
r3 | — | 第4引数 |
r4〜r11 | — | 汎用(呼び出し先保存) |
r13 | sp | スタックポインタ |
r14 | lr | リンクレジスタ(戻り先アドレス) |
r15 | pc | プログラムカウンタ |
最も頻出の命令です。レジスタ間のコピー、即値のセット、メモリからの読み書きに使います。
mov r0, #3 ; r0 = 3(即値)
mov r1, r0 ; r1 = r0(レジスタコピー)
ARM では即値に # を付ける。
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 に対応
rax ← 3 のような表記で値の流れが示されます。
同時にレジスタパネルで変化したレジスタが白くハイライトされます。
2つの値を加減算して結果を第1オペランドに書き戻します。
add r0, r1, r2 ; r0 = r1 + r2
sub r0, r0, #1 ; r0 = r0 - 1
ARM は3オペランド形式が多い(行き先を明示)。
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
eax ← eax(3) + edx(5) = 8 のように
計算前後の値が表示されます。
レジスタはCPU内の一時置き場なので、変数の値を永続的に保持するにはメモリへの読み書きが必要です。
ldr r0, [r7, #4] ; r0 = メモリ[r7+4]
[ ] の中がメモリアドレス。ベースレジスタ+オフセット。
mov eax, DWORD PTR [rbp-4]
; eax = メモリ[rbp-4](4バイト読み出し)
DWORD PTR は4バイト幅を指定。
str r0, [r7, #4] ; メモリ[r7+4] = r0
LDR と逆。r0 の値をメモリに保存する。
mov DWORD PTR [rbp-4], eax
; メモリ[rbp-4] = eax(4バイト書き込み)
x86 は MOV1つでロードもストアも表現する。
[rbp-4] や [r7, #8] はアドレスの計算式です。
rbp-4 なら「rbp の値から 4 を引いたアドレス」を意味します。
これはローカル変数がフレームポインタからの固定オフセットに配置されているためです。
変数が増えるごとにオフセット(-4, -8, -12…)が増えていきます。
| 指定子 | サイズ | 対応するC型 |
|---|---|---|
BYTE PTR | 1バイト | char |
WORD PTR | 2バイト | short |
DWORD PTR | 4バイト | int, float |
QWORD PTR | 8バイト | long, double, ポインタ |
mov [...], reg(x86)のステップでは
スタックパネルの該当アドレスのセルがハイライトされ、値の変化が確認できます。
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 の後に続く条件ジャンプ命令がこのフラグを参照します。
| x86 | ARM | 条件 | フラグ |
|---|---|---|---|
je | beq | 等しい(==) | ZF=1 |
jne | bne | 等しくない(!=) | ZF=0 |
jl | blt | 小さい(<) | NF≠OF |
jg | bgt | 大きい(>) | ZF=0 かつ NF=OF |
jle | ble | 以下(<=) | ZF=1 または NF≠OF |
jge | bge | 以上(>=) | NF=OF |
; C: if (a > 0) { ... }
cmp eax, 0 ; a - 0 のフラグを計算
jle .L_else ; a <= 0 なら else へジャンプ
; ↑ if の中身
.L_else:
if (a > 0) の条件を 反転 して jle(以下ならスキップ)を生成することが多いです。
「条件が成立しないときにジャンプ」という発想で読むと理解しやすくなります。
PUSH はスタックに値を積み、POP は取り出します。スタックポインタ(SP / RSP)が自動的に動きます。
push {r7, lr} ; r7 と lr をスタックに保存
; sp -= 8
pop {r7, pc} ; r7 と pc を復元
; sp += 8
複数レジスタを一度に保存・復元できる。
push rbp ; [rsp-8] = rbp, rsp -= 8
pop rbp ; rbp = [rsp], rsp += 8
1レジスタずつ操作する。
PUSH/POP は関数のプロローグ・エピローグで必ず登場します。「呼び出し元のレジスタ値を壊さないように保存して、帰りに復元する」という用途です。
[ ] はメモリ参照の印。カッコの中はアドレスの計算式MOV=Move, LDR=Load Register)push rbp / mov rbp, rsp、エピローグ: pop rbp / ret)# が付くのは ARM だけ。x86 Intel 構文では付かない