2012년 8월 12일 일요일

어셈블리 명령어 - 기초

인터넷에 어셈블리 명령어에 관한 좋은 위키 문서가 있어 그 문서를 나름대로 정리한 글입니다.

먼저 어셈블리 명령어를 이해하기 전에 필요한 기초 지식을 정리해 보았습니다.
어셈블리를 공부하기위해 제 글을 보고 계신 분들은
대부분 C나 C++를 공부하면서 이런 기초 지식을 한번쯤은 공부하셨을 겁니다.
만약 아래의 내용들이 지루하거나 이미 알고 있는 내용이라면 사뿐히 건너뛰어도 좋습니다.

16진법

어셈블리어를 다루기 위해선 16진법 읽기에 익숙해져야 합니다.
암산으로 16진법을 10진법으로 바꾸는 능력까지 필요한건 아니지만
적어도 산수를 통해서 바꾸는 방법은 알고 있어야 합니다.

16진법 표기에는 여러가지 방법이 있지만 대부분의 경우 아래와 같은 표기법을 사용합니다.

  • 0x 접두사와 함께 표기하는 방법, eg. 0x1ef7
  • h 접미사와 함께 표기하는 방법, eg. 1ef7h

16진수에서 0부터 f까지의 숫자와 문자는 10진수 0부터15까지의 수를 의미합니다.

  • 0 = 0
  • 1 = 1
  • ...
  • 9 = 9
  • a = 10
  • b = 11
  • c = 12
  • d = 13
  • e = 14
  • f = 15

16진수를 10진수로 바꾸기 위해선 오른쪽부터 각각의 수를
160, 161, 162... 만큼 곱해야 합니다. 다음은 0x1ef7를 10진수로 바꾸는 과정입니다.
(7 * 160) + (f * 161) + (e * 162) + (1 * 163)
= (7 * 160) + (15 * 161) + (14 * 162) + (1 * 163)
= (7 * 1) + (15 * 16) + (14 * 256) + (1 * 4096)
= 7 + 240 + 3584 + 4096
= 7927

16진수가 나올때 마다 이 과정을 반복해야 하는 것은 아니지만,
00부터 FF (10진수로 0부터 255) 숫자에 익숙해 져야 합니다.

2진법

2진법은 숫자 0과 1를 이용한 수 표기법입니다. 
10진수로 바꾸는 방법은 16진수의 경우와 유사하지만
16의 제곱이 아닌 2의 제곱을 곱한 값들을 더해주어야 합니다.
다음은 2진수 1101를 10진수로 바꾸는 과정입니다.

(1 * 20) + (1 * 21) + (0 * 22) + (1 * 23)
= (1 * 1) + (1 * 2) + (0 * 4) + (1 * 8)
= 1 + 2 + 0 + 8
= 11


10진수와 2진수의 변환은 그리 많지 않지만,
16진수와 2진수의 변환은 그 변환 과정이 쉽기 때문에 많이 볼 수 있습니다.  
다음은 16진수와 2진수의 변환을 보여줍니다.

  • 0x0 = 0000
  • 0x1 = 0001
  • 0x2 = 0010
  • 0x3 = 0011
  • 0x4 = 0100
  • 0x5 = 0101
  • 0x6 = 0110
  • 0x7 = 0111
  • 0x8 = 1000
  • 0x9 = 1001
  • 0xa = 1010
  • 0xb = 1011
  • 0xc = 1100
  • 0xd = 1101
  • 0xe = 1110
  • 0xf = 1111

예를 들어, 이진수 100101101001110를 16진수로 바꾸려면:
  1. 수의 길이가 4의 배수가 되도록 이진수 앞에 0을 추가합니다: 0100101101001110
  2. 수를 4자리만큼씩 나눕니다: 0100 1011 0100 1110
  3. 각각의 4자리 이진수를 16진수로 바꿉니다: 0x4 0xb 0x4 0xe
  4. 바꾼 수를 하나로 모아서 씁니다: 0x4b4e

반대로 16진수 0x469e를 2진수로 바꾸려면:
  1. 수를 한자리씩 나눕니다: 0x4 0x6 0x9 0xe
  2. 각각의 16진수를 2진수로 바꿔줍니다: 0100 0110 1001 1110
  3. 맨 앞의 0을 빼고 수를 하나로 모아서 씁니다: 100011010011110

