여기서 CPU의 자세한 내부 구조를 설명과 어셈블리와 컴퓨터 구조등의 low level 에 대한 이야기는 하지 않겠습니다. 왜냐하면 하다 보면 자연히 알게 될것이고 인라인어셈블러라는 목표에 맞는 내용만 담기 위해서입니다
(커플군의 실력,능력부족도 하나의 이유입니다 :D)

간단하게만 얘기해 보면,
CPU는 어떠한 처리를 할때, 기본적인 방식은

1. RAM에 있는 데이타를 CPU 안에 복사
2. 이를 처리(연산)
3. 결과를 다시 RAM에 저장.


이러한 과정을 거치게 됩니다.

따라서, CPU 내부에는 RAM의 데이타를 읽어 와서 임시로 저장할 공간이 있는데 이 공간을 Register(레지스터) 라고 합니다.
이 레지스터라의 크기는 32bit 즉 integer 값 하나정도를 저장할 공간 밖에 되지않습니다.
불행중다행인지 이러한 Register가 하나로 부족해 몇개가 있습니다

이들에게 이름을 붙였는데.
EAX, EBX, ECX, EDX..

등과 같은 방법으로 붙는다. (절때 아무의미없이 ABCD 순이 아니다)
레지스터의 종류나 그 역활에 대해서는 차차 알아 보자.

나중에시간이되면 이에 대한글도 포스트하겠지만
보다 자세한 내용을 알고 싶은 분들은 어셈관련 참고서적을 참고하기 바랍니다 :)

이 번에는 소스를 한 번 살펴 봅시다

void memset ( void* dest, char fill, int num )
{
  if (num <= 0) return;

  _asm
  {
    mov   al, fill
    mov   ecx, num
    mov   edi, dest
    rep   stosb
  }
}

일단 얼핏 보기에도 C 코드와 어셈 코드가 섞여 있다.
잘 알 듯이 _asm { } 로 둘러 싸여 있는 부분이 어셈 코드입니다.

그 안을 살펴 보면,

mov, rep, stosb 는 cpu 명령어 이고
al, ecx, edi 는 register(레지스터) 입니다.

fill, num, dest 는 변수 입니다.
변수를 CPU가 어찌 알겠냐고 물어보겠지만 고맙게도 컴파일러가 변수들을 각 변수들의 주소로 변환해 줍니다.
이러한 것은 디버깅시에 살펴 보면 알 수 있으니 그때 살펴보도록합시다.

잠시 위 코드의 뜻을 살펴 보면

fill 변수의 값을 al 레지스터로 옮겨 오고,
num 변수의 값을 ecx 레지스터로 옮겨 오고,
dest 변수의 값을 edi 레지스터로 옮겨 오고,
(여기서 dest 변수의 값을 타입에서 알수 있듯이 포이터입니다.)

반복을 하라 - rep
어떻게?
al 레지스터의 값을
edi 레지스터에 있는 주소로
ecx 레지스터의 값 만큼 반복해서 저장해라. - stosb

아마도 이런뜻일 것입니다.

이 내용을 종합해보면 우리가 자주 사용하는 memset 함수를 한 번 만들어 본 것임을 알수있습니다.
다음에는 memcpy 를 한 번 만들어 보도록합시다 :)

요약 어셈 명령어
MOV

이 명령어는 데이타를 옮길때 사용한다,
1. 레지스터에 특정 값을 넣거나,
2. 레지스터에서 레지스터로 값을 옮기거나,
3. 메모리(RAM)에 있는 값을 레지스터로 가져 오거나,
4. 레지스터에서 메모리(RAM)로 데이타를 옮길때....

애석하게도 MOV는 메모리(RAM)에서 메모리(RAM)으로
바로 데이타를 옮기지는 못한다.

ex)  MOV  al, 10    // al 레지스터에 10이라는 값을 넣어라.
      MOV  ebx, eax   // eax 레지스터의 값을 ecx 레지스터에 넣어라.
      MOV  ecx, [edi] // edi 레지스터가 가리키는 주소(RAM)에 있는 값을 ecx로 가져 와라.
      MOV  [edi], ecx  // ecx 레지스터에 있는 값을 edi 레지스터가 가리키는 주소(RAM)로 옮겨라.


