어셈블리어
어셈블리 언어는 컴퓨터의 기계어와 치환되는 언어이다. 기계어가 여러 종류라면 어셈블리어도 여러 종류여야 함을 의미하며, CPU에 사용되는 ISA(명령어 집합 구조)에 따라서 IA-32, X86-64, ARM, MIPS 등 많은 종류의 어셈블리어가 존재한다.
어셈블리어 기본 구조
어셈블리의 문장은 동사에 해당하는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성된다.
Opcode Operand1, Operand2
어셈블리어 명령어의 종류
- 데이터 이동 : mov, lea
- 산술 연산 (Arithmetic) : inc, dec, add, sub
- 논리 연산 (Logical) : and, or, xor, not
- 비교 (Comparison) : cmp, test
- 스택 (Stack) : push, pop
- 프로시저 (Procedure) : call, ret, leave
- 시스템 콜 (System Call) : syscall
어셈블리어의 피연산자
피연산자에는 상수, 레지스터, 메모리가 올 수 있다.
메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다. 여기서 타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 지정한다.
ex. 메모리 피연산자의 예
QWORD PTR[0x8048000] : 0x8048000의 데이터를 8바이트만큼 참조한다.
DWORD PTR[0x8048000] : 0x8048000의 데이터를 4바이트만큼 참조한다.
WORD PTR[rax] : rax가 가르키는 주소에서 데이터를 2바이트만큼 참조한다.
참고.
WORD의 크기가 2바이트인 이유
Intel 사에서 IA-16 아키텍처 기반 16비트 프로세서를 만들었을 때 기본 처리 단위인 16비트를 WORD로 재정의(typedef) 했었다. 이후 IA-32, X86-64 아키텍처가 등장했지만 기존의 프로그램들이 새로운 아키텍처와 호환되지 않을 수 있기 때문에 WORD 자료형의 크기를 16비트로 유지하기로 하였다. 대신 DWORD (Double Word, 32bit)와 QWORD(Quad Word, 64bit) 자료형을 추가로 만들었다.
대표 명령어
명령어 | 예시 | 설명 |
mov | mov dst, src | src에 들어있는 값을 dst에 대입한다. |
lea | lea dst, src | src의 유효주소(Effective Address, EA)를 dst에 저장한다. |
add | add dst, src | dst에 src의 값을 더한다. |
sub | sub dst, src | dst에 src의 값을 뺀다. |
inc | inc op | inc op : op의 값을 1 증가시킨다. |
dec | dec op | dec op : op의 값을 1 증가시킨다. |
and | and dst, src | dst와 src의 비트가 모두 1이면 1, 아니면 0 |
or | or dst, src | dst와 src의 비트 중 하나라도 1이면 1, 아니면 0 |
xor | xor dst, src | src의 비트가 서로 다르면 1, 같으면 0 |
not | not op | op의 비트를 전부 반전시킨다. |
cmp | cmp op1, op2 | op1과 op2를 비교한다. 두 피연산자를 빼서 대소를 비교하는데, 서로 같은 두 수를 빼면 ZF = 1 로 ZeroFlag가 설정되어 두 값이 같았는지 판단할 수 있다. |
test | test op1, op2 | op1과 op2를 비교 두 피연산자에 AND 비트연산을 취하여 대소를 비교한 후 ZF를 설정한다. |
jmp | jmp addr | addr로 rip를 이동시킨다. |
je | je addr | 직전에 비교한 두 피연산자가 같으면 점프한다. (Jump to Equal) |
jg | jg addr | 직전에 비교한 두 연산자 중 전자가 더 크면 점프한다. (jump if greater) |
push | push val | val을 스택 최상단에 쌓음 |
pop | pop reg | 스택 최상단의 값을 꺼내서 reg에 대입 |
call | call addr | addr에 위치한 프로시져 호출 |
leave | leave | 스택 프레임 정리 |
ret | ret | 호출자의 실행 흐름으로 돌아감 |
int | int $0x80 | OS에 할당된 인터럽트 영역을 system call |
nop | nop | 아무 동작도 하지 않는다. (No Operation) |
출처 : DreamHack
어셈블리어 해석
#include<stdio.h>
int main() {
int a = 1;
int b = 2;
int c = a + b;
printf("%d", c);
}
push ebp
mov ebp,esp
sub esp,0E4h
push ebx
push esi
push edi
lea edi,[ebp+FFFFFF1Ch]
mov ecx,39h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov ecx,9AC003h
call 009A1316
mov dword ptr [ebp-8],1
mov dword ptr [ebp-14h],2
mov eax,dword ptr [ebp-8]
add eax,dword ptr [ebp-14h]
mov dword ptr [ebp-20h],eax
mov eax,dword ptr [ebp-20h]
push eax
push 9A7D08h
call 009A10CD
add esp,8
xor eax,eax
pop edi
pop esi
pop ebx
add esp,0E4h
cmp ebp,esp
call 009A123F
mov esp,ebp
pop ebp
ret
우선 특정 함수에 접근하려면 사용할 스택의 공간을 만들어야 한다. ebp를 push한뒤 esp의 값을 ebp에 넣어주어 기존에 사용하던 esp의 값(스택의 꼭대기)을 스택의 가장 밑 부분으로 만들었다.그런 뒤 esp(스택의 꼭대기)에 0E4h(16진수)의 크기를 할당하여 ebp ~ esp까지 공간을 할당하여 사용할 스택의 크기를 만든다.
dword ptr [ebp-8]에 1이라는 값을 넣고 dword ptr[ebp-14h]에 2라는 값을 넣는다. 이것이 위의 코드에서 변수 a에 1을 넣고 변수 b에 2를 넣은 부분이다. 이렇듯 어셈블리어에서는 변수의 이름 같은 것이 없고, 단지 메모리 주소만 있을 뿐이다.
그런 다음 eax에 다시 [ebp-8]에 있는 값을 넣고 eax와 [ebp-14]의 값을 더한다. 이 부분이 바로 a+b이다. 다음 mov 함수를 통해 변수 c인 [ebp-20h] 부분에 방금 add한 값을 넣어준다.
call 009A10CD을 통해 print함수를 출력한다. 마지막으로 사용이 끝난 레지스터들을 pop해주고 ret한다.
'지식창고 > IT 지식' 카테고리의 다른 글
정규표현식 (0) | 2022.12.11 |
---|---|
ping 명령어 옵션, tracert 명령어 (0) | 2022.10.26 |
19.7년 마다 돌아오는 GPS판 Y2K 버그 GPS Week Number Rollover (0) | 2022.10.17 |
TCP Flag(URG, ACK, PSH, RST, SYN, FIN) (0) | 2022.10.05 |
TCP/UDP 포트 번호 정리 (0) | 2022.10.02 |