레지스터와 데이터 이동
레지스터 개요
x86-64에는 16개의 범용 64비트 레지스터가 있습니다. 32비트 서브레지스터(예: eax)에 쓰면 전체 레지스터의 상위 32비트가 0으로 확장됩니다. 16/8비트 서브레지스터에 쓰면 상위 비트는 변경되지 않습니다.
; 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)를 지정하세요.
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], raxlea — 유효 주소 로드
lea는 메모리 피연산자의 주소를 계산하지만 메모리에 접근하지 않습니다. 빠른 산술 명령(스케일+인덱스로 5/3/9배 등)과 스택 변수나 데이터 레이블의 주소를 가져오는 데 자주 쓰입니다.
; 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 slotpush / 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) 저장에 쓰입니다.
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 1xchg / xadd — 원자적 교환
메모리 피연산자를 가진 xchg는 항상 원자적입니다(암시적 잠금). xadd는 교환과 덧셈을 결합합니다. lock cmpxchg(비교교환)와 함께 잠금 없는 동기화 프리미티브의 기초를 형성합니다.
; 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 acquiredmovzx / movsx — 0/부호 확장
movzx는 작은 값을 큰 레지스터로 0 확장합니다(부호 없는 로드). movsx는 부호 확장합니다(부호 있는 로드). movsxd로 32비트 부호 있는 값을 64비트 레지스터로 로드하세요.
; 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)산술과 논리
add / sub — 정수 산술
add/sub는 CF, OF, SF, ZF, PF, AF를 설정합니다. jc/jo로 부호 없는/부호 있는 오버플로를 감지하세요. rsp에서 빼서 스택 공간을 할당합니다 — ABI에 따라 호출 전 rsp를 16바이트 정렬로 유지하세요.
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 — 곱셈과 나눗셈
두 피연산자 imul이 일반적입니다 — 하위 64비트만 유지합니다. 전체 너비 곱셈은 단일 피연산자 mul/imul(결과는 rdx:rax)을 사용하세요. idiv 전에 cqo로 피제수의 부호를 확장하세요.
; 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 = remainderinc / dec / neg
inc와 dec는 캐리 플래그(CF)를 업데이트하지 않습니다 — CF에 의존하는 다중 정밀도 덧셈/뺄셈 체인에서 사용할 수 있습니다. ZF/SF/OF는 업데이트합니다. neg는 2의 보수 부정을 계산하고 피연산자가 0이 아니면 CF=1을 설정합니다.
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 == 0and / or / xor / not / test
xor reg,reg는 레지스터를 0으로 만드는 관용적 방법입니다(mov reg,0보다 짧은 인코딩). test a,a는 and a,a와 동일하지만 결과를 버립니다. and/or/xor는 CF와 OF를 지웁니다.
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는 논리 시프트(0 채우기)이고, sar는 부호 비트를 보존합니다(산술 시프트). 2의 거듭제곱으로 곱셈/나눗셈하는 것이 imul/idiv보다 훨씬 빠릅니다. 시프트 카운트는 5 또는 6비트로 마스킹됩니다.
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제어 흐름과 분기
cmp / test — 비교
cmp는 저장하지 않고 뺄셈처럼 플래그를 설정합니다. 부호 있는 비교에는 je/jne/jl/jg/jle/jge, 부호 없는 비교에는 jb/ja/jbe/jae를 사용하세요. 혼동은 전형적인 버그입니다 — jl은 SF!=OF를, jb는 CF를 검사합니다.
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 unsignedjmp — 무조건 점프
jmp는 무조건 점프를 수행합니다. 조건 점프(jcc)는 cmp/test/산술로 설정된 플래그를 검사합니다. 64비트 모드에서 근접 조건 점프는 ±2GB에 도달합니다. 간접 점프로 switch/case 점프 테이블을 구현합니다.
; 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) vs 부호 없는(b/a) 구분이 중요합니다. 부호 있는 정수 비교 후에는 부호 있는 형식을 사용하세요.
; 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를 생성합니다.
; 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 .loopcmov — 조건부 이동
cmovcc는 조건부 이동을 수행합니다 — 분기 예측 실패 페널티를 피하는 분기 없는 코드입니다. 대상은 레지스터여야 합니다. cmov는 항상 두 소스 피연산자를 평가하므로 부작용이 있는 경로에서는 피하세요.
; 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|스택, 함수와 호출 규약
함수 프롤로그와 에필로그
표준 프롤로그는 rbp를 저장하고, rbp를 프레임 포인터로 설정하고, rsp에서 빼서 지역 변수를 할당합니다. leave는 1바이트 에필로그로 mov rsp,rbp; pop rbp와 동일합니다. System V AMD64 ABI는 호출 지점에서 rsp 16바이트 정렬을 요구합니다.
; 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
retcall / ret — 호출과 복귀
call은 반환 주소(64비트에서 8바이트)를 스택에 푸시하고 대상으로 점프하며, ret는 rip로 팝합니다. 꼬리 호출에는 jmp를 사용하여 피호출자가 직접 호출자에게 반환하게 하세요 — 스택을 보존하고 꼬리 호출 최적화를 가능하게 합니다.
; 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에는 레드 존이 없습니다. 리프 함수는 종종 프롤로그조차 필요 없습니다.
; 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에서 직접 지역 변수를 참조합니다.
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
retcallee-saved 레지스터 보존
callee-saved 레지스터(rbx, rbp, r12-r15)는 보존되어야 합니다 — 진입 시 push, 출구 시 역순으로 pop. caller-saved(rax, rcx, rdx, rsi, rdi, r8-r11)는 자유롭게 사용할 수 있지만 호출 간 값이 필요하면 직접 저장하세요.
; 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시스템 콜과 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을 파괴합니다.
; 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 시스템 콜로 종료됩니다.
; 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를 받습니다.