자 이번에는 memcpy를 한 번 만들어 봅시다.

void memcpy ( void* dest, void* src, int num )
{
        if (num <= 0) return;

        _asm
        {
                mov   edi, dest
                mov   esi, src
                mov   ecx, num
                cld
                rep   movsb
        }
}

한 번 분석해 봅시다.
edi, eax, esi, ecx 는 레지스터 일테고,
mov, cld, rep, movsb 는 cpu 명령어 일것이고
dest, src, num은 변수 입니다.

edi 에 dest 변수 값을 넣고,
esi 에 src 변수 값을 넣고,
ecx 에 num 변수값을 넣을 테고,
음 cld 는 뭘까? 일단은 건너뛰고 다음부터 하도록하죠.
rep는 ecx 값만큼 반복하라는 뜻이고,
(그럼, ecx에 num 값이 있으니, num 만큼 반복하겠지요?)
저번에는 stosb 였는데 이번에는 movsb 이네요

stosb 는 al 의 값을 edi 주소에 저장하는 것이고,
movsb 는 esi주소의 값을 edi 주소로 옮기는 명령어입니다..
그런뒤, esi의 값과 edi 값을 하나씩 증가 시키게됩니다.

이를 종합해 보면,
rep movsb 는 esi 주소의 값을 edi 주소로 ecx 갯수만큼 옮기는 역활을 하게됩니다
우리가 알고 있는 memcpy 함수와 동일한 역활을 하는 것이지요 :)

여기서 우리는 esi 레지스터는 소스 주소,
edi 레지스터는 목적지의 주소를,
ecx 레지스터는 카운트의 용도로 쓰임을 볼 수 있습니다

프로그래머가 레지스터를 어떻게 사용할 것이냐는 개인이 알아서 결정할 문제이지만, 대부분 위와 같은 역활로 쓰입니다.



이제 위에서 지나간 cld 에 대해서 알아 보도록합시다
레지스터에는 여러 종류의 레지스터가 있는데 그 중 플래그레지스터 라는 것이 있습니다
이 놈은 자신의 각 비트가 하나의 상태를 나타낸다.
(하나의 비트 이므로 0 아니면 1 의 값만을 갖는다.)
이 놈은 cpu 의 명령이 실행될때마다.....그 결과에 대한 상태의 변화를 기록한다.

mov   edi, dest

라는 명령을 실행했다고 하면,

플래그 레지스터의 상태를 나타내는 플래그에는
OF (Overflow가 발생했으면 set)
DF (방향- set되었을때 esi,dei 감소)
ZF (Zero- 실행 결과가 0이면 set)
CF (Carry- 실행 결과가 자리 올림이 발생했으면 set)

등등이 있다.

그 사용예는 앞으로 아주 자주 볼것입니다. 그 중 설명이 모호한 DF를 한 번 살펴보도록합시다
위에서 살펴본 movsb 가 데이타를 옮긴후 esi, edi 값을 하나씩 증가 시킨다고 했습니다.
사실은, 이 DF 플래그 값에 따라 값을 증가시킬것인가 감소시킬것인가를 정하게됩니다

즉, DF 가 0 이면 esi, edi 의 값을 증가 시키고,
    DF 가 1 이면 esi, edi 의 값을 감소 시킨다.

cld 라는 명령어는 이 DF 플래그의 값을 0으로 세팅한다.
std 라는 명령도 있느데 이는 DF 플래그를 1로 세팅한다.
      
자 그럼 위의 명령들을 한 번 확장해 보도록합시다

movsb 라는 명령어에서 마지막 b는 byte 만큼씩 옮기라는 뜻입니다.
그렇다면 movsw 도 있고, movsd도 있습니다
여기서 w = word 를 d = double word 를 의미합니다

실제 우리가 사용하는 memcpy 코드는 아래와 같다.
한 번 생각해보면 좋을꺼같다.

void memcpy ( void* dest, void* src, int num )
{
        if (num <= 0) return;

        _asm
        {
              mov   edi, dest
              mov   eax, edi
              mov   esi, src
              mov   ecx, num
              mov   edx, ecx
              shr   ecx, 1
              shr   ecx, 1
              cld
              rep   movsd
              mov   ecx, edx
              and   ecx, 0x03
              rep   movsb
         }
}


