[컴퓨터구조, opcode] expending an opcode application
gdb 분석을 위해 알아야할 지식 (tistory.com)
gdb 분석을 위해 알아야할 지식
1. 프로세스 구조 프로세스 : 프로그램이 메모리 상으로 올라와 실행된 상태 위의 사진은 가상메모리의 레이아웃 상태이다. (물리적인 메모리는 저 가상메모리와 다르게 섞여있을 수 있다. 이유
sumsum2.tistory.com
여기서 연장선상의 내용을 한번 포스팅 해보려고한다.
설명이 너무 컴팩트하다고 느껴졌고, 좀더 자세한 내용을 담고 싶었다.
원래는 본문 수정만 하려고 했는데 생각보다 내용 추가할 게 많아서 새 글로 포스팅을 하게 되었다.
2. 어셈블리
cpu 아키텍처가 x86에서 x86-64로 변함에 따라 레지스터들 또한 늘었다.
rax, rbx, rcx,rdx,rsi,rdi,rbp,rsp,r8,r9,r10,r11,r12,r13,r14,15,rip
이 중에서 rbp, rsp, rip는 항상 주소를 넣는 포인트 변수의 역할로 사용되었다.
하지만 요즘에는 컴파일러의 발달로 rbp 레지스터가 없어도 base 위치를 알 수 있어 rsp, rip가 온전히 포인터의 역할을 하고 있다.
레지스터를 쪼개면 rax(64비트)->eax(32비트) -> ax(16비트) -> ah,al(8비트) 레지스터로 절반의 사이즈씩 쪼개진다.
이렇게 쪼개는 이유는 64비트환경에 있던 것들이 32비트같이 더 낮은 비트의 아키텍처 환경으로 가면 문제가 일어나기 때문에 쪼개진다.
이전에는 이렇게만 포스팅했지만 사실 연산할때에도 and rax, 0x1111111111111110이라는 연산을 할 때에도 생각해보면 가장 끝자리만 and 연산을 할 생각인데 모든 것을 다 할 필요가 없기도 하다. 이 때문에
and eax, 0x11111110이나, and al, 0 과 같이 사용하는 식으로도 이용이 되어진다.
이후에 리버싱을 하다보면 x86-64 아키텍처에서도 rax만 아니라 eax 레지스터들을 종종 볼 수 있는 이유이다.
일단 (rax)와 rax의 차이를 알아야한다.
()는 포인터의 의미를 가지고 있다
gdb는 intel 기반에서는 포인터의 의미로 [] 대괄호를 사용한다.
또 대괄호는 아래와 같이 활용을 하기도 한다.
[rax,rbx]#[rax+rbx]
8[rax, rbx] #[rax+rbx+8]
[rax,rbx,8] #[rax + 8*rbx]
mov - 데이터값 이동
이전 포스팅 내용에 내용들 추가
mov dest, src #오른쪽 소스를 왼쪽 데스티네이션으로 옮겨라. intel 문법
mov ebp, esp #esp=ebp
mov rax, [0x12345678] #0x12345678를 p rax를 변수 A라고 가정 가정, A = *p; --> 역참조
mov [rbx], rax #rax 값을 rbx가 가리키는 값에 넣겠다, *B=A 라는 식으로 생각하면 된다.
mov rax, 8[rbx]# rax = rbx + 8(byte)
응용 :
mov BYTE PTR [edi], 0x41 #바이트 피티알은 1바이트 단위의 메모리를 뜻하고 이 안에 괄호가 메모리 주소로 생각하고 있겠다, edi의 메모리 주소가 가리키는 곳에 0x41이라는 1바이트의 숫자를 이동시킨다.
mov eax, DWORD PTR(4바이트) [esp+4] #esp+4가 가리키는 메모리 위치에 eax에 있는 값을 쓰겠다.
lea - 데이터 주소 이동
예시 ) intel 기준
lea dest src(src의 주소를 dest로 이동한다)
lea eax, DWORD PTR [esp+4](eax에 esp+4에 있는 주소를 넣는다)
lea는 주소를 이동하기도 하지만 연산을 하기도 한다.
기계어 처리 과정에서도 01001011이 있다면 이를 12배를 하는 것보다 2^n으로 곱셈한 후, 3배를 하는 과정이 훨씬 빠르기 때문에 lea에서 다음과 같은 연산들을 사용한다.
왜냐하면 left shift 연산이 빠르기 때문이다.
이와 같은 이유로 lea를 사용할 때
rax*12
lea [rax, rax*2] # 3rax
sal rax, 2# rax<<2
다음과 같이 동작하는 것을 알아두는 것도 좋다.
cmp, test - 비교의 역할을 하는 operand로 보통 분기문을 처리하는 경우가 많은데
위와 같은 처리를 하고 어디에 저장되는지에 대해서 궁금해 할 것 같다.
싱글 비트 레지스터, 상태 코드가 있다
CF, ZF, SF, OF 4종류이다
CF :Carry flag(unsigned)
SF : Sign flag(signed)
ZF : Zero flag
OF : Overflow flag(signed)
ex) r = a+b
1. unsigned의 숫자가 어떠한 연산때문에 overflow되어 carry가 발생하면 CF set.'
2. if r < 0 이 맞다면 SF가 SET.
3. if r==0 이 맞다면 ZF가 set.
4. (r<0 && a>0 && b>0) || (r>=0 && a<0 && b<0)와 같이 signed overflow가 나타나면 OF set.
자 다시 비교 opcode를 살펴보자.
cmp a,b
의 경우는 보통 b-a로 뺄셈하고 0이면 a와 b가 같다고 판단한다. b나 a레지스터에 결과값이 저장되는 게 아니라 ZF 레지스터에 set이 되느냐 안 되느냐로 결과값이 참인지, 거짓인지 구분한다.
test의 경우를 보면
주로 test rax, rax의 경우를 종종 볼 것이다.
이또한 같은 rax를 and연산을 하여서 rax가 0이면 ZF가 set될 것이고, 0이 아니면 set 되지 않을 것이다.
그렇다면 분기 점프를 할지 안 할지를 정해주는 역할을 한다.
분기의 형태
if(a){
//code1, 주소 0x12345678
}
else{
//code2, 주소 0x123456FF
}
그 이후 0x88888888 code3
주소
test rax, rax
je 0x123456FF #rax가 0, 즉 분기문이 false이면 0x123456FF 호출
0x12345678 code 1 ..... #rax가 1, 즉 분기문이 true이면 바로 다음 코드로 넘어감
jmp 0x88888888 #이후, 0x123456FF 를 건너 뛰고 바로 0x88888888을 호출
0x123456FF code2
.
.
.
0x88888888 이후 code3....
sete | ZF | Equal |
setne | ~ZF | Notequal |
sets | SF | Negative |
setns | ~SF | Nonnegative |
setg | ~(SF^OF)&~ZF | Greater (Signed) |
setge | ~(SF^OF) | Greater of Equal (Signed) |
setl | (SF^OF) | Less (Signed) |
setle | (SF^OF) | ZF | Less or Equal (signed) |
seta | ~CF&~ZF | Above (unsigned) |
setb | CF | Below(unsigned) |
위와 같은 표가 있다
이 set은 어디에 쓰이냐면 주로 movzb 같은 친구와 같이 사용된다.
set으로 레지스터를 세팅하고 movzb로 초기화하는 식.