기계어는 0과 1로 구성된다. 단어위주로 문장을 구사하는 우리한테는 이해하기 매우 어렵다. 그래서 과학자 중 한 명인 David Wheeler은 EDSAC를 개발 하면서 어셈블리어와 어셈블러를 고안해냈다.
어셈블러는 개발자들이 어셈블리어(C언어 같은 프로그래밍 언어)로 코드를 작성하면 컴퓨터가 이해할 수 있는 기꼐어로 치환해준다.
소프트웨어를 역분석하는 사람들은 여기에 역발상을 더해 기계어를 어셈블리언어로 번역하는 역어셈블러를 개발해 소프트웨어를 분석할려고 기계어를 읽는 필요를 덜어줬다.
어셈블리어는 CPU에 사용되는 명령어 집합구조가 많은만큼 어셈블리어의 종류도 다양하다 x64, ARM등 많은 어셈블리어가 존재하는데 x64의 어셈블리어를 공부할거다.
x64 어셈블리언어는 단순한 문법구조를 가진다.
동사에 해당하는 명령어(Opcode)와 목적어(Operand)에 해당하는 피연산자로 구성된다.
mov eax, 3
opcode oprerand1 operand2
대입해라 eax에 3을
x64에는 많은 명령어가 존재
주요한 21개의 명령어를 자세히 학습
명령코드
데이터 이동 : mov, lea 산술 연산 : inc, dec, add, sub 논리 연산 : and, or, xor, not 비교 : cmp, test 분기 : jmp, je, jg 스택 : push, pop 프로시져 : call, ret, leave 시스템 콜 : syscall
피연산자(operand)에는 상수, 레지스터, 메모리 총 3가지 종류가 올수있다. 메모리 피연산자는 []으로 둘러싸인것으로 표현되며, 앞에 크기 지정자 TYPE PTR이 추가될수 있다. 여기서 타입 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.
메모리 피연산자
QWORD PTRT[0x8048000] : 0x08048000의 데이터를 8바이트만큼 참조 DWORD PTR[0x8048000] : 0x08048000의 데이터를 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 : rst의 값을 rdi+8*rcx가 가리키는 주소에 대입
lea dst,src : src의 유효주소를 dst에 저장한다
lea rsi,[rbx+8*rcx] : rbx+8*rcx를 rsi에 대입
mov는 저장되어 있는 값을 이동시키고 lea는 값의 주소를 이동시킨다.
산술연산은 덧셈 뺄셈 곱셈 나눗셈 연산을 지시한다.
add dst,src : dst에 src값을 더한다.
add eax,3 : eax += 3 add ax,WORD PTR[rdi] : ax += *(WORD *)rdi
suv dst,src : dst에서 src의 값을 뺀다.
sub eax,3 : eax -= 3 sub ax, WORD PTR[rdi] : ax-= *(WORD *)rdi
inc op : op의 값을 1 증가시킴
inc eax : eax += 1
dec op : op의 값을 1 감소시킴
dec eax : eax -= 1
논리 연산 명령어는 and, or, xor, neg등의 비트연산을 한다. (비트 단위)
and dst, src : dst와 src의 비트가 모두 1이면 1, 아니면 0
[레지스터] eax = 0xffff0000 ebx = 0xcafebabe [Code] and eax, ebx [Result] eax = 0xcafe0000(eax와 ebx를 비교해 1인 부분만 eax에 대입)
or dst, src : dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
[레지스터] eax = 0xffff0000 ebx = 0xcafebabe [Code] or eax, ebx [Result] eax = 0xffffbabe(eax와 ebx를 비교해 1이 존재하는곳은 그대로 대입)
xor dst, src : dst와 src의 비트가 서로 다르면 1, 같
으면 0[레지스터] eax = 0xffffffff ebx = 0xcafebabe [Code] xor eax, ebx [Result] eax = 0x35014541 (eax와 비교를 하고 같으면 0 다르면 ebx의 비트를 반전시킨값을 eax에 대입)
not op : op의 비트 전부 반전
[레지스터] eax = 0xffff0000 [Code] not eax [Result] eax = 0x0000ffff(그냥 비트를 전부 반전)
비교 명령어는 두 피연산자의 값을 비교하고 플래그를 설정한다.
cmp op1, op2 : op1과 op2를 비교
(cmp는 두 피연산자의 결과를 대입하지않고 플래그를 설정하는데 예시처럼 서로 같은 두수를 빼면 결과값이 0이되어 ZF플래그가 설정된다. 이 플래그를 보고 CPU가 두값이 같았는지를 알수있다.)[code] mov rax, 0xA mov rbx, 0xA cmp rax, rbx ; ZF = 1
test op, op2 : op1과 op2를 비교
cmp가 빼서 결과값으로 비교를 했다면 test는 두 피연산자에 AND 비트연산을 취해 결과값으로 플래그를 설정한다. 이후 CPU는 플래그를 보고 비교 결과를 판단할수있다.[code] xor rax, rax test rax, rax ; ZF = 1
분기 명령어는 rip를 이동시켜 실행 흐름을 바꾼다.
jmp addr : addr로 rip를 이동시킵니다
[code] xor rax, rax jmp 1 ; jump to 1
je addr : 직전에 비교한 두 피연산자가 같으면 점프
[code] mov rax , 0xcafebabe mov rbx , 0xcafebabe cmp rax, rbx ; rax == rbx je 1 ; jump to 1
jg addr : 직전에 비교한 두연산자중 전자가 더 크면 점프(jump if greater)
mov rax , 0x31337 mov rbx , 0x13337 cmp rax, rbx ; rax ? rbx je 1 ; jump to 1
데이터 이동 연산자
mov dst, src: src의 값을 dst에 대입 lea dst, src: src의 유효 주소를 dst에 대입
산술 연산
add dst, src: src의 값을 dst에 더함 sub dst, src: src의 값을 dst에서 뺌 inc op: op의 값을 1 더함 dec op: op의 값을 1 뺌
논리 연산
and dst, src: dst와 src가 모두 1이면 1, 아니면 0 or dst, src: dst와 src중 한 쪽이라도 1이면 1, 아니면 0 xor dst, src: dst와 src가 다르면 1, 같으면 0 not op: op의 비트를 모두 반전
비교
cmp op1, op2: op1에서 op2를 빼고 플래그를 설정 test op1, op2: op1과 op2에 AND 연산을 하고, 플래그를 설정
분기
jmp addr: addr로 rip 이동 je addr: 직전 비교에서 두 피연산자의 값이 같을 경우 addr로 rip 이동 jg addr: 직전 비교에서 두 피연산자 중 전자의 값이 더 클 경우 addr로 rip 이동
레지스터의 이름 및 크기
x86에서 x64로 넘어가면서 더 큰사이즈들의 레지스터들이 등장했다. 자연스레 이전 x86의 EAX(4byte)가 RAX(8byte)로 더 커졌지만 EAX는 RAX의 하위 4바이트를 의미하며 사용된다.
EAX는 RAX의 하위 4바이트, AX는 EAX의 하위 2바이트를 의미한다.