Skip to content

Assembly チートシート

システムプログラミング、パフォーマンス最適化、ベアメタルプログラミング向けの低レベル x86-64 アセンブリ言語(Intel/NASM 構文)。

01

レジスタとデータ転送

レジスタ概要

x86-64 には 16 個の汎用 64 ビットレジスタがあります。32 ビットサブレジスタ(例: eax)への書き込みは完全レジスタの上位 32 ビットをゼロ拡張します。16/8 ビットサブレジスタへの書き込みは上位ビットを変更しません。

assembly
; General-purpose 64-bit registers (x86-64)
; rax, rbx, rcx, rdx         — main accumulators / scratch
; rsi, rdi                    — source/destination index (string ops)
; rbp, rsp                    — base pointer / stack pointer
; r8  .. r15                  — extended registers
; 32-bit: eax, 32-bit: ecx, 16-bit: ax, 8-bit: al/ah

mov rax, 42        ; load immediate into 64-bit register
mov eax, 0x1F      ; 32-bit move (zero-extends to rax)

mov — データ転送

mov はレジスタ、メモリ、即値の間でデータをコピーします。メモリ間転送は不可——レジスタを経由してください。オペランドサイズが曖昧な場合は必ずサイズ(byte/word/dword/qword)を指定します。

assembly
mov rax, 60          ; immediate  -> register
mov rbx, rax         ; register   -> register
mov qword [rsp-8], 7 ; immediate  -> memory (qword = 8 bytes)
mov rcx, [rsp-8]     ; memory     -> register

; INVALID: mov [rsp-8], [rsp-16]  (memory-to-memory not allowed)
; Use two steps: mov rax, [src]; mov [dst], rax

lea — 実効アドレスのロード

lea はメモリオペランドのアドレスを計算しますがメモリにはアクセスしません。高速な算術命令(スケール+インデックスで 5/3/9 倍など)や、スタック変数やデータラベルのアドレス取得に多用されます。

assembly
; Compute address without dereferencing
lea rax, [rbx + rcx*8 + 16]   ; rax = rbx + rcx*8 + 16

; Common idiom: fast arithmetic (no memory access)
lea rax, [rax + rax*4]        ; rax *= 5  (rax = rax + rax*4)

; Pointer into a buffer
lea rsi, [buffer]             ; rsi = address of buffer
lea rdi, [rsp + 32]           ; rdi = address of stack slot

push / pop — スタック操作

push は rsp を 8 減らしてから値を格納し、pop はロードしてから rsp を 8 増やします。System V AMD64 ABI では最初の 6 つの整数引数は rdi、rsi、rdx、rcx、r8、r9 に入ります。push は主に callee-saved レジスタ(rbx、rbp、r12-r15)の保存に使われます。

assembly
push rax       ; rsp -= 8; [rsp] = rax
push qword 42  ; push immediate
pop rbx        ; rbx = [rsp]; rsp += 8

; Save/restore callee-saved registers
push rbx
push r12
; ... function body ...
pop r12
pop rbx
ret

; Push arguments in reverse (C calling convention)
push 3
push 2
push 1

xchg / xadd — アトミック交換

メモリオペランドを持つ xchg は常にアトミックです(暗黙のロック)。xadd は交換と加算を組み合わせます。lock cmpxchg(比較交換)とともに、これらはロックフリー同期プリミティブの基礎をなします。

assembly
; xchg swaps two operands (implicitly LOCKed with memory)
xchg rax, rbx          ; swap register/register
xchg [counter], rcx    ; swap memory/register (atomic)

; xadd: swap then add (returns old value in src)
; lock xadd [counter], rax   ; atomic fetch-and-add

; Spinlock idiom
spin:
  xor eax, eax
  lock cmpxchg [lock_var], 1  ; if [lock_var]==0, set to 1
  jnz spin                     ; retry if not acquired

movzx / movsx — ゼロ/符号拡張

movzx は小さな値を大きなレジスタにゼロ拡張します(符号なしロード)。movsx は符号拡張します(符号付きロード)。movsxd で 32 ビット符号付き値を 64 ビットレジスタにロードします。

