동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성됩니다.
mov eax, 3
opcode | operand1 | operand2
피연산자에는 총 3가지 종류가 올 수 있습니다.
메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자 TYPE PTR이 추가될 수 있습니다. 여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정합니다.
👇메모리 피연산자의 예
QWORD PTR [0x8048000]: 0x8048000의 데이터를 8바이트만큼 참조
DWORD PTR [0x8048000]: 0x8048000의 데이터를 4바이트만큼 참조
WORD PTR [rax]: rax가 가르키는 주소에서 데이터를 2바이트만큼 참조
데이터 이동 명령어는 어떤 값을 레지스터나 메모리에 옮기도록 지시합니다.
mov dst, src : src에 들어있는 값을 dst에 대입
mov rdi, rsi: rsi의 값을 rdi에 대입
mov QWORD PTR[rdi], rsi: rsi의 값을 rdi가 가리키는 주소에 대입
mov QWORD PTR[rdi+8*rcx], rsi: rsi의 값을 rdi+8*rcx가 가리키는 주소에 대입
lea dst, src : src의 유효 주소(Effective Address, EA)를 dst에 저장합니다
lea rsi, [rbx+8*rcx]: rbx+8*rcx를 rsi에 대입
산술 연산 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시합니다. 곱셈과 나눗셈은 여기서 설명하지 않겠습니다.
add dst, src : dst에 src의 값을 더합니다.
add eax, 3: eax += 3
add ax, WORD PTR[rdi]: ax += *(WORD*)rdi
sub dst, src : dst에서 src의 값을 뺍니다.
sub eax, 3: eax -= 3
sub ax, WORD PTR[rdi]: ax -= *(WORD*)rdi
inc op : op의 값을 1 증가시킴
inc eax: eax++
dec op : op의 값을 1 감소시킴
dec eax: eax--
논리 연산 명령어는 and, or, xor, neg 등의 비트 연산을 지시합니다. 이 연산은 비트 단위로 이루어 집니다.
and dst, src : dst와 src의 비트가 모두 1이면 1, 아니면 0
eax = 0xffff0000 ebx = 0xcafebabe and eax, ebx Result : eax = 0xcafe0000
or dst, src : dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
eax = 0xffff0000 ebx = 0xcafebabe or eax, ebx Result : eax = 0xffffbabe
xor dst, src : dst와 src의 비트가 서로 다르면1, 같으면 0
eax = 0xffffffff ebx = 0xcafebabe xor eax, ebx Result : eax = 0x35014541
not op : op의 비트 전부 반전
eax = 0xffffffff not eax Result : eax = 0x00000000
비교 명령어는 두 피연산자의 값을 비교하고, 플래그를 설정합니다.
cmp op1, op2 : op1과 op2를 비교
cmp연산자는 두 피연산자를 빼서 대소 비교를 합니다. 연산의 결과는 op1에 대입하지 않습니다.1: mov rax, 0xA 2: mov rbx, 0xA 3: cmp rax, rbx ; ZF(Zero Flag) = 1
test op1, op2 : op1과 op2를 비교
test는 두 피연산자에 AND 비트연산을 취합니다. 연산의 결과는 op1에 대입하지 않습니다.1: xor rax, rax ; rax를 0으로 초기화 2: test rax, rax ; ZF = 1
분기 명령어는 rip를 이동시켜 실행 흐름을 바꿉니다.
(rip는 실행중인 다음 명령어의 메모리 주소를 가리키는 레지스터입니다.)
❗분기문은 여기 소개된 것 외에도 굉장히 많은 수가 존재합니다. 그러나 몇 가지만 살펴보면 이름을 통해 직관적으로 의미를 파악할 수 있기 때문에 전부 다루지 않겠습니다.
jmp addr : addr로 rip를 이동시킵니다.
1: xor rax, rax 2: jmp 1 ; jump to 1
je addr : 직전에 비교한 두 피연산자가 같으면 점프 ( jump if equal)
1: mov rax, 0xcafebabe 2: mov rbx, 0xcafebabe 3: cmp rax, rbx ; rax == rbx 4: je 1 ; jump to 1
jg addr : 직전에 비교한 두 연산자 중 전자가 더 크면 점프 ( jump if greater)
1: mov rax, 0x31337 2: mov rbx, 0x13337 3: cmp rax, rbx ; rax > rbx 4: jg 1 ; jump to 1
x64아키텍처에서는 다음의 명령어로 스택을 조작할 수 있습니다
push val : val을 스택 최상단에 쌓음
👇연산rsp -= 8 [rsp] = val
pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
👇연산rsp += 8 reg = [rsp-8]
컴퓨터 과학에서 프로시저(Procedure)는 특정 기능을 수행하는 코드 조각을 말합니다. 프로시저를 사용하면 반복되는 연산을 프로시저 호출로 대체할 수 있어서 전체 코드의 크기를 줄일 수 있으며, 기능별로 코드 조각에 이름을 붙일 수 있게 되어 코드의 가독성을 크게 높일 수 있습니다.
프로시저를 부르는 행위를 호출(Call)이라 부르며, 프로시저에서 돌아오는 것을 반환(Return) 이라고 부릅니다. 프로시저를 호출할 때는 프로시저를 실행하고 나서 원래의 실행 흐름으로 돌아와야 하므로, call 다음의 명령어 주소(Return Address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시킵니다.
call addr : addr에 위치한 프로시저 호출
👇연산push return_address jmp addr
leave : 스택프레임 정리
👇연산mov rsp, rbp pop rbp
ret : return address로 반환
👇연산pop rip
x64 아키텍처에서는 시스템 콜을 위해 syscall 명령어가 있습니다.
syscall
요청 : rax
인자 순서 : rdi -> rsi-> rdx -> rcx -> r8 -> r9 -> stack