이번에는 플래그 레지스터가 어떻게 활용되는지를 한 번 살펴 보도록합시다

void   strcpy (char* dest, char* src)
{
         _asm
         {
                  mov   edi, src
                  mov   ecx, 0xffffffff
                  xor   al, al
                  cld
                  repnz scasb
                  not   ecx
                  ...
         }
}

자.. 이제 거의 다 아는 것들이 눈에 보이기 시작한다.

xor 은 배타적 논리합으로 상반된 비트일때만 set(1) 을 해줍니다
예를들어

           0101001
      xor 1011000
   -------------
           1110001

이해가 되시나요? 근대..xor al, al 왜 자신을 xor 했을까요?
같은레지스터를 xor 하면 0이 될텐데 말이죠,

네, 생각하신대로 이 코드는 mov al, 0 과 같습니다.
근데 왜 mov 를 쓰지않고 xor 을 쓰냐면 속도면에서 매우 빠르기 때문입니다.

따라서 거의 이렇게 사용하는 코드
레지스터 값을 0으로 세팅할때 xor 연산을 하게 됩니다.



아주 반가운 rep (repeat,반복하라),
근데, 이번에는 뒤에 nz 가 붙었다.
그 뜻은 (not zero)...이고, 이를 Repeat와 합해 보면,

repnz = Repeat While not zero   ( 0 이 아닐 동안 반복하라 )

여기서 Not Zero 라는 것은 앞서 실행한 명령의 결과가 0 이 아닐때,
만약 실행한 결과가 0이면 플래그레지스터의 ZF(zero flag)가 1로 세팅되어 있을 것입니다.
0 이 아닐때이므로 플래그레지스터의 ZF(zero flag)가 0일때라는 뜻입니다

rep는 반복할때마다 ecx 값을 하나씩 빼나간다 그러다 ecx = 0 이 되면 반복을 중단하며
repnz 는 ecx = 0 일때도 중단하고, ZF = 1 일때도 중단하게됩니다.

카운트 만큼 반복하는 것은 C에서
      for(int i=0;i<cnt;i++ ) { }
     
특정조건동안 반복하는 것은
     while (a != 0) { }  
    
뭐 이런 구문들을 생각나게 합니다.

드디어 반복문에 쓰이는 조건문을 이해하는순간이네요 ^^*


그 다음, scasb 는 Scan String의 뜻으로  

     al - [edi] (edi가 가리키는 주소의 값)

연산한 결과를 가지고 플래그 레지스터의 값들을 바꾼고 edi 값을 하나 증가 시킵니다.
물론, 그 결과값이 0 이면 ZF 플래그를 1 로 세팅합니다.

바로 이놈이 반복을 멈추게 하는 녀석입니다.
즉, 조건절을 만들어 내는 부류의 명령이지요 :)

이러한 조건문은
1. 어떤 명령이....실행결과로써 플래그 레지스터를 건든다. (값을 바꾼다)
2. 뒤에서 이 플래그의 조건 (플래그의 값)에 따라 분기를 하거나 반복을 한다.


의 순을 따르게 됩니다.

위 소스를 정리하면,
ecx에 0xffffffff를 넣고, al에 NULL을 넣고,
NULL을 찿을때까지 반복해서 ecx값을 빼 나갑니다
NULL을 찿고 난뒤 ecx 값을 not 연산을 합니다
그러면, ecx에 src의 문자열에서 NULL까지의 문자수가 나오게됩니다 :)

ecx값을 return 하면 바로 strlen 함수가 된는것이지요!
그리구 부분에 memcpy 기능을 붙여 넣으면 (ecx 만큼 memcpy를 하면) 바로 strcpy 함수가 완성되는 것이다.

ecx에 0xffffffff 값을 넣고 빼 나가다가 이를 not 연산하면 원하는 카운트를 얻는 코드는 자주쓰이니 잘알아두자!

strcpy 의 나머지 부분은 여러분들이 완성해보세요 :D

이번에는 ZeroMemory 함수를 작성해봅시다
이 함수 굳이 설명을 하지 않아도 memset과 자주 쓰이는 0으로의 초기화 함수중에 하나 입니다 :D 이것을 이번에 인라인 어셈으로 바꾸어 보도록합시다!

