← Blog

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)로 구성

피연산자는 해당 연산의 입력 값(레지스터, 상수, 메모리 주소 등)

image.png

명령어 예시 피연산자 개수
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

함수에서 반환하여 원래 실행하던 코드로 돌아오는 명령어

어셈블리어에서의 함수 선언

어셈블리어에서 함수를 만드는 과정

  1. 함수 시작 위치를 나타낼 라벨(Label)을 정의
  2. 스택 프레임이 필요한 경우, 함수 프롤로그를 통해 스택 프레임을 구성
  3. 함수 내부에서 실제 동작을 구현
  4. 함수 마지막에 함수 에필로그를 통해 스택 프레임을 해제하고, ret 명령어로 종료

x86

x86에선 cdecl 함수 호출 규약을 통해 함수를 선언

cdecl 호출 규약: 함수 인자를 스택으로 전달

x64

x64에선 SYSV 함수 호출 규약을 통해 함수를 선언

SYSV 호출 규약: 함수 인자를 레지스터로 전달

함수 호출 과정

x86

함수에 전달할 인자를 스택에 push 한 뒤 call 명령어로 함수를 호출함 → 함수가 끝난 뒤에는 호출자(Caller) 측에서 스택 정리

x64

  1. 함수 호출 시 인자를 레지스터(Register)에 저장한 뒤 call 명령어로 함수를 호출함
  2. 반환값은 rax 레지스터에 저장됨
  3. 추가 인자가 있거나 지역 변수가 많을 경우 스택(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