자료형

자료형이란 16진수의 숫자가 어떻게 구분되고 나뉘어 지는지와 관련이 있습니다. 
보통 자료형은 비트(혹은 바이트) 수와 음수 표현이 가능 여부에 따라 분류할 수 있습니다.

비트(혹은 바이트) 수는 수의 길이를 정의합니다.
예를 들어 8비트(1바이트) 수는  0x03, 0x34, 0xFF와 같이두 개의 16진수로 구성됩니다.
이와 달리 16비트(2바이트) 수는 0x1234, 0x0001, and 0xFFFF 와 같이 표현 될 수 있습니다.

부호수(signed)와 무부호수(unsigned)는 음수 표현의 가능 여부에 의해 구분됩니다. 
음수 표현이 가능한 자료형은 그렇지 않은 경우에서
표현할 수 있는 최대값의 반 값까지만 표현할 수 있습니다.
음수와 양수를 표현하기 위해 수의 가장 앞자리 수를 이용합니다.
2진수의 경우 가장 앞자리가 1일때, 16진수의 경우에는
가장 앞자리 수가 8부터 F 사이의 수일때 그 수는 음수가 됩니다.
2진수 음수표현의 경우 http://ko.wikipedia.org/wiki/2의 보수 를 참고하시기 바랍니다.

  • 0x10는 2진수로 0001 0000이며, 10진수로 +16이 됩니다.
  • 0xFF는 2진수로 1111 1111이며 음수입니다. 가장 앞자리 수를 제외한 7비트 수 1111111의 0과 1을 바꿔서 0000000을 만들고, 그 수에 1을 더한 0000001이 수의 크기가 됩니다. 따라서 10진수 -1을 의미합니다.
  • 0x80는 2진수로 1000 0000이며, 마찬가지로 음수가 됩니다. 가장 앞자리 수를 제외한 7비트 0000000의 0과 1을 바꿔 1111111을 만들고, 그 수에 1을 더한 10000000이 수의 크기가 됩니다. 따라서  10진수 -128을 의미합니다.

여러가지 자료형 :
  • 8비트 (1바이트) 자료형 = char (혹은 BYTE)
    • 16진수 : 0x00 - 0xFF
    • 부호수 (10진수) : -128 - 127
    • 무부호수 (10진수) : 0 - 255

  • 16비트 (2바이트) 자료형 = short int (종종 WORD라고 부릅니다)
    • 16진수 : 0x0000 - 0xFFFF
    • 부호수 (10진수) :  -32768 - 32767
    • 무부호수 (10진수) :  0 - 65535

  • 32비트 (4바이트) 자료형 = long int (종종 DWORD 또는 double-WORD라고 부릅니다)
    • 16진수 :0x00000000 - 0xFFFFFFFF
    • 부호수 (10진수) :  -2147483648 - 2147483647
    • 무부호수 (10진수) :  0 - 4294967295

  • 64비트 (8바이트) 자료형 = long long (종종 QWORD 또는 quad-WORD라고 부릅니다)
    • 16진수 :0x0000000000000000 - 0xFFFFFFFFFFFFFFFF
    • 부호수 (10진수) : -9223372036854775808 - 9223372036854775807
    • 무부호수 (10진수) : 0 - 18446744073709551615

메모리


각각의 실행중인 프로그램은 다른 프로세스와 공유하지 않는
자신만의 메모리 공간을 차지하고 있습니다.
이 메모리에는 프로그램 코드, 변수, 로드된 dll, 그리고 프로그램 스택 등 
프로그램이 필요로 하는 모든 것이 담겨 있습니다.

프로그램이 실행되면, .exe 파일의 코드 부분이 메모리에 로드되고,
이 메모리 이미지에서 명령어들이 실행됩니다.
우리는 물리 디스크에 있는 .exe 파일을 수정하지 않고
메모리에 로드된 이미지만을 수정할 수 있다는 점이 중요합니다.

