앞서 CPU 아키텍쳐를 살펴본 바 있다. 다양한 명령어 집합구조가 있는 만큼 서로 다른 어셈블리어가 존재한다. 본 문서에서는 x64아키텍쳐를 대상으로 설명을 한다.
기본구조
x64 어셈블리 언어의 문법은 명령어(Operation Code; Opcode) 와 피연산자(Operand) 로 구성된다.
명령어
매우 많은 명령어가 존재한다 본 문서에서는 21개의 명령어를 설명한다.
| 명령코드 분류 | 예시 |
|---|---|
| 데이터 이동(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 |
[]로 둘러싸여 표현되며, 크기 지정자 TYPE PTR이 추가될 수 있다. TYPE에는 1, 2, 4, 8바이트 순으로 BYTE, WORD, DWORD, QWORD가 지원된다.데이터 이동 명령어는 어떤 값을 레지스터나 메모리로 옮기도록 지시한다. mov 명령어는 다음과 같은 형식을 가진다.
mov dst, src // src에 들어있는 값을 dst에 대입
lea는 다음과 같은 형식을 가진다.
lea dst, src //src의 유효주소(EA)를 dst에 저장
add dst, src //dst에 src의 값을 더한다.
sub dst, src //dst에서 src의 값을 뺀다.
inc op //op의 값을 1 증가시킴
dec op //op의 값을 1 감소시킴
and dst, src
or dst, src
xor dst, src
not op
cmp op1, op2 // op1과 op2를 비교, ZF 설정시 두 값이 같음
test op1, op2 // op1과 op2에 AND 비트연산 수행
push val // val을 스택 최상단에 쌓음
pop reg // 스택 최상단의 값을 꺼내서 reg에 대입
프로시저는 특정 기능을 수행하는 코드 조각이다. 이를 이용해서 코드 길이를 줄이고, 가독성을 높인다.
프로시저를 부르는 행위를 호출이라고 한다. 그리고 프로시저에서 돌아오는 것을 반환이라고 한다. 즉 원래의 흐름으로 복귀해야하므로 호출 다음의 명령어 주소를 스택에 저장하고 난 뒤에 rip로 이동한다.
- call addr: addr에 위치한 프로시저 호출
push return_address
jmp addr
- leave: 스택프레임 정리
mov rsp, rbp
pop rbp
- ret: return address로 반환
pop rip
- 스택프레임
스택프레임은 복수의 함수를 호출하는 경우 같은 스택영역을 사용하지 않도록 만드는 것이다. Application binary interface(ABI)에서는 함수 호출시, 자신의 스택프레임을 만들고 반환할 떄 이를 정리한다.
즉 함수 호출에서 반환에 이르기까지 과정을 설명하면 다음과 같다.
(1) call addr로 addr에 위치한 프로시저를 호출한다. 이때 돌아올 위치를 push한다.
(2) 기존 rbp를 스택에 push하고, rbp에 rsp를 mov한다.(rsp == rbp)
(2-1) 이 단계에서 rsp에는 rbp가 돌아갈 메모리 주소를 담고 있는 메모리를 가리키고있다.
(3) 필요한 크기 만큼 rsp에 sub를 수행, 스택 공간을 확보하면서 스택프레임을 만든다.
(4) 필요한 연산을 한다.
(5) 연산을 마무리 한 뒤, rsp에 rbp를 mov 한다. rsp는 스택 공간을 확보하기 전, 즉 (2-1)에서의 위치를 가리킨다.(rsp == rbp)
(6) 지금 rsp는 (2-1)에서 말한 메모리를 가리킨다.(rsp는 현재 사용하는 스택을 가리키며, pop을 수행하면 rbp가 돌아가야하는 메모리 주소가 나온다. pop을 rbp에 수행한다. rbp는 프로시저를 호출하기 전의 위치로 돌아온다.
(6-1) pop을 수행하면서 rsp는 (1)에서 push한 스택을 가리킨다.
(7) ret을 수행하면서 (1)에서 푸쉬한 주소를 rip에 넣으면서 프로시저 마무리.
요약하자면
rip, rbp 순으로 복귀할 주소들을 스택에 push해 놓고 스택을 확장해서 필요한 연산을 한 뒤 rsp는 원래 자리를 rbp가 가리키고 있으므로 그 자리로 돌아가고, rbp -> rip 순으로 스택에서 pop을 수행해서 복귀한다.
| syscall | rax | arg0(rdi) | arg1(rsi) | arg2(rdx) |
|---|---|---|---|---|
| read | 0x00 | unsigned int fd | char *buf | size_t count |
| write | 0x01 | unsigned int fd | const char *char | size_t count |
| open | 0x02 | const char *filename | int flags | umode_t mode |
| close | 0x03 | unsigned int fd | ||
| mprotect | 0x0a | unsigned long start | size_t len | unsigned long prot |
| connect | 0x2a | int sockfd | struct sockaddr *addr | int addrlen |
| execve | 0x3b | cons char *filename | const char *const *argv | const char *const *envp |