push ebp
mov esp,ebp
..
..
pop ebp
ret

그리고 여기에 쓰이는 최소한의 스택 2개들 그리고
수를 알수 없는 cmp 명령어의 사용

위의 코드는 함수의 전형적인 코드가 될것입니다.
이것들을 없에고 좀더 간단하게 코드를 줄여 보자!! 입니다.
아! 루프안이라면 아무래도 함수 보단 매크로 쪽이 좋을 듯 합니다..
(왜 인지는 한번 생각해 보심이 제 생각은 밑에 적겠습니다.)

우선 4의 배수의 크기를 가진 메모리를 초기화 하도록 합시다
타입을 2가지로 나누어야 합니다.
우선 포인터인것과 일반 적인 변수 형태 입니다.
코드 상에는 크게 차이는 없습니다. 그럼 적어 보도록 합니다
크기가 4의 배수 라면

xor eax,eax
mov ecx,크기
shr ecx,2
mov edi,주소 or lea edi,변수
rep stosd

이것 입니다.
코드를 설명 하면

xor eax,eax  // eax 를 0 으로 set해줍니다. 아까도 이야기했었죠? :)

mov ecx,크기
이곳에는 크기를 넣어 주시면 됩니다. 직접 수를 적어 주셔도 되고 변수로도 가능합니다. 만약 변수의 이름이 length 라면 mov ecx,length 이런 식으로 해주셔도 됩니다.

shr ecx,2
이것은 4로 나누어 주기 위한 코드 입니다. 오른쪽으로 2번 쉬프트는 4로 나눈것과 같다는건 다들아시죠? :)

mov edi,주소 or lea edi,변수

edi 에는 변수의 주소가 꼭 들어 가야 합니다. 즉 0으로 채워질 메모리 주소 입니다. mov는 뭔지 아시리라 보고 설명을 하지 않겠습니다, lea는 그 변수의 주소를 넣어 줍니다. 만약 메모리 변수 이름이 buff 라면(포인터 변수는 아닙니다.) lea edi,buff 로 해주시면 됩니다. 변수 타입에 따라 2개 중에 하나를 쓰시기 바랍니다.

rep stosd
가장 중요한 코드 인데 다시한번 자세히 설명해보도록합시다
굳이 풀자면 rep(eat) sto(re)s(tring)d(word)입니다.
반복해서(repeat) 저장해라(store) 문자열을(string) Dword로(Dword)
주 요는 edi에 eax를 ecx 만큼 저장 하라는 말입니다. Dword는 4바이트 입니다.

그러니까 eax전체가 edi의 주소로 복사가 됩니다.(eax는 4바이트이기 때문입니다.) 좀 부실 한것 같아서 재차 설명을 드리자면 eax는 4바이트 입니다
그런데 지금 위에서는 4바이트로 저장을 하죠? 그러니까 eax전체가 저장이 되는 것입니다.
만약 eax가 0x00FF00FF라면 그대로 0x00FF00FF가 저장이 되는 것입니다.

xor eax,eax
mov ecx,크기
mov edi,주소 or lea edi,변수

이부분은  

rep stosd

이 부분을 실행하기 위해 초기화 해준다고 생각 하시면 될 듯 합니다
자 이제는 나머지 4의 배수가 아닌 부분을 생각 해보죠. 이것도 보시면 아하 하실 정도로 간단 합니다.

mov eax,0
mov ecx,크기
mov edx,ecx
shr ecx,2
mov edi,주소 or lea edi,변수
rep stosd

mov ecx,edx
and ecx,03
rep stosb

입니다.
약간의 차이는 있지만 거의 비슷 하죠?
우선 추가된 코드를 보죠
mov edx,ecx 임시적으로 크기를 저장한것입니다.

mov ecx,edx
and ecx,03
이것은 4로 나눈 나머지를 구하는 것입니다.
어떻게 되는지는 생각 해보세요 :D(이런 무책임한!!)

rep stosb
rep(eat) sto(re)s(tring)b(yte)입니다. 대충 무슨 말인지 알겠죠?
부실하지만 여기까지 입니다
내친김에 이것을 매크로로 만들어 쓰시면 괜찮을듯 합니다 :D

