1. x64 어셈블리어 개요
어셈블리 언어는 컴퓨터의 기계어와 치환되는 언어다.
따라서 기계어의 종류가 다양하다면 어셈블리 언어 또한 다양하게 존재한다.
필자는 ISA x64를 사용하기 때문에 x64 어셈블리어를 배울 예정이다.
2. 어셈블리어 구조
어셈블리어는 opcode(명령어)와 operand(피연산자)로 이루어져있다.
opcode | operand1 | operand2 |
mov | eax | 3 |
명령어는 이런 종류가 올 수 있고,
명령어 | |
데이터 이동 | mov, lea |
산술 연산 | inc, dec, add, sub |
논리 연산 | and, or, xor, not |
비교 | cmp, test |
분기 | jmp, je, jg |
스택 | push, pop |
프로시져 | call, ret, leave |
시스템 콜 | syscall |
피연산자에는 세 종류가 올 수 있다.
- 상수
- 레지스터
- 메모리
이 중에 메모리는
TYPE PTR [ 크기 ] 형태로 나타나는데,
TYPE에는 BYTE, WORD, DWORD, QWORD가 올 수 있고, 각각 1, 2, 4, 8 바이트의 크기를 지정한다.
예) QWORD PTR [0x8048000] -> 0x8048000의 데이터를 8바이트만큼 참조
3. 어셈블리 명령어
◾이동
• mov dst, src: src에 들어있는 값을 dst에 대입
• lea dst, src: src의 유효 주소를 dst에 저장
◾산술 연산
• add dst, src: dst에 src 값을 더함
• sub dst, src: dst에 src 값을 뺌
• inc op: op의 값을 1 증가시킴
• dec op: op의 값을 1 감소시킴
◾논리 연산
• and dst, src: 모두 1이면 1 아니면 0
• or dst, src: 둘 중 하나만 1이면 1, 아니면 0
• xor dst, src: 둘 다 다르면 1, 같으면 0
• not op: op의 비트 모두 반전
◾비교
• cmp op1, op2: 두 연산자를 빼서 비교, 차가 0이면 ZF 설정
• test op1, op2: XOR 연산자로 비교, 0이면 ZF 설정
[cmp]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
[test]
1: xor rax, rax
2: test rax, rax ; ZF=1
◾분기
• jmp addr: addr로 rip를 이동
• je addr: 직전에 비교한 두 연산자가 같으면 점프
• jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프
◾스택
• push val: val을 스택 최상단에 쌓음
[Register]
rsp = 0x7fffffffc400 // rsp = top
[Stack]
0x7fffffffc400 | 0x0 <- rsp
0x7fffffffc408 | 0x0
[Code]
push 0x31337
[Register]
rsp = 0x7fffffffc3f8 // rsp 8만큼 감소
[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0
• pop val: 스택 최상단 값을 reg에 대입
[Register]
rax = 0
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0
[Code]
pop rax
[Register]
rax = 0x31337 // 최상단의 값 rax에 대입
rsp = 0x7fffffffc400 // rsp 8만큼 증가
[Stack]
0x7fffffffc400 | 0x0 <- rsp
0x7fffffffc408 | 0x0
◾프로시저
• call addr: addr에 위치한 프로시저 호출
[Register]
rip = 0x400000 // 현재 명령어 위치
rsp = 0x7fffffffc400 // top
[Stack]
0x7fffffffc3f8 | 0x0
0x7fffffffc400 | 0x0 <- rsp
[Code]
0x400000 | call 0x401000 <- rip
0x400005 | mov esi, eax
...
0x401000 | push rbp
[Register]
rip = 0x401000 // 호출 위치로 이동
rsp = 0x7fffffffc3f8 // 8만큼 감소
[Stack]
0x7fffffffc3f8 | 0x400005 <- rsp // rbp 대입
0x7fffffffc400 | 0x0
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax // return_address(rbp)
...
0x401000 | push rbp <- rip // 현재 코드 위치
• leave: 스택프레임 정리 (예) 조건을 만족하지 않으면 다음 스택프레임으로
mov rsp, rbp
[Register]
rsp = 0x7fffffffc400 // 스택프레임의 top
rbp = 0x7fffffffc480 // 스택프레임의 베이스 주소
[Stack]
0x7fffffffc400 | 0x0 <- rsp
...
0x7fffffffc480 | 0x7fffffffc500 <- rbp
0x7fffffffc488 | 0x31337
[Code]
leave
pop rbp
[Register]
rsp = 0x7fffffffc488
rbp = 0x7fffffffc500
[Stack]
0x7fffffffc400 | 0x0
...
0x7fffffffc480 | 0x7fffffffc500
0x7fffffffc488 | 0x31337 <- rsp
...
0x7fffffffc500 | 0x7fffffffc550 <- rbp
• ret: return address로 반환
pop rip
[Register]
rip = 0x401008
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x400005 <- rsp
0x7fffffffc400 | 0
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax
...
0x401000 | mov rbp, rsp // 여기서 쭉 실행하다가
...
0x401007 | leave
0x401008 | ret <- rip // 여기서 끝남
[Register]
rip = 0x400005
rsp = 0x7fffffffc400
[Stack]
0x7fffffffc3f8 | 0x400005
0x7fffffffc400 | 0x0 <- rsp // rip라는 스택프레임 pop
[Code]
0x400000 | call 0x401000
0x400005 | mov esi, eax <- rip // 호출 명령어 다음 코드로 이동
...
0x401000 | mov rbp, rsp
...
0x401007 | leave
0x401008 | ret