프로그램은 또한 프로세스의 메모리 공간에 .dll 파일들을 로드 합니다.
각각의 .dll은 실행될 때 마다 달라지는 메모리 공간을 가지고 있습니다.
또한 .dll 파일은은 각자 필요한 변수를 저장할 섹션(section)들을 가지고 있습니다.

메모리에 저장된 변수들은 리틀 에디안 혹은
빅 에디안 형식의 바이트 정렬 방식으로 저장되어 있습니다.
이 방식들은 CPU 아키텍쳐에 의해 구분됩니다. 
모든 인텔 x86 프로세서는 리틀 에디안 방식을 사용하며, 
모든 PowerPC는 빅 에디안을 사용합니다.
이 가이드는 인텔 x86 프로세서에 관한 내용이므로, 빅 에디안에 관해서는 설명하지 않겠습니다.

리틀 에디안 방식에서 바이트는 역순으로 저장됩니다. 예를 들어:
  • 0x12345678 (4바이트)는 78 56 34 12 의 순서로 저장됩니다.
  • 0x00001234 (4바이트)는 34 12 00 00 의 순서로 저장됩니다.
  • 0xaabb (2바이트)는 bb aa 의 순서로 저장됩니다.

처음엔 이런 저장 방식이 혼란스러울 수도 있지만, 이 가이드를 보면서 자주 보게 되면 쉽게 익숙해 질 것입니다.

포인터

포는터는 C언어를 이해하는 과정에서 가장 어려운 부분일 것입니다.
하지만, 여러분은 어셈블리를 공부하기 전에 반드시 포인터에 대해 이해하고 있어야 합니다.
만약 다음의 글을 이해하지 못하신다면 포인터 강의를 찾아서 읽어 보시는 것을 권합니다.

포인터는 단순히 변수의 메모리 주소를 저장하는 변수입니다.
포인터가 저장하는 메모리 주소는 어떤것이든 될 수 있습니다.
포인터는 또한 역참조를 하기 위해 사용 되기도 합니다.

다음은 정수를 가리키는 포인터 변수를 선언하는 C 코드 입니다.
int *i;
그리고 다음은 char를 가리키는 포인터 변수를 선언하는 코드입니다.
char *c;
'*' 를 붙이는 것을 제외하면 다른 변수를 선언하는 방법과 같습니다.
'*' 는 단순히 변수가 포인터형이라는 것을 나타내는 것 외에는 별 의미가 없습니다.

변수의 주소값을 얻기 위해선 주소 연산자 '&'를 사용하면 됩니다.
'&' 를 변수 앞에 붙이면 그 변수의 주소가 리턴되고, 이 주소는 포인터에 저장될 수 있습니다.
다음은 주소 연산자를 이용하는 예 입니다.

 int *i;       /* 포인터 형으로 정의합니다. */
 int somevar = 7;      /* 변수를 somevar 로 정의하고 정수 값 7을 저장합니다 */
 i = &somevar;      /* 변수 somevar의 주소값을 포인터 i 에 저장합니다*/

포인터의 마지막 용법은 앞에서 말씀드렸다 시피 "역참조" 입니다.
포인터를 역참조 하기 위해 포인터 앞에 '*'을 붙입니다.
이것을 포인터 정의에서 사용된 '*'과 혼동하시면 안됩니다.
다음은 역참조의 예시이며, 반드시 모두 이해하시기 바랍니다.
(예시를 위해서 존재하지 않는 printf() 함수를 사용했습니다.)

 int *i;       /* i를 포인터로 정의 합니다 */
 int somevar = 7;      /* somevar라는 변수를 정의하고 정수 값 7을 젖. */
 i = &somevar;      /* 변수 somevar의 주소값을 포인터 i 에 저장합니다. */
 print(i);       /* 이 함수는 i에 저장된 somevar의 주소값을 출력할 것입니다. */
 print(*i);     /* i 는 somevar의 주소를 가리키며 somevar의 값이 7이므로, printf() 함수는 역참조를 통해 7을 출력합니다. */ 
*i = 10;       /* 역참조를 통해 somevar에 정수값 10을 저장합니다. */ 
print(somevar);        /* 역참조를 통해 somevar의 값이 변했으므로 10이 출력됩니다. */ 