이제 조건문에 대해서 한 번 알아 봅시다.
for (int i=0; i<10; i++)
{
  ...
}

우리가 C 에서 보는 가장 흔한 문장 중에 하나입니다.

for 문을 살펴보면,
1. 초기값 설정  ( int i=0; )
2. 조건부       ( i < 10; )
3. 반복부분     ( i++; )

이렇게 나눌 수 있다!

먼저, 초기값 설정부문 (int i=0;) 은
mov  eax, i
xor  eax, eax

로 바꾸면 될 듯 하네요.

다음 조건부 ( i < 10; )
복잡할 것 같으니 나중에 보도록합시다

반복부분 (i++;) 은
inc  i
대충 합한 코드를 보면서 조건부분을 살펴보도록하자.

mov eax, i      // 초기값 세팅 부분 (i=0;)
xor eax,eax     //
mov i,eax       //
loop_part:  <-- 이게 뭔지는 알겠지? (위치를 표시하는 라벨)
  ...
  ...
  ...
inc i           // 반복부분  (i++;)
mov edx,i       // 조건부분  (i<10;)
cmp edx, 0x0a   //
jl  loop_part   //


cmp 라는 명령어가 보이네요.
cmp = Compare (비교하라)
cmp edx, 10  ==  (edx - 10) 연산을 해서 플레그 값들을 세팅하게되므로
(edx-10)이 0보다 작으면 SF(Sign Flag,부호플래그)=1 이 될 것입니다.

jl = Jump Less (작으면 분기하라)

cmp edx, 0x0a
jl  loop_part ==  edx 가 0x0a 보다 작으면 loop_part로 이동하라.

결국 loop_part 부분을 반복하게 될것입니다.

여기서 조건관련 수식어들을 살펴 보도록하자.

JE  = Jump Equal     ( 같으면 분기하라)
JNE = Jump Not Equal (같지 않으면 ...)
JL  =          Less      ( 작으면 ... )
JNL =         Not Less  
JG  =         Greater   ( 크면 ... )
JS  =          Sign      ( (-)마이너스 이면 ... )
JZ  =          Zero      ( 0 이면... )

간단한 영어약자만 눈익힌다면 별거 아니다. (^_^)



while 문도 한 번 살펴 보자
While 문과 For 문과 비교해 보면 초기값설정부와 반복부분이 없으니 오히려 더 쉽지않을까?
그 외는 아마도 동일할 것이다.

while (i>0)
{
   ...
   ...
}

이를 코드로 바꾸면...

loop_part: (라벨)
...
...
mov   eax, i
test  eax,eax
jnle  loop_part


별루 설명할 것도 없겠지만
test = Logical Compare
test eax,eax 는 eax 와 eax를 논리적 AND 연산하여 그 결과로 플래그들을 세팅합니다.
 
여기서 하나 재밋는문제를 내보겠다 :D
jnle 와 jg 는 무엇이 다를까?

즉 "작지도않고 같지도 않을때(초과)일때 점프하라" 와 "클때 점프하라" 의 차이점은 무엇이냐는것이다.

답은 둘다 같은명령어입니다.
이와 비슷한 명령어들이 있습니다

JG  = JNLE
JLE = JNG
JNL = JGE
JNGE = JL
JPO = JNP
JP = JPE
JNBE = JA
JNA = JBE
JNZ = JNE
JE = JZ      // 비교하는 대상들의 차가 0이면 같은거겠죠?
JAE = JNB
JNAE = JB

는 서로 같습니다


앞에서는 if 문과 while문을 살펴 보았습니다
이번에는 switch 문을 한 번 살펴 보도록합시다

c 로 된 아래와 같은 코드가 있다고 생각해 보자!

  switch (x)
  {
    case 0:
            printf  ("강태공 0");
            break;
    case 2:
            printf ("강태공 2");
            break;
    case 3:
            printf ("강태공 3");
            break;
    case 8:
            printf ("강태공 8");
            break;
    default:
            printf ("강태공 default");
            break;
  };

이를 어셈으로 바꾸면 아래와 같이 됩니다.