assembly
; movzx: zero-extend (unsigned)
movzx rax, byte [rsi]    ; load byte, zero-extend to 64-bit
movzx eax, word [rdi]    ; load word, zero-extend to 32-bit

; movsx: sign-extend (signed)
movsx rax, byte [rsi]    ; sign-extend byte to 64-bit
movsx rax, dword [rdi]   ; sign-extend 32-bit to 64-bit (movsxd)
02

算術と論理

add / sub — 整数算術

add/sub は CF、OF、SF、ZF、PF、AF を設定します。jc/jo で符号なし/符号付きオーバーフローを検出します。rsp からの減算でスタック領域を確保できます——ABI に従いコール前に rsp を 16 バイト境界に保ってください。

assembly
add rax, rbx      ; rax = rax + rbx
add rax, 10       ; rax = rax + 10
sub rcx, rdx      ; rcx = rcx - rdx
sub rsp, 32       ; allocate 32 bytes of stack (align!)

; 64-bit addition with carry check
add rax, rbx
jc .overflow      ; jump if unsigned overflow (CF=1)

imul / idiv — 乗算と除算

2 オペランド imul が一般的な形式で、下位 64 ビットのみを保持し、コンパイラが a*b に出力します。全幅乗算には 1 オペランド mul/imul(結果は rdx:rax)。idiv の前に cqo で被除数を符号拡張します。

assembly
; Two-operand imul (most common): dst = dst * src
imul rax, rbx        ; rax = rax * rbx (lower 64 bits)
imul rcx, 10         ; rcx = rcx * 10

; One-operand: rdx:rax = rax * src (full 128-bit)
mul  rbx             ; unsigned: rdx:rax = rax * rbx
imul rbx             ; signed:   rdx:rax = rax * rbx

; idiv: signed divide rdx:rax by src
; Must sign-extend rax into rdx:rax first!
cqo                  ; sign-extend rax -> rdx:rax
idiv rcx             ; rax = rdx:rax / rcx, rdx = remainder

; Unsigned: use div (zero rdx first)
xor rdx, rdx
div  rcx             ; rax = rdx:rax / rcx, rdx = remainder

inc / dec / neg

inc と dec はキャリーフラグ(CF)を更新しません——CF に依存する多精度加減算チェーン内で使用できます。ZF/SF/OF は更新します。neg は 2 の補数否定を計算し、オペランドが 0 でなければ CF=1 を設定します。

