1. 데이터 타입(Data Type)


타입

설명

BYTE

8비트 부호 없는 정수

SBYTE

8비트 부호 있는 정수

WORD

16비트 부호 없는 정수

SWORD

16비트 부호 있는 정수

DWORD

32비트 부호 없는 정수

SDWORD

32비트 부호 있는 정수

FWORD

48비트 정수

QWORD

64비트 정수

TBYTE

80비트 정수


2. 피연산자 타입(Operand Type)


피연산자

설명

r8

8비트 범용 레지스터

r16

16비트 범용 레지스터

r32

32비트 범용 레지스터

Reg

임의의 범용 레지스터

Sreg

16비트 세그먼트 레지스터

Imm

8, 16, 32비트 즉시값

imm8

8비트 즉시값

imm16

16비트 즉시값

imm32

32비트 즉시값

r/m8

8비트 범용 레지스터, 메모리

r/m16

16비트 범용 레지스터, 메모리

r/m32

32비트 범용 레지스터, 메모리

mem

8, 16, 32비트 메모리


3. 어셈블리 명령어(Assembly Command)


오늘은 어셈블리 명령어에 대해서 배워보도록 하겠습니다. 오늘 보게될 명령어는 INC, DEC, ADD, SUB, MUL, IMUL, DIV, MOV, OR, XOR, AND, SHL, SHR, PUSH, POP, XCHG, LEA, JMP(조건 점프 명령 포함), CALL, CMP, NOP에 대해 알아볼건데, 상당히 명령어가 많으므로 짧게짧게 설명하도록 하겠습니다.


3-1. INC, DEC, ADD, SUB, MOV, MUL, IMUL, DIV


우선, INC와 DEC부터 보도록 하겠습니다. INC에는 피연산자에 1을 더하는 명령이며, DEC는 피연산자에 1을 빼는 명령입니다. (피연산자에는 레지스터 혹은 메모리도 올 수 있음.) 아래 예를 한번 보도록 합시다.


INC <Target>

DEC <Target>

#include <stdio.h>

int main()
{
	int num = 0;

	__asm {
		INC num
		INC num
		INC num
		DEC num
	}

	printf("result: %d\n", num);
	return 0;
}

결과:

result: 2
계속하려면 아무 키나 누르십시오 . . .


결과를 보시면, result가 2가 출력됨을 아실 수 있습니다. 코드를 보시게 되면, 정수형 변수 num의 선언과 동시에 0으로 값이 초기화 되고, 어셈블리 명령어인 INC를 통해 피연산자인 num의 값이 1만큼 증가하게 됩니다. 이렇게 3번의 INC 명령을 만나 1이 3번 증가되어, num의 값이 3인 상태에서 DEC 명령을 만나 1만큼 감소하게 됩니다. 그러고 최종 결과는 2죠.


이번엔, ADD와 SUB를 보도록 합시다. ADD란 녀석은 Destination에 Source의 값을 더한 뒤에, 그 더한 값을 Destination에 저장합니다. 반면에 SUB는 Destination에 Source의 값을 뺀 뒤에, 그 뺀 값을 Destination에 저장합니다. 한번 보도록 하죠.


ADD <destination>, <source>

SUB <destination>, <source>

#include <stdio.h>

int main()
{
	int destination = 20;

	__asm {
		ADD destination, 1000
		SUB destination, 500
	}

	printf("result: %d\n", destination);
	return 0;
}

결과:

result: 520
계속하려면 아무 키나 누르십시오 . . .


결과를 보시면, result가 1020이 출력되는걸 보실 수 있습니다. 코드를 보시면, 정수형 변수 destination의 선언과 동시에 20으로 값이 초기화 되고, 어셈블리 명령인 ADD를 통해 destination의 값이 1000(source)의 값만큼 증가하게 되는 것입니다. 그리고 그다음에 등장하는 SUB 명령을 통해 destination의 값에서 500(source)의 값만큼 감소하게 되는 것이죠.