mov   edx, x
sub   edx, 0x01
jb    case_0
dec   edx
jz    case_2
dec   edx
jz    case_3
sub   edx,0x05
jz    case_8
jmp   case_default

case_0: (라벨)
  ...

case_2: (라벨)
  ...

case_3: (라벨)
  ...

case_8: (라벨)
  ...

case_default: (라벨)
  ...


이제 구지 설명하지 않아도 잘 알것이라 믿습니다 :D

switch 문이라서 별다를것 같지만, 막상 코드를 보면 별 다른것이 없습니다.
실망하셨나요? :(

if/while/switch 문을 어셈으로 표현해 보면서
반대로 C 에서의 이들 구문의 특성을 알 수 있었습니다.
요즈음과 같이 펜티엄 4가 대세인 시대에 이들 구문들을 속도때문에 구분해서 사용한다는 것은 별 의미가 없을 수 있습니다
하지만, 혹시라도 속도를 고려해야할경우나, 좀더 손이더많이가는 구현이 필요한경우, 운영체제같은 프로그램을 작성할때라면 우리가 지금까지 한 이야기들은 절때 헛되지 않을것입니다 :)


마지막으로..

이 쯤에서, 정리할 겸해서 CPU 명령들을 한 번 전체적으로 살펴 봐야할 것 같습니다
간단히 눈으로도 알아맞출수있는 명령어들이 많습니다.
명령어들만 나열할테니 자세한 것들을 알아내는건 여러분들에게 맏기도록하겠습니다 :D

1. 데이타 전송 명령들

CMOVE/CMOVZ
CMOVNE/CMOVNZ
CMOVA/CMOVNBE
CMOVAE/CMOVNB
CMOVB/CMOVNAE
CMOVBE/CMOVNA
CMOVG/CMOVNLE
CMOVGE/CMOVNL
CMOVL/CMOVNGE
CMOVLE/CMOVNG
CMOVC
CMOVNC
CMOVO
CMOVNO
CMOVS
CMOVNS
CMOVP/CMOVPE
CMOVNP/CMOVPO
XCHG
BSWAP
XADD
CMPXCHG
CMPXCHG8B
PUSH
POP
PUSHA/PUSHAD
POPA/POPAD
IN
OUT
CWD/CDQ
CBW/CWDE
MOVSX
MOVZX


 

Trackback Address :: http://couple.haruschool.com/tc/trackback/226

댓글을 달아 주세요

  1. TTF 2008/02/09 15:12  address  modify / delete  reply

    강태공 naming sense가 멋지십니다. ㅎㅎ

  2. indra 2008/02/14 18:07  address  modify / delete  reply

    왔다가 그냥 티티에프가 보여서 흔적이라도... 커플이 화이팅...

  3. 듀르 2008/02/29 17:20  address  modify / delete  reply

    Intel64 Architecture에선 R?X 계열 레지스터가 나오죠. 64bit 크기입니다.
    Really ? eXtended 의 줄임말이라고 구글님이 말씀해주셨습니다.

    32bit E?X 는 Extended ? eXtended

    16bit ?X는 eXtended

    8bit ?L ?H High와 Low

    • Couple 2008/03/02 16:01  address  modify / delete

      감사합니다!
      앞으로 포스트 달때 관련내용이 나오면 참고해서 쓰도록하겠습니당!^_^

  4. 듀르 2008/02/29 17:21  address  modify / delete  reply

    아는척좀 해봤습니다. ㅋㅋㅋ

  5. 듀르 2008/02/29 17:23  address  modify / delete  reply

    그리고 Float 형 자료를 위한 레지스터도 나왔더군요... 모두 인텔홈피에서 매뉴얼을 다운로드 받아봅시다 +_+
    택배신청하면 인쇄된 책을 무료로 주더군요. 배타고 건너오더군요. 전 두박스나 받았습니다. 하나 남았음

  6. 듀르 2008/03/01 15:15  address  modify / delete  reply

    http://www.intel.com/products/processor/manuals/index.htm

    지금 바로 링크를 타고 가세요

    • Couple 2008/03/02 16:02  address  modify / delete

      한글로 봐도 잘모르는데 영어라니 ㅠ_ㅠ..
      좀더 공부한뒤에 보도록할께요~ 감사합니다~