x86 어셈블리
Intel 문법 기준 x86-64 어셈블리의 기본 구조와 주요 명령어를 정리했습니다.
어셈블리어와 x86-64
어셈블리어
컴퓨터의 기계어와 치환되는 언어
CPU에 사용되는 ISA는 굉장히 다양함 → 이들의 종류만큼 많은 수의 어셈블리어가 존재
어셈블리어의 기본 규칙
대표적인 두 문법은 Intel 문법과 AT&T 문법임
- Intel 문법: NASM(Netwide Assembler)
- AT&T 문법: GAS(Gnu Assembler)
x86-64 어셈블리어
기본 구조
명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성
피연산자는 해당 연산의 입력 값(레지스터, 상수, 메모리 주소 등)

| 명령어 예시 | 피연산자 개수 |
|---|---|
| mov, eax 3 | 2 |
| push rbp | 1 |
| add rax, 0x30 | 2 |
| ret | 0 |
| imul rax, rdx, 0x3 | 3 |
명령어
| 데이터 이동(Data Transfer) | mov, lea |
| 산술 연산(Arithmetic) | inc, dec, add, sub |
| 논리 연산(Logical) | and, or, xor, not |
| 비교(Comparison) | cmp, test |
| 분기(Branch) | jmp, je, jg |
| 스택(Stack) | push, pop |
| 프로시져(Procedure) | call, ret, leave |
| 시스템 콜(System call) | syscall |
피연산자
메모리 피연산자와 크기 지정자
메모리 피연산자는 [] 형태로 표현하며, 대괄호 안에는 접근할 메모리 주소를 작성함
mov eax, [rdi]
위 코드는 rdi 가 가리키는 메모리 주소의 값을 읽어와 eax에 저장한다는 의미
크기 지정가자 필요한 이유
메모리는 단순히 데이터가 저장된 공간일 뿐, CPU는 해당 주소에서 몇 바이트를 읽어야 하는지 스스로 알지 못함
→ 메모리를 사용할 때는 데이터 크기를 명시하기 위해 크기 지정자를 사용함
크기 지정자 종류
| 크기 지정자 | 크기 |
|---|---|
| BYTE PTR | 1바이트 |
| WORD PTR | 2바이트 |
| DWORD PTR | 4바이트 |
| QWORD PTR | 8바이트 |
산술 연산과 논리 연산
add, sub, mul, div
- add: 덧셈
- sub: 뺄셈
- mul, imul: 곱셈
- div, idiv: 나눗셈
x86-64에서는 부호 없는 연산(mul, div)과 부호 있는 연산(imul, idiv)를 구분함
add - 덧셈
add 명령어는 두 피연산자를 더하는 명령어
add <destination>, <source>
<destination>에 <source>를 더한 결과를 <destination>에 대입한다고 해석하면 됨
sub - 뺄셈
sub 명령어는 두 피연산자를 빼는 연산
sub <destination>, <source>
<destination>에 <source>를 뺀 결과를 <destination>에 대입한다고 해석하면 됨
mul / imul - 부호 없는 곱셈 / 부호 있는 곱셈
mul 레지스터
mul 명령어는 암시적으로 RAX 레지스터를 첫 번째 피연산자로 사용함 → <source>에 레지스터 혹은 메모리 주소를 두 번째 피연산자로 사용함
- 8비트 x 8비트 AL 레지스터의 값과 <source> 피연산자의 값을 곱함 → 두 숫자의 연산 결과는 최대 16비트 이기때문에, AX(16비트) 레지스터에 보관함
- 16비트 x 16비트 AX 레지스터의 값과 <source> 피연산자의 값을 곱함 → 곱한 결과의 상위 16비트는 DX 레지스터, 하위 16비트는 AX 레지스터에 보관
- 32비트 x 32비트 EAX 레지스터의 값과 <source> 피연산자의 값을 곱함 → 곱한 결과의 상위 32비트는 EDX 레지스터, 하위 32비트는 EAX 레지스터에 보관
- 64비트 x 64비트 RAX 레지스터의 값과 <source> 피연산자의 값을 곱함 → 곱한 결과의 상위 64비트는 RDX 레지스터, 하위 64비트는 RAX 레지스터에 보관
imul 레지스터
imul 명령어는 부호 있는(signed) 정수에 대해 곱셈을 수행하는 명령어
mul은 피연산자가 1개만 올 수 있는 반면, imul은 최대 3개까지 올 수 있음
- 피연산자가 1개인 경우: mul과 유사하게 정해진 레지스터와 <source>를 곱한 결과를 보관함
- 피연산자가 2개인 경우: <destination>에 <source>를 곱한 결과를 <destination>에 대입함
- 피연산자가 3개인 경우: <source>와 <immediate>(즉시값)을 곱한 결과를 <destination>에 대입함
피연산자의 크기를 초과하여 데이터 손실이 발생하면 캐리 플래그(CF)와 오버플로우플래그(OF)를 1로 설정함
div / idiv - 부호 없는 나눗셈 / 부호 있는 나눗셈
div 명령어는 부호가 없는 나눗셈을 진행할 때 사용, idiv 명령어는 부호가 있는 나눗셈을 진행할 때 사용
나눗셈을 수행할 피제수의 크기에 따라 AX, DX:AX, EDX:EAX, RDX:RAX에 저장하고, 수행한 결과인 몫과 나머지를 각 크기에 따라 AL:DX, RAX:RDX에 저장함
inc, dec
레지스터나 메모리에 저장된 값을 1씩 증가/감소 시키는 명령어
연산(and, or, xor, not)
| and | 두 개의 비트가 모두 1일 때만 결과가 1 |
| or | 하나의 비트가 1이면 결과가 1 |
| xor | 두 개의 비트가 다를 때만 결과가 1 |
| not | 비트를 완전히 반전(Complement) |
데이터 저장과 이동
mov
데이터를 복사/이동하는 명령어
mov <destination>, <source>
source의 값을 destination에 대입
lea
메모리에 실제 접근을 하지 않고, 유효 주소를 저장하기 위해 사용
유효 주소: 연산의 대상이 되는 데이터가 저장된 위치
int arr[5] = {10,20,30,40,50};
int *ptr = &arr[2]; //유효 주소
mov rbx, &arr //배열 arr의 시작 주소를 rbx 레지스터에 복사
mov rcx, 2 //배열의 인덱스 값을 rcx 레지스터에 복사
lea rax, [rbx+rcx*4] //배열의 세 번째 원소를 가리키는 주소 값 rax = rbx + (rcx*4) = 1008
mov 명령어는 메모리에 직접 접근해 값을 가져오지만 lea 명령어는 메모리에 접근하는 것이 아닌 단순히 메모리 주소를 계산함
비교 연산과 분기문
비교
cmp
내부적으로 두 피연산자를 빼는 방식으로 플래그를 갱신
cmp rax, rbx
| rax | rbx | ZF | SF | OF |
|---|---|---|---|---|
| 10 | 20 | 0 | 1 | 0 |
| 20 | 10 | 0 | 0 | 0 |
| 10 | 10 | 1 | 0 | 0 |
| 0 | -10 | 1 | 0 | 0 |
| -10 | 0 | 0 | 1 | 0 |
| 9,223,372,036,854,775,807 (16진수로 0x7FFFFFFFFFFFFFFF) | -1 | 0 | 1 | 1 |
test
내부적으로 <destination>과 <source>를 AND 연산한 뒤, 결과는 버리고 플래그 레지스터만 갱신
분기
jmp
특정 주소로 rip를 바꾸는 명령어
아무 조건 필요 없이 무조건 지정된 주소로 점프하는 명령어
je(Jump if Equal) / jz(Jump if Zero)
ZF(Zero Flag)가 1이면 점프하는 명령어
jg(Jump if Greater) / jge(Jump if Greater or Equal)
jg: 두 피연산자 중 왼쪽 피연산자가 더 클 때 점프
| ZF | SF | OF | 점프 조건 (ZF=0 AND SF=OF) | 점프 발생 여부 |
|---|---|---|---|---|
| 0 | 0 | 0 | O | 점프 발생 |
| 0 | 1 | 0 | X | 점프 안함 |
| 1 | 0 | 0 | X | 점프 안함 |
| 0 | 1 | 1 | O | 점프 발생 |
jge: 두 피연산자 중 왼쪽 피연산자가 더 크거나 같을 때 점프
| SF | OF | 점프 조건 (SF=OF) | 점프 발생 여부 |
|---|---|---|---|
| 0 | 0 | O | 점프 발생 |
| 0 | 0 | O | 점프 발생 |
| 1 | 0 | X | 점프 안함 |
| 1 | 1 | O | 점프 발생 |
jl(Jump if Less) / jle(Jump if Less or Equal)
jl: 두 피연산자 중 왼쪽 피연산자가 더 작을 때 점프
| SF | OF | 점프 조건 (SF≠OF) | 점프 발생 여부 |
|---|---|---|---|
| 0 | 0 | X | 점프 안함 |
| 1 | 0 | O | 점프 발생 |
| 0 | 1 | O | 점프 발생 |
| 1 | 1 | X | 점프 안함 |
jle: 두 피연산자 중 왼쪽 피연산자가 더 작거나 같을 때 점프
| ZF | SF | OF | 점프 조건 (ZF=1 OR SF≠OF) | 점프 발생 여부 |
|---|---|---|---|---|
| 0 | 1 | 0 | O | 점프 발생 |
| 1 | 0 | 0 | O | 점프 발생 |
| 0 | 0 | 0 | X | 점프 안함 |
| 0 | 1 | 1 | X | 점프 안함 |
jne(Jump if Not Equal) / jnz(Jump if Not Zero)
두 피연산자가 다를 때 점프하는 명령어
반복문
반복문
특정 조건이 만족될 때까지 동일한 동작을 반복 실행하는 문법
어셈블리에서는 비교 연산(cmp)과 조건 분기 명령(jxx)을 조합하여 반복문을 구현함
반복문 종류
1. 카운터 기반 반복문(for 형태)
정해진 횟수만큼 반복하는 방식
mov rcx, 10
loop_start:
; 반복 코드
loop loop_start
- rcx 값을 자동 감소
- rcx가 0이 될 때가지 반복
2. 조건 기반 반복문(while 형태)
조건을 먼저 검사한 뒤 반복 수행
check_condition:
cmp rax, 50
jge loop_exit
; 반복 코드
jmp check_condition
loop_exit:
3. 후조건 검사 반복문(do-while 형태)
반복 코드를 최소 한 번 실행한 뒤 조건 검사
do_loop:
; 반복 코드
cmp rax, 10
jl do_loop
함수/프로시저
함수
어셈블리어에서 프로그램이 처리해야 할 명령어들을 한 덩어리로 모아 놓은 코드 블록
함수를 호출한 함수(Caller)는 Call 명령어로 함수를 불러서 사용하고, 호출된 함수(Callee)는 ret 명령어를 이용해 다시 이전 함수에서 실행 중이던 코드로 돌아감
스택 관련 명령어
스택
LIFO(Last In, First Out, 후입선출) 방식으로 동작하는 구조 → 가장 나중에 삽입된 데이터가 가장 먼저 꺼내지는 구조
데이터를 추가할 때마다 메모리 주소가 감소하며, 데이터를 제거하면 다시 증가하는 특성
스택의 가장 중요한 요소는 스택 포인터(Stack Pointer) 레지스터인 rsp임
rsp: 항상 스택의 가장 위를 가리킴
- push 명령어를 실행하면 rsp가 감소하면서 스택에 값이 저장됨
- pop 명령어를 실행하면 rsp가 증가하면서 스택에 저장되어 있던 값을 꺼낼 수 있음
push
스택에 값을 저장하는 명령어
pop
스택에서 값을 꺼내는 명령어
함수 호출 및 반환 관련 명령어
call
함수를 호출할 때 사용하는 명령어
leave
스택 프레임을 정리하는 명령어
ret
함수에서 반환하여 원래 실행하던 코드로 돌아오는 명령어
어셈블리어에서의 함수 선언
어셈블리어에서 함수를 만드는 과정
- 함수 시작 위치를 나타낼 라벨(Label)을 정의
- 스택 프레임이 필요한 경우, 함수 프롤로그를 통해 스택 프레임을 구성
- 함수 내부에서 실제 동작을 구현
- 함수 마지막에 함수 에필로그를 통해 스택 프레임을 해제하고, ret 명령어로 종료
x86
x86에선 cdecl 함수 호출 규약을 통해 함수를 선언
cdecl 호출 규약: 함수 인자를 스택으로 전달
x64
x64에선 SYSV 함수 호출 규약을 통해 함수를 선언
SYSV 호출 규약: 함수 인자를 레지스터로 전달
함수 호출 과정
x86
함수에 전달할 인자를 스택에 push 한 뒤 call 명령어로 함수를 호출함 → 함수가 끝난 뒤에는 호출자(Caller) 측에서 스택 정리
x64
- 함수 호출 시 인자를 레지스터(Register)에 저장한 뒤 call 명령어로 함수를 호출함
- 반환값은 rax 레지스터에 저장됨
- 추가 인자가 있거나 지역 변수가 많을 경우 스택(Stack)을 함께 사용함
| 구분 | x86 | x64 |
|---|---|---|
| 인자 전달 | 스택 사용 | 레지스터 사용 |
| 반환값 | eax | rax |
| 특징 | caller가 스택 정리 | 레지스터 기반으로 더 빠름 |
시스템 콜
Opcode: 시스템 콜
커널 모드(Kernel Mode)
운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한
사용자 모드(User Mode)
운영체제가 사용자에게 부여하는 권한으로, 접근할 수 있는 메모리 영역과 권한이 매우 한정되어 있고, 하드웨어에 직접적으로 접근할 수 없음
시스템 콜(System Call, syscall)
사용자 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용
ex. 사용자가 cat flag 명령어를 실행하면, 프로그램은 flag 파일을 읽기 위해 커널에 파일 접근을 요청함 → 사용자 모드(User Mode) 프로그램이 커널(Kernel)에 특정 작업을 요청하는 과정을 시스템 콜이라고 함
시스템 콜 사용
x86
- x86에서는 int 0x80 명령어를 사용해 시스템 콜을 호출함
- eax 레지스터에는 시스템 콜 번호를 저장
- 시스템 콜 인자는 레지스터를 통해 전달함 ebx → ecx → edx → esi → edi → ebp → …
- 시스템 콜 수행 후 반환값은 eax 레지스터에 저장
x64
- x64에서는 syscall 명령어를 사용해 시스템 콜을 호출함
- rax 레지스터에는 호출하고자 하는 시스템 콜의 번호를 저장
- 시스템 콜 인자는 rdi → rsi → rdx → r10 → r8 → r9 → 스택 순서로 전달
- 시스템 콜 수행 후 반환값은 rax 레지스터에 저장
Comments