이번에는 MOV 명령입니다. 이 MOV 명령은, 위에서 말했듯 Source의 데이터를 Destination으로 복사하는 작업을 수행합니다. 


MOV <destination>, <source>

#include <stdio.h>

int main()
{
	int result;

	__asm {
		MOV result, 10000
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 10000

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, MOV 명령으로 result 변수에다 10000 이란 값을 복사했습니다. 그 후에 result의 결과를 출력했더니, 우리가 복사시켰던 10000이란 값이 출력되었습니다.


이번엔, MUL과 IMUL을 보도록 하겠습니다. MUL 명령은 부호 없는 AL, AX, EAX의 값을 피연산자와 곱합니다. 피연산자가 1바이트이면 AL과 곱해서 AX에 저장되며, 2바이트면 AX와 곱하고 DX:AX에 저장됩니다. 4바이트일 경우에는 EAX와 곱해서 EDX:EAX에 저장됩니다. IMUL명령은, MUL명령에서 부호가 없었다면 IMUL에서는, 부호가 있는 AL, AX, EAX의 값을 피연산자와 곱합니다. 한번 보시죠.


MUL <Target>

IMUL <Value>

IMUL <destination>, <value>

IMUL <destination>, <value>, <value>

#include <stdio.h>

int main()
{
	int result;
	int num = 20;

	__asm {
		MOV EAX, 100
		MUL num
		MOV result, EAX
	}

	printf("result: %d\n", result);

	__asm {
		MOV EAX, -17
		IMUL num
		MOV result, EAX
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 2000
result: -340
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 처음보는 MOV가 보이는데, 지금은 그저 데이터를 복사하는 녀석이라고 기억해두시기 바랍니다. EAX 레지스터에 10이란 값을 복사한 뒤에, MUL 명령을 통해 EAX와 num의 값을 곱해, EAX 레지스터에 값을 저장합니다. 그리고, 다시 MOV 명령을 통해, EAX 레지스터에 저장된 값을 result에 복사하는 것입니다. 그런 뒤에 다시, EAX 레지스터에 -17이란 값을 복사하고, IMUL 명령을 통해 EAX와 num의 값을 곱한 뒤에, EAX 레지스터에 값을 저장하고 나서 MOV 명령을 통해 EAX 레지스터에 저장된 값을 result에 다시 복사합니다.


이번에는 DIV 명령입니다. DIV 명령은 부호 없는 정수의 나눗셈을 수행하는 명령입니다. 


DIV <Target>

#include <stdio.h>

int main()
{
	int result1, result2;

	__asm {
		MOV EDX, 0
		MOV EAX, 2000
		MOV EBX, 3
		DIV EBX
		MOV result1, EAX
		MOV result2, EDX
	}

	printf("result1: %d, result2: %d\n", result1, result2);
	return 0;
}

결과:

result1: 666, result2: 2
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, MOV 명령으로 EDX 레지스터의 값에다 0을 복사하고, EAX 레지스터의 값에다 2000을 복사하고, EBX 레지스터의 값에다 3을 복사합니다. 그러고 나서 DIV 명령으로 EBX의 값으로 EAX의 값을 나누게 됩니다. 이때, 몫은 EAX 레지스터에 저장되며, 나머지는 EDX 레지스터에 저장됩니다.


3-2. AND, OR, XOR, SHL, SHR


이번에는 AND, OR, XOR입니다. 이미 보신분들도 있겠지만, AND 부터 차근차근 보며 넘어가도록 하겠습니다. AND는 마주보는 비트가 서로 1일때만 결과값이 1이 됩니다. 만약에 한 비트는 1인데, 다른 한쪽의 비트는 0이라면 결과값은 0이 나오게 됩니다. 이 AND 명령은 Destination과 Source 피연산자의 각 비트가 AND 연산됩니다.


AND <destination>, <source>

#include <stdio.h>

int main()
{
	int result=10;

	__asm {
		AND result, 6
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 2
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, AND 명령이 쓰였습니다. result와 6의 각 비트가 AND 연산됩니다. 10은 2진수로 바꾸게되면 1010이고, 6을 2진수로 바꾸게되면 110이 됩니다. 이 둘을 AND 연산하게되면, 0010이 나오고, 이는 10진수로 2를 나타냅니다.


이번에는 OR 명령인데, OR 명령은 AND 명령과는 달리 마주보는 비트가 서로 0이 아닌이상, 1을 결과로 내보냅니다. 만약에, 한 비트가 1이고, 다른 한쪽의 비트는 0이여도 결과는 1이 나온다는 겁니다. 이 OR 명령은 Destination과 Source 피연산자의 각 비트가 OR 연산됩니다.


OR <destination>, <source>

#include <stdio.h>

int main()
{
	int result=10;

	__asm {
		OR result, 6
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 14
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, OR 명령이 쓰였습니다. result와 6의 각 비트가 OR 연산됩니다. 10은 2진수로 바꾸게되면 1010이고, 6을 2진수로 바꾸게되면 110이 됩니다. 이 둘을 OR 연산하게되면, 1110이 나오고, 이는 10진수로 14를 나타냅니다.


이번에는 XOR 명령인데, XOR 명령은 특이하게도, 마주보는 비트가 서로 달라야 결과값으로 1을 내보냅니다. 만약에, 한 비트는 1인데, 또 한쪽의 다른 비트도 1이라면 서로 같으므로 결과가 0이 나오는거죠. 이 XOR 명령은 Destination과 Source 피연산자의 각 비트가 XOR 연산됩니다.


XOR <destination>, <source>

#include <stdio.h>

int main()
{
	int result=10;

	__asm {
		XOR result, 6
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 12
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면 XOR 명령이 쓰였습니다. result와 6의 각 비트가 XOR 연산됩니다. 10은 2진수로 바꾸게되면 1010이고, 6을 2진수로 바꾸게되면 110이 됩니다. 이 둘을 XOR 연산하게되면, 1100이 나오고, 이는 10진수로 12를 나타냅니다. 결과를 보시면, 12란 값이 출력됨을 확인하실수 있습니다.


이번에는 SHL, SHR 명령인데, SHL은 Destination 피연산자를 Source 피연산자의 크기만큼 왼쪽으로 비트를 이동시킵니다. 반대로 SHR은 Destination 피연산자를 Source 크기만큼 오른쪽으로 비트를 이동시킵니다.


SHL <destination>, <source>

SHR <destination>, <source>

#include <stdio.h>

int main()
{
	int result=10;

	__asm {
		SHL result, 4
		SHR result, 2
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 40
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, SHL 명령을 통해 result의 각 비트가, 왼쪽으로 4칸 이동합니다. result에 담긴 값은 10, 10을 2진수로 바꾸게 되면 1010이므로, 1010을 4칸 이동시키게 되면 10100000이 됩니다. 즉 160이 되는 셈입니다. 그리고 SHR 명령을 통해 result의 각 비트가, 오른쪽으로 2칸 이동합니다. 10100000이므로, 오른쪽으로 2칸 이동하게되면 101000 이 되고, 이는 10진수로 40을 나타냅니다.


3-3. PUSH, POP


이번에는 스택에 관련된 명령어를 보도록 하겠습니다. 먼저, PUSH와 POP 명령을 보도록 합시다. PUSH 명령은 스택에 값을 집어넣는 명령이고, POP 명령은 스택에서 값을 빼버리는 명령입니다. PUSH 명령을 만나면 ESP(스택 프레임의 끝 주소 저장)의 값이 4만큼 줄어듭니다. 반대로 POP 명령을 만나면 ESP의 값에서 4가 더해지고, ESP 레지스터가 가르키고 있는 위치의 스택 공간에서 4바이트 만큼을 Destination 피연산자에 복사합니다.


POP <destination>

PUSH <destination>

#include <stdio.h>

int main()
{
	int result;

	__asm {
		PUSH 16
		PUSH EAX
		PUSH 10

		POP result
		POP EAX
		POP EBX
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: 10
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, PUSH 명령을 통해 스택 공간에 16, EAX 레지스터의 값, 10을 쌓아 올립니다. 그리고 POP 명령을 통해, 마지막에 있던 10이 result로 복사되고, 스택에서 빠져나갑니다. 그러고, EAX, EBX 레지스터에 복사된 뒤에 나머지 값도 다 빠져나갑니다.


3-4. XCHG, LEA


XCHG 명령은 두 피연산자의 내용을 서로 교환하는 명령입니다. 피연산자로 레지스터, 메모리가 올 수 있습니다.


XCHG <reg/mem>, <reg/mem>

#include <stdio.h>

int main()
{
	int result1;
	int result2;

	__asm {
		MOV EAX, 1127
		MOV EBX, 2012
		XCHG EAX, EBX
		MOV result1, EAX
		MOV result2, EBX
	}

	printf("result1: %d, result2: %d\n", result1, result2);
	return 0;
}

결과:

result1: 2012, result2: 1127
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, MOV 명령을 통해 EAX 레지스터와 EBX 레지스터에 각각 1127과 2012란 값을 복사했습니다. 그러고 나서 XCHG 명령을 통해, 두 레지스터에 담긴 값을 서로 교환했습니다. 그리고 EAX 레지스터의 값은 result1로 복사했고, EBX 레지스터의 값은 result2로 복사하여 result1과 result2의 결과를 출력했더니, 두 레지스터가 가지고 있던 값이 바뀜을 확인하실 수 있습니다.


NEG 명령은 피연산자의 2의 보수를 계산한 뒤, 그 결과의 값을 피연산자에 저장하는 녀석입니다.


NEG <reg/mem>

#include <stdio.h>

int main()
{
	int result;

	__asm {
		MOV EAX, 442
		NEG EAX
		MOV result, EAX
	}

	printf("result: %d\n", result);
	return 0;
}

결과:

result: -442
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, MOV 명령을 통해, 442란 값을 EAX 레지스터에 복사한 뒤에 NEG 명령을 통해, EAX 레지스터의 값에 2의 보수를 취한 값이 EAX 레지스터에 들어갑니다. 그리고 EAX 레지스터의 값을 result로 복사하여, result를 출력합니다.


LEA 명령은 Source 피연산자의 유효 주소를 계산해서 Destination 피연산자에 복사하는 명령입니다.


LEA <destination>, <source>

#include <stdio.h>

int main()
{
	int result;

	__asm {
		LEA EBX, result
		MOV result, EBX
	}

	printf("result: %d, result address: %d\n", result, &result);
	return 0;
}

결과:

result: 15203852, result address: 15203852
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, LEA 명령을 통해 result의 주소를, EBX 레지스터에 복사합니다. 그리고 EBX 레지스터에 담긴 값을 result로 다시 복사하여, result의 값과, result의 주소값을 출력하여 보았더니 똑같은 결과를 출력하고 있습니다. (즉, 주소를 알아낸 뒤 복사하는 명령어.)


3-5. CALL, CMP, NOP, JMP


CALL 명령은 함수 호출시 사용되는 명령입니다. 이 CALL 함수는, CALL의 다음 명령 포인터를 스택에 PUSH 합니다. (함수가 호출된 뒤에, 다시 원위치로 돌아오기 위해서.)


CALL <function address>

#include <stdio.h>

void func()
{
	printf("func() call!\n");
}

int main()
{
	__asm {
		CALL func
		CALL func
	}
	return 0;
}

결과:

func() call!
func() call!
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, CALL 명령을 통해 func 함수가 호출되고 있습니다. 결과를 보시면, func 함수가 두번 호출됨을 확인하실 수 있습니다.


CMP 명령은 두 피연산자를 비교하는 명령입니다. 단순하게도, Destination 피연산자에서 Source 피연산자를 빼서 값을 비교합니다. 연산을 수행한 뒤, 결과값이 0이라면, 두 피연산자가 같음을 나타내기 위해 제로 플래그(ZF)가 1로 세트됩니다. 다를 경우에는 0으로 해제됩니다. (여기서 ZF(Zero Flag, 제로 플래그)란 플래그 레지스터 중의 하나로, 연산 결과가 0일때 세트됩니다. 반대로 연산 결과가 0이 아닐때는 0으로 해제됩니다.)


CMP <destination>, <source>

#include <stdio.h>

int main()
{
	int operand = 5;

	__asm {
		MOV EAX, 5
		CMP operand, EAX
	}

	printf("%d\n", operand);
	return 0;
}

결과:

5
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, MOV 명령을 통해 5란 값을 EAX 레지스터로 복사합니다. 그리고 CMP 명령을 통해 EAX 레지스터의 값과, operand의 값을 비교합니다. (operand(5), EAX(5)) 그리고 operand와 EAX의 값을 서로 빼고난 뒤의 결과가 0이 나오는데, 서로 같음으므로 ZF가 1로 세트됩니다. 물론, 뺀 결과는 영향을 미치지 않고 소멸됩니다.


NOP 명령은 아무것도 하지 않는 명령입니다. 리버싱 시에 유용하게 사용되는 녀석입니다. (JMP 구문을 NOP로 처리하여 다른곳으로 점프하지 못하게 하는 등..)


NOP


마지막으로, JMP와 조건 점프에 관한 명령들입니다. 우선, JMP 명령은 피연산자의 위치로 실행 흐름을 변경시키는 명령입니다. (피연산자에는 레지스터, 메모리, 레이블이 올 수 있음.)


JMP <label/reg/mem>

#include <stdio.h>

int main()
{
	__asm {
		JMP e
	}

	printf("b!\n");
e:
	printf("e!\n");
	return 0;
}

결과:

e!
계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, JMP 명령을 통해 e 레이블로 점프(Jump)합니다. b!와 e! 모두 출력되지 않고, 점프되어 e!만 출력된 것입니다. JMP 말고도, 조건 점프 명령이라 해서 상당히 많은 조건 점프 명령이 있으니 한번 참고해보시는 것도 좋을듯 합니다.


명령어

명령어의 의미

명령어가 수행되기 위한 플래그

레지스터와 범용 레지스터의 상태

JA

Jump if (unsigned) above

CF=0 and ZF=0

JAE

Jump if (unsigned) above or equal

CF=0

JB

Jump if (unsigned) below

CF=1

JBE

Jump if (unsigned) below or equal

CF=1 or ZF=1

JC

Jump if carry flag set

CF=1

JCXZ

Jump if CX is 0

CX=0

JE

Jump if equal

ZF=1

JECXZ

Jump if ECX is 0

ECX=0

JG

Jump if (signed) greater

ZF=0 and SF=0

JGE

Jump if (signed) greater or equal

SF=OF

JL

Jump if (signed) less

SF!=OF

JLE

Jump If (signed) less or equal

ZF=1 and SF!=OF

JNA

Jump if (unsigned) not above

CF=1 or ZF=1

JNAE

Jump if (unsigned) not above or equal

CF=1

JNB

Jump if (unsigned) not below

CF=0

JNBE

Jump if (unsigned) not below or equal

CF=0 and ZF=0

JNC

Jump if carry flag not set

CF=0

JNE

Jump if not equal

ZF=0

JNG

Jump if (signed) not greater

ZF=1 or SF!= OF

JNGE

Jump if (signed) not greater or equal

SF!=OF

JNL

Jump if (signed) not less

SF=OF

JNLE

Jump if (signed) not less or equal

ZF=0 and SF=OF

JNO

Jump if overflow flag not set

OF=0

JNP

Jump if parity flag not set

PF=0

JNS

Jump if sign flag not set

SF=0

JNZ

Jump if not zero

ZF=0

JO

Jump if overflow flag is set

OF=1

JP

Jump if parity flag set

PF=1

JPE

Jump if parity is equal

PF=1

JPO

Jump if parity is odd

PF=0

JS

Jump if sign flag is set

SF=1

JZ

Jump is zero

ZF=1