어셈블리어는 2개의 구조로 이루어져 있다
문장으로 치면 동사에 해당하는 opcode와 그 목적어에 해당하는 피연산자가 있다.
mov dst,src
mov명령어는 src에 들어있는 값을 dst에 대입하는 opcode이다.
lea dst,src
lea 명령어는 mov 명령어와 비슷하지만 mov는 src에 들어있는 값을 대입하지만, lea는 src의 주소값을 대입한다
add dst,src
add는 말그대로 dst에 src값을 더하는 명령어이다
sub dst,src
sub은 dst에서 src를 빼는 명령어이다.
inc op
op의 값을 1증가 한다.
dec op
op의 값을 1감소 한다.
cmp op1,op2
op1과 op2를 빼서 대소를 비교한다. 결과는 따로 대입하지않고 만약 두값이 같다면 0이되며, ZF(zero flag)가 설정된다.
test op1,op2
op1과 op2룰 AND연산한다. 결과는 따로 대입하지않고, 만약 하나의 값이 0이라면 ZF가 설정된다.
분기 명령어는 rip(instruction pointer)를 이동시켜 실행 흐름을 바꾸는 명령어이다
jmp addr
addr로 rip를 이동시키는 명령어로, 예시를 들어보자면
1: xor rax, rax 2: jmp 1 ; jump to 1
여기에 파생으로 직전의 비교한 두 피연산자가 같으면 점프하는 je (jump if equal)
1: mov rax, 0xcafebabe 2: mov rbx, 0xcafebabe 3: cmp rax, rbx ; rax == rbx 4: je 1 ; jump to 1
전자가 더 크면 점프하는 jg (jump if greater)
1: mov rax, 0xcafebabe 2: mov rbx, 0xcafebabe 3: cmp rax, rbx ; rax == rbx 4: je 1 ; jump to 1
등 종류가 엄청많다
push val
val을 스택의 최상단에 쌓는다.
pop reg
스택의 최상단의 값을 꺼내서 reg에 대입한다.
프로시저란 특정기능을 수행하는 코드조각을 말한다. 반복되는 연산을 프로시저 호출로 대체하고, 기능별로 코드 조각에 이름을 붙여 가독성을 크게 높일 수 있다.
1. call
call addr
addr에 있는 프로시저를 호출한다
코드실행전
[Register] rip = 0x400000 rsp = 0x7fffffffc400 [Stack] 0x7fffffffc3f8 | 0x0 0x7fffffffc400 | 0x0 <= rsp [Code] 0x400000 | call 0x401000 <= rip 0x400005 | mov esi, eax ... 0x401000 | push rbp
코드실행후
[Register] rip = 0x401000 rsp = 0x7fffffffc3f8 [Stack] 0x7fffffffc3f8 | 0x400005 <= rsp 0x7fffffffc400 | 0x0 [Code] 0x400000 | call 0x401000 0x400005 | mov esi, eax ... 0x401000 | push rbp <= rip
스택프레임이란??
스택은 함수별로 지역변수나 임시값들을 저장하는 영역이다.
하지만 이 영역을 아무런 구분없이 사용하게 된다면 서로 다른 함수가 같은 메모리 영역을 사용해 충돌이 일어날 수 있고,
따라서 스택의 영역을 명확히 구분하기 위해 스택 프레임이 이용된다.
코드 실행전
[Register] rsp = 0x7fffffffc400 rbp = 0x7fffffffc480 [Stack] 0x7fffffffc400 | 0x0 <= rsp ... 0x7fffffffc480 | 0x7fffffffc500 <= rbp 0x7fffffffc488 | 0x31337 [Code] leave
코드 실행 후
[Register] rsp = 0x7fffffffc488 rbp = 0x7fffffffc500 [Stack] 0x7fffffffc400 | 0x0 ... 0x7fffffffc480 | 0x7fffffffc500 0x7fffffffc488 | 0x31337 <= rsp ... 0x7fffffffc500 | 0x7fffffffc550 <= rbp
3.ret
음.. 대충 설명하자면 우리가 call코드를 이용해 프로시저를 호출하게 되면 스택프레임이 새로 할당되면서 원래 있던 스택에 call코드 다음에 실행해야될 코드의 주소를 넣는다. 그 후 프로시저가 종료하게 되면 ret명령어를 통해 원래 있던 스택의 주소값을 가져와 call코드 다음부터 정상적으로 실행할 수 있게 해준다.
코드 실행 전
[Register] rip = 0x401000 rsp = 0x7fffffffc3f8 [Stack] 0x7fffffffc3f8 | 0x400005 <= rsp [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 [Code] 0x400000 | call 0x401000 0x400005 | mov esi, eax <= rip ... 0x401000 | mov rbp, rsp ... 0x401007 | leave 0x401008 | ret
현재의 운영체제는 하드웨어 접근권한, 소프트웨어 접근권한들에 대한 해킹으로부터 보호하기 위해 커널모드와 유저모드를 분리하여 관리한다. 하지만 우리는 그러한 권한을 필요로하는 동작을 수행할때가 있는데, 이럴때 커널모드에게 어떤 동작을 요청하는 코드가 바로 시스템 콜(syscall)이다.
필요한 인자는 rdi → rsi → rdx → rcx → r8 → r9 → stack 순으로 전달한다
기능과 인자에 대한 더 자세한 정보는 구글짱을 이용하자.
[Register] rax = 0x1 rdi = 0x1 rsi = 0x401000 rdx = 0xb [Memory] 0x401000 | "Hello Wo" 0x401008 | "rld" [Code] syscall
결과
Hello World
피연산자는 총 3가지 종류가 있는데
그중에서 메모리 피연산자는 []의 형태로 표현되며, 앞에 크기 지정자가 올 수 있다 크기 지정자는