assembly
inc rax          ; rax++  (does NOT affect CF!)
dec rcx          ; rcx--  (does NOT affect CF!)
neg rdx          ; rdx = -rdx  (two's complement negate)

; Common loop pattern
mov rcx, 10
.loop:
  ; ... loop body ...
  dec rcx
  jnz .loop       ; repeat until rcx == 0

and / or / xor / not / test

xor reg,reg はレジスタをゼロにする慣用的な書き方です(mov reg,0 より短いエンコード)。test a,a は and a,a と同等ですが結果を破棄します。and/or/xor は CF と OF をクリアします。

assembly
and rax, 0xFF    ; mask low byte (rax &= 0xFF)
or  rcx, 0x10    ; set bit 4
xor rdx, rdx     ; rdx = 0 (idiomatic zeroing)
xor rax, rax     ; clear rax (shorter than mov rax,0)
not r8           ; bitwise NOT (one's complement)
test rax, rax    ; set flags from rax & rax (checks zero/sign)
test rcx, 0x1    ; test if low bit set (odd/even check)

shl / shr / sar — シフト

shl/shr は論理シフト(ゼロ埋め)、sar は符号ビットを保持します(算術シフト)。2 の冪による乗除は imul/idiv より高速です。シフトカウントは 32 ビットで 5 ビット、64 ビットで 6 ビットにマスクされます。

assembly
shl rax, 4       ; logical left shift  (rax *= 16)
shr rax, 3       ; logical right shift (unsigned rax /= 8)
sar rax, 3       ; arithmetic right shift (signed rax /= 8)

; Rotate (carry not involved)
rol rax, 4       ; rotate left
ror rax, 4       ; rotate right

; Shift by CL (only low 5 bits used in 64-bit mode)
mov cl, 4
shl rax, cl      ; shift left by 4
03

制御フローと分岐

cmp / test — 比較

cmp は結果を格納せずに減算のようにフラグを設定します。符号付き比較には je/jne/jl/jg/jle/jge、符号なしには jb/ja/jbe/jae を使います。混同は典型的なバグです——jl は SF!=OF を、jb は CF をチェックします。

assembly
cmp rax, rbx     ; compute rax - rbx, set flags (discard result)
cmp rax, 10      ; compare with immediate

; Signed comparisons
cmp rax, rbx
je  .equal       ; jump if rax == rbx        (ZF=1)
jl  .less        ; jump if rax <  rbx signed (SF!=OF)
jg  .greater     ; jump if rax >  rbx signed (ZF=0 and SF==OF)
jle .le          ; jump if rax <= rbx signed
jge .ge          ; jump if rax >= rbx signed

; Unsigned comparisons (use 'below'/'above' mnemonics)
cmp rax, rbx
jb  .below       ; rax <  rbx unsigned (CF=1)
ja  .above       ; rax >  rbx unsigned (CF=0 and ZF=0)
jbe .be          ; rax <= rbx unsigned
jae .ae          ; rax >= rbx unsigned

jmp — 無条件ジャンプ

jmp は無条件ジャンプを実行します。条件ジャンプ(jcc)は cmp/test/算術で設定されたフラグをテストします。64 ビットモードでは近傍条件ジャンプは ±2GB に届きます。間接ジャンプで switch/case ジャンプテーブルを実現します。

assembly
; Direct jump to a label
jmp .end

; Conditional jumps (short/near, 64-bit allows near)
.loop:
  dec rcx
  jnz .loop        ; jump if ZF=0 (rcx != 0)

; Indirect jump through register/memory (jump table)
lea rax, [table]
mov rdi, [rax + rbx*8]
jmp rdi            ; jump to address in rdi

; Jump table example (switch statement)
table: dq .case0, .case1, .case2, .case3

一般的な条件ジャンプ

多くの同義語があります(je==jz、jb==jc==jnae)。符号付き(l/g)と符号なし(b/a)の区別が重要です。符号付き整数の比較後には符号付き形式を、ポインタ/符号なし整数の比較後には符号なし形式を使います。

assembly
; After cmp/test:
je  / jz   ; jump if equal / zero        (ZF=1)
jne / jnz  ; jump if not equal / nonzero (ZF=0)

; Signed
jl / jnge  ; less            (SF!=OF)
jge / jnl  ; greater-or-equal (SF==OF)
jle / jng  ; less-or-equal   (ZF=1 or SF!=OF)
jg  / jnle ; greater         (ZF=0 and SF==OF)

; Unsigned
jb / jnae / jc  ; below / carry       (CF=1)
jae / jnb / jnc ; above-or-equal      (CF=0)
jbe / jna       ; below-or-equal      (CF=1 or ZF=1)
ja  / jnbe      ; above               (CF=0 and ZF=0)

; Special
js  ; jump if sign     (SF=1)
jns ; jump if not sign (SF=0)
jo  ; jump if overflow (OF=1)
jno ; jump if no overflow (OF=0)

loop 命令

loop は rcx をデクリメントし、rcx!=0 ならジャンプします。便利ですが、现代の CPU では dec/jnz ペアより遅いため、コンパイラは dec/jnz を出力します。

assembly
; loop: dec rcx then jump if rcx != 0
mov rcx, 5
.loop:
  ; ... body executes 5 times ...
  loop .loop

; loope/loopz: loop while equal/zero
; loopne/loopnz: loop while not equal/not zero

; Modern compilers prefer dec + jnz (loop is slower on many CPUs)
mov rcx, 5
.loop:
  ; ...
  dec rcx
  jnz .loop

cmov — 条件付き移動

cmovcc は条件付き移動を実行し、分岐予測ミスペナルティを回避する分岐なしコードです。宛先はレジスタでなければなりません。cmov は常に両方のソースオペランドを評価するため、副作用があるパスやフォールする可能性のあるメモリアクセスでは避けてください。

assembly
; cmovcc: move only if condition is true (branchless)
cmp rax, rbx
cmovl rax, rbx    ; if rax < rbx (signed), rax = rbx  -> rax = min(a,b)

; max(a, b)
cmp rax, rbx
cmovl rax, rbx    ; rax = max(rax, rbx)? No: if rax<rbx, set rax=rbx -> max

; Branchless abs:
; abs(x): mask = x >> 63; result = (x ^ mask) - mask
mov rax, rdi
sar rdi, 63       ; all 1s if negative, 0 if positive
xor rax, rdi
sub rax, rdi      ; rax = |original rdi|
04

スタック、関数と呼び出し規約

関数プロローグとエピローグ

標準プロローグは rbp を保存し、rbp をフレームポインタに設定し、rsp から減算してローカル変数を確保します。leave は 1 バイトのエピローグで mov rsp,rbp; pop rbp と同等です。System V AMD64 ABI はコール時点で rsp の 16 バイト整列を要求します。

assembly
; System V AMD64 calling convention (Linux/macOS)
; Args: rdi, rsi, rdx, rcx, r8, r9 (then stack)
; Return: rax. Callee-saved: rbx, rbp, r12-r15

my_func:
    push rbp
    mov  rbp, rsp        ; standard prologue
    sub  rsp, 32         ; allocate locals (keep 16-byte aligned)

    ; ... function body ...

    mov  rsp, rbp        ; or: leave
    pop  rbp             ; standard epilogue
    ret

; Compact form using leave
my_func2:
    push rbp
    mov  rbp, rsp
    sub  rsp, 16
    ; ...
    leave               ; mov rsp,rbp; pop rbp
    ret

call / ret — 呼び出しと復帰

call はリターンアドレス(64 ビットで 8 バイト)をスタックにプッシュしてターゲットにジャンプし、ret はそれを rip にポップします。末尾呼び出しには jmp を使うと、被呼び出しが直接呼び出し元に復帰し、スタックを保持して末尾呼び出し最適化が可能です。

assembly
; call pushes return address, then jumps
call my_func

; ret pops return address into rip
my_func:
    ; ...
    ret

; Call with arguments (System V AMD64)
mov rdi, 1     ; 1st arg
mov rsi, 2     ; 2nd arg
mov rdx, 3     ; 3rd arg
call add_three ; result in rax

; Tail call: jmp instead of call+ret
my_wrapper:
    jmp target_func   ; reuses our return address

葉関数とレッドゾーン

System V AMD64 ABI は rsp の下方 128 バイトの「レッドゾーン」を予約し、葉関数(呼び出しを行わない関数)は rsp を調整せずに使用できます。Windows x64 にはレッドゾーンがありません。葉関数は多くの場合プロローグすら不要です。

assembly
; Leaf function (no calls) can use the red zone:
; 128 bytes below rsp that won't be clobbered by signals/interrupts
leaf_sqrt_sum:
    ; rdi, rsi = args, rax = result
    mov rax, rdi
    add rax, rsi
    ret             ; no prologue needed!

; Non-leaf functions MUST save rsp properly because
; a call would write into the red zone.
; Red zone is NOT honored on Windows x64.

ローカル変数とスタックフレーム

フレームポインタ(rbp)がある場合、ローカル変数は [rbp - offset] で、スタック引数は [rbp + offset] でアクセスします。コンパイラはしばしばフレームポインタを省略し、rsp から直接ローカルを参照します。

assembly
my_func:
    push rbp
    mov  rbp, rsp
    sub  rsp, 32          ; 32 bytes for locals

    ; Local variables accessed via [rbp - offset]
    mov qword [rbp-8],  10   ; local1
    mov qword [rbp-16], 20   ; local2

    ; Read arguments (also via rbp once saved on stack,
    ; or directly from registers)
    mov rax, [rbp-8]
    add rax, [rbp-16]

    leave
    ret

callee-saved レジスタの保存

callee-saved レジスタ(rbx、rbp、r12-r15)は関数内で保存する必要があります——入口で push、出口で逆順で pop。caller-saved レジスタ(rax、rcx、rdx、rsi、rdi、r8-r11)は自由に使用できますが、コールをまたいで値が必要な場合は自分で保存します。

assembly
; rbx, rbp, r12, r13, r14, r15 are callee-saved
; rax, rcx, rdx, rsi, rdi, r8-r11 are caller-saved

my_func:
    push rbx          ; we want to use rbx
    push r12          ; and r12

    mov rbx, rdi      ; use them
    mov r12, rsi

    ; ... do work, may call other functions ...
    ; (those calls will preserve rbx/r12 for us)

    mov rax, rbx      ; prepare return value

    pop r12           ; restore in REVERSE order
    pop rbx
    ret
05

システムコールと Hello World

syscall — Linux システムコール

Linux x86-64 では、syscall 命令で番号を rax に、最大 6 つの引数を rdi、rsi、rdx、r10、r8、r9 に設定します(第 4 引数は rcx ではなく r10)。一般的な番号:write=1、read=0、exit=60、mmap=9。syscall は rcx と r11 を破壊します。macOS は異なる番号を使用します。

assembly
; Linux x86-64 syscall convention:
; rax = syscall number
; rdi, rsi, rdx, r10, r8, r9 = args (NOTE: r10 not rcx!)
; return value in rax; clobbers rcx and r11

; write(1, msg, 12)
mov rax, 1          ; syscall: write
mov rdi, 1          ; fd = stdout
lea rsi, [msg]      ; buf
mov rdx, 12         ; count
syscall

; exit(0)
mov rax, 60         ; syscall: exit
xor rdi, rdi        ; status = 0
syscall

section .data
msg: db "hello world", 10   ; 12 bytes with newline

完全な Hello World(NASM, Linux)

スタンドアロンの hello world は libc にリンクせず、syscall で直接カーネルを呼び出します。$ - msg はアセンブル時に長さを計算します。_start は ld のデフォルトエントリポイントです。プログラムは exit システムコールで終了します。

assembly
; nasm -f elf64 hello.asm && ld hello.o -o hello && ./hello
section .data
    msg:     db "Hello, World!", 10
    msg_len: equ $ - msg          ; length computed at assemble time

section .text
    global _start

_start:
    ; write(1, msg, msg_len)
    mov rax, 1          ; write
    mov rdi, 1          ; stdout
    mov rsi, msg
    mov rdx, msg_len
    syscall

    ; exit(0)
    mov rax, 60
    xor rdi, rdi
    syscall

コマンドライン argc / argv の読み取り

_start への入口で(Linux、libc なし)、カーネルは argc を [rsp] に、続いて argv ポインタ、envp、NULL を配置します。libc にリンクして main を使う場合、C ランタイムがこれらを解析し、main は rdi/rsi で argc と argv を受け取ります。

assembly
; At _start the stack looks like:
;   [rsp]      = argc
;   [rsp+8]    = argv[0]
;   [rsp+16]   = argv[1]
;   ...
;   [rsp + 8*(argc+1)] = NULL

_start:
    mov rdi, [rsp]           ; argc
    mov rsi, [rsp+8]         ; argv[0] (program name)
    mov rdx, [rsp+16]        ; argv[1] (first user arg)

    ; Loop over argv
    mov rbx, rsp
    add rbx, 8               ; skip argc, point at argv[0]
.loop:
    mov rax, [rbx]
    test rax, rax
    jz .done                 ; NULL terminator
    ; rax -> one argv string
    add rbx, 8
    jmp .loop
.done:
    ; exit
    mov rax, 60
    xor rdi, rdi
    syscall

C ライブラリ関数の呼び出し

C 関数を呼び出す際、call の前に rsp を 16 バイトに整列させます。printf などの可変引数関数では、rax にベクタ(XMM)レジスタ引数の数を設定します——整数のみの呼び出しでは 0 にします。main をエントリポイントとして使うと、C ランタイムが libc をセットアップします。

assembly
; Link with gcc: nasm -f elf64 demo.asm && gcc demo.o -o demo -no-pie
extern printf

section .data
    fmt:  db "sum = %d", 10, 0
section .text
    global main

main:
    push rbp
    mov  rbp, rsp
    sub  rsp, 16                ; keep stack 16-byte aligned

    ; printf("sum = %d\n", 42)
    ; Variadic: rdi=fmt, rsi=arg, rax=#vector regs (0)
    lea  rdi, [fmt]
    mov  rsi, 42
    xor  rax, rax               ; 0 floating-point args
    call printf

    xor  rax, rax               ; return 0
    leave
    ret

Was this helpful?