요악:
  • An asterisk ("*") is used in declaration to show a variable is a pointer: int *i
  • An ampersand ("&") is used in front of any variable to retrieve its address: i = &somevar
  • An asterisk ("*") is used on a pointer to dereference it, to get the value it's pointing to: print(*i)
Note that if a pointer without a valid address is dereferenced, the program will crash.
Some arithmetic can be done on pointers, such add addition and subtraction. Doing addition/subtraction on pointers is different than a normal variable because the size of the data type is taken into account. That is, instead of "ptr + 1" going to the next whole number, if ptr is an integer it goes to the next possible integer in memory, which is 4 bytes away. If the ptr was a short (2 bytes), "ptr + 1" goes ahead 2 bytes, to the next short in memory. The reason for this is so that arrays can be easily stepped through, which will be shown below.

아스키 코드

아스키 코드는 문자, 숫자, 그리고 기호를 메모리에 푠현하는 방식입니다.
하나의 아스키 문자는 1바이트 크기이며, 보통 아스키 문자는 0x00와 0x7F 사이의 값을 저장합니다.
  • 0x00 또는 '\0' 기호는 스트링 (아스키 문자 배열)의 끝을 나타냅니다.
  • 0x0d 와 0x0a는 각각 리턴과 개행 문자를 나타냅니다. 새 줄을 만들기 위해 사용됩니다.
  • 0x20 는 공백(space) ' '를 의미합니다. 
  • 0x30 - 0x39 는 '0' 부터 '9'를 나타냅니다.  (0x30 = '0', 0x31 = '1', 0x32 = '2', 등)
  • 0x41 - 0x5A 는 대문자 'A' 부터 'Z' 를 나타냅니다.
  • 0x61 - 0x7A 는 소문자 'a' 부터 'z' 를 나타냅니다.

배열

An array is a sequence of 1 or more values of the same type. In memory, all entries in an array are stored sequentially.
For example, an array of these five integers {1, 2, 3, 0xaabb, 0xccdd} will be stored like this:
01 00 00 00 02 00 00 00 03 00 00 00 bb aa 00 00 dd cc 00 00           |           |           |           |
Note that values stored are all expanded to the full size of integers, padded with zeros. Also note that the values are stored in little endian, so the order of the bytes are reversed.
An array in C is declared like this:
int arr[5] = { 1, 2, 3, 0xaabb, 0xccdd }; 
This creates an array of 5 integers, which reserves 20 bytes (5 integers * 4 bytes/integer) to store them. An array in C must have a static length, because the space is allocated before the program ever runs.
When an array is created this way, the array variable ("arr") is actually a pointer to the first element. Then when an array is accessed, anaddition and a dereference occur.
This code:
int arr[5] = { 1, 2, 3, 0xaabb, 0xccdd };print(arr[2]); 
Will display the third element in the array, the number 3. This code:
int arr[5] = { 1, 2, 3, 0xaabb, 0xccdd };print( *(arr + 2) );
Is identical. The address that is 2 past the first element will be dereferenced, and that address contains the third value. Recall that addition on a pointer increments based on the type, so "arr + 2" in this case goes ahead by 2 integers, or 8 bytes.
Here is a way to loop through an array using pointers:
 int arr[5] = { 1, 2, 3, 0xaabb, 0xccdd }; int *ptr; int i; ptr = arr; // Point ptr at arr. Note that we don't use "address of" on arr, since arr is already a pointer and therefore already contains an address.  for(i = 0; i < 5; i++) {  print(*ptr); // Print the value of the element, starting at 0, ending with 4  ptr++; // Go to the next element in the array }

문자열 (string)

문자열은 아스키 문자의 배열이며,null 문자 '\0' 로 끝납니다.

char str[] = "Hello";
위의 코드는 6개의 문자 배열을 만들며, 문자열에 그 값을 복사해서, 다음의 배열을 만듭니다.
{ 'H',  'e',  'l',  'l',  'o',  '\0' }
또는
{ 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00 }

char *str = "Hello";
위의 코드는 수정할 수 없는(static) 문자열 "Hello" 를 가리키는 포인터를 정의합니다.
그 전의 코드와 유사하지만, 이 경우엔 값을 변경할 수 없습니다.


댓글 없음:

댓글 쓰기