IT_Programming/Assembly

[전광성의 어셈블리어 이해하기] 프로시져(Procedure)

JJun ™ 2007. 7. 2. 10:34
LONG
  • 프로시져(Procedure)

    고급언어를 공부했다면, 프로그램을 함수(function)로 잘게 쪼개어 작생하는 것이 얼마나 유용한지에 대해 잘 알고 있을 것이다. 해결해야할 복잡한 문제들을 작은 부분들로 쪼개어 하나하나 풀어간다면 프로그래밍이 한결 쉬워진다. 어셈블리에서는 이를 프로시져(Procedure)라고 부른다.

  • PROC 디렉티브

    프로시져의 정의란, 고급언어에서 함수를 정의하는 것과 같다. 먼저 우리가 앞선 예제들에서 나오던 main 프로시져를 생각해 보자. 기억이 나지 않을 테니 main프로시져를 정의했던 예를 적어보겠다.

      main PROC
      .
      .
      main ENDP

    프로시져의 정의는 "프로시져이름 PROC"으로 시작해서 "프로시져이름 ENDP"로 끝난다. 한가지 유의할 사항은 프로시져를 정의할 때 "프로시져이름 ENDP"앞에 함수의 끝을 나타내는 ret 인스트럭션을 반드시 적어 주어야 한다는 것이다. 그런데 가만보자. 방금 들었던 main PROC의 예에서는 ret인스트럭션을 적어주지 않았다. 왜일까? main은 프로세스가 시작되는 특별한 프로시져이기 때문에 ret대신 프로세스를 종료시켜주는 프로시져를 호출해 주어야 한다.

    이렇게 말해서는 제대로 느낌이 오지 않을 듯 싶다. 그래서 eax,ebx,ecx에 들어있는 값들의 합을 eax에 넣어주는 프로시져를 정의해보겠다.

      SumOf PROC
             add eax, ebx
             add eax, ecx
            ret
      SumOf ENDP

    그렇다면 프로시져의 호출은 어떻게 할까? 이전에 잠깐 봤던 call 인스트럭션을 기억하는가?

  • CALL 인스트럭션

    방금 정의한 프로시져를 이용해 세 수를 더하는 예제를 작성해 보겠다.

      mov eax, 1000h
      mov ebx, 2000h
      mov ecx, 3000h
      call SumOf         ; eax = 6000h

    보다시피 우리가 정의해 두었던 프로시져를 호출하려면 "call 호출할프로시져이름"을 적어주기만 하면 된다.

  • CALL 인스트럭션이 하는 일

    본 회의 앞부분에서 함수호출에 대해 간단히 언급하였다. 이번에는 함수호출이 어떻게 이루어 지며, 어떻게 해서 호출환경으로 제어가 넘어가게 되는지에 대해 조금 더 자세히 알아보자. 먼저, 다음과 같은 코드가 있고, 각 인스트럭션 앞에 있는 것이 해당 인스트럭션의 주소라고 하자.

                    main PROC
           00000020 call SumOf
           00000025 mov eax, ebx
                     .....
                    main ENDP

           .....

                    SumOf PROC
            00000040 mov add eax, ebx
                    .....
                    ret
                    SumOf ENDP

    <그림 2 : 프로시져 호출 구조>


    먼저 main프로시져 내에서 call인스트럭션을 만났을 때, 그림 2-(1)과 같이 프로시져가 호출된 다음에 수행할 인스트럭션 주소(25)가 스택에 push된다. 그 다음 인스트럭션 포인터(EIP)에는 호출한 함수가 시작되는 부분의 주소(40)가 들어간다. 따라서 다음의 수행은 EIP에 들어있는 주소의 코드부터 하게 된다. 그러다가 ret인스트럭션을 만나면 그림 2-(2)와 같이 스택에서 아까 저장해둔 값(25)을 pop하여 EIP에 넣게 된다. 따라서 다음에 수행할 인스트럭션은 mov eax, ebx가 된다. 이런 식으로 프로시져는 호출되며 리턴된다.

  • 중첩된 프로시져 호출하기

    함수가 중첩되어 호출되는 상황을 좀 더 쉽게 이해할 수 있게 하기 위해 간단한 예를 들어 보이겠다.

      main PROC
              .
              .
              call Sub1
              exit
      main ENDP

      Sub1 PROC
              .
              .
              call Sub2
              ret
      Sub1 ENDP

      Sub2 PROC
              .
              .
              call Sub3
              ret
      Sub2 ENDP

      Sub3 PROC
              .
              .
              ret
      Sub3 ENDP

    Sub3프로시져가 호출되었을 때는 스택이 그림 3과 같이 되어 있을 것이다.

    < 그림 3 : 스택 >


    이제 스택이 함수 호출에 왜 필수적인지 이해가 가는가?
  •  

    ============================================================================================

     

     

  • USES 연산자
    프로시져를 호출하고 나면 레지스터가 변경되곤 한다. 예를 들어 main프로시져에서 eax에 3000이라는 값을 넣어두었는데 한 프로시져를 호출한 후에는 0으로 변해 있을 수 있다.

    레지스터를 변수나 스택에 넣어두었다가 다시 가져오면 이런 문제를 해결할 수 있을 것이다. 매크로 어셈블러는 스택에 레지스터 변수를 넣었다가 함수호출이 끝나면 다시 읽어들이는 방법을 제공해 준다. 다음을 보자.

         ArraySum PROC USES esi ecx
             mov eax, 0

         L1:
             add eax, [esi]
             add esi, 4
             loop L1
             ret
         ArraySum ENDP

    위와 같이 USES연산자를 사용한다면 다음과 같은 효과를 갖는다.
        
        ArraySum PROC
            
    push esi
            push ecx

             mov eax, 0

         L1:
             add eax, [esi]
             add esi, 4
             loop L1
            
    pop ecx
            pop esi

             ret
         ArraySum ENDP

    esi를 넣은 후 ecx를 넣었으니, 나중에 집어넣은 값을 먼저 빼야 하므로 ecx에 pop한 후 esi에 pop해야 한다. 이렇게 하면 프로시져를 호출하기 전에 esi와 ecx에 넣어두었던 값이 함수가 리턴된 후에도 변경되지 않을 것이다.

    USES연산자를 사용할 때 조심해야 할 점은, 리턴값으로 사용할 레지스터 변수는 절대 USES 뒤에 써주지 말아야 한다는 것이다. 프로시져가 호출되기 전과 후의 레지스터값이 같도록 보장되어야 하는 경우에만 USES를 써야 한다. 한 레지스터에 프로시져가 수행된 결과값이 들어가게 될 경우 USES사용에 조금 더 신경써주길 바란다.

  • 마치는 글
    본 회에서는 프로시져를 정의하고 호출하는 방법에 대해서 공부해 보았다. 이제 어셈블리어로 프로그램을 짤 때 메인에 몰아넣는 일은 없어야 할 것이다. 프로시져 구조를 잘 짜는 것은 아무리 강조해도 지나치지 않다. 혹시 이번 회에서 고급언어와 같이 파라미터도 넘기는 함수호출은 없냐는 궁금증을 가질 분이 계실지 모르겠다. 그것은 8회에서 설명할 것이니 기다려주기 바란다.

    다음 회에서는 조건처리에 대해 배울 것이다. 고급언어에서 if-else문, WHILE문과 같은 것을 직접 어셈블리어로 짜려면 어떻게 해야 할지 다음 회를 기대하시라.

  •  
     
    ARTICLE
  • 시작하기에 앞서...

    아무리 저급언어라고 해도, 함수는 존재한다. 함수는 특정 코드가 반복될때, 이를 따로 떼어내어 만들어 두고 필요할 때 호출함으로써 코드의 길이를 줄일 수 있다. 또한 함수는 지나치게 긴 코드를 보기좋고 깔끔하게 만들어 주기도 하며, 함수의 이름을 잘 짓는다면 문서화에도 도움이 된다. 어셈블리어에서는 함수를 이용할 수 있는 직접적인 명령(Instruction)은 없지만, 디렉티브(Directive)를 갖고 있다. 하지만 그 이전에, 먼저 런타임 스택(Runtime Stack)을 알아야 한다.

    본 강좌의 목적은 어셈블리어 활용보다는 '이해'에 중점을 두고 있으므로, 어떻게 함수 호출이 이루어지는지를 이해하는데에 많은 분량을 할당하려고 한다.

  • 함수의 동작 원리

    함수의 동작 원리라고 하니 뭔가 거창해 보이지만 사실 별 것 아니다. 그저 스택이라는 자료구조를 이용해 함수 호출이 가능하다는 것을 이야기 하고 싶을 뿐이다. 함수를 호출하게 되면 제어가 그 곳으로 넘어가야 하니까, 이전의 인스트럭션 포인터(EIP)레지스터를 어딘가에 저장시켜놨다가 나중에 다시 불러와야 할 것이다. 그런데 함수 여러개가 중첩되어 호출되었다면, 인스트럭션 포인터를 저장하는 자료구조는 가장 나중에 들어간 자료가 가장 먼저 나와야 올바르게 수행될 것이다. 즉, LIFO(Last-In, First-Out)여야 한다. 그렇다면 스택을 사용하여야 한다는 것은 두말할 필요도 없을 것이다.

  • 런타임 스택(Runtime Stack)

    런타임 스택이란, 메모리를 이용하여 CPU에 의해 직접 관리되는 스택이다. 이는 프로시져(Procedure)를 호출하고 리턴하는데 있어서 필수적인 메커니즘이다. 위에서는 쉬운 이해를 위해 함수라고 하였지만, 개념이 비슷할 뿐 어셈블리어에서는 프로시져(Procedure)라고 한다. 런타임 스택은 항상 ESP(Extended Stack Pointer)레지스터를 이용하여 top을 가리키게 된다. 우리는 보통 스택의 개념을 설명할때 '쌓음'의 원리를 이용하여 아래서부터 위로 쌓여가는 것으로 설명하곤 한다. 하지만 IA-32아키텍쳐에서 스택은 아래로 자란다. 즉, 스택을 거꾸로 뒤집어 놓은 것이다. 별다른 이유없이 그렇게 된 것이니 너무 깊이 생각할 필요는 없다. 다음의 그림을 보면 이해가 빠를 것이다

    <그림 1 : push동작>


  • PUSH, POP 인스트럭션

    독자가 스택이라는 자료구조에 알고 있다는 가정하에 설명하겠다. 어셈블리어에서는 다른 고급언와 달리 직접 런타임 스택에 PUSH/POP할 수 있다. 사용방법은 매우 간단한다. push의 피연산자로는 16비트나 32비트 메모리, 레지스터, 그리고 임의의 상수가 올 수 있다. 임의의 상수를 사용한다면 32bit로 스택에 저장된다. 이 인스트럭션은 피연산자를 스택에 집어넣게 될 것이다.

    pop인스트럭션의 피연산자로 올 수 있는 것에는 16비트나 32비트 메모리, 레지스터가 있다. pop인스트럭션은 스택에서 하나를 빼와 피연산자에 집어넣는다. 피연산자가 16bit면 스택에서 16bit만 빼올 것이고, 피연산자가 32bit면 스택에서 32bit만 빼올 것이다. 다음의 사용예들을 참고하여라.

      .data
      value DWORD 0h
       .code
             mov eax, 3000h
             push eax
             push 0FFFFFFFFh
             pop value ; 0FFFFFFFFh
             pop edx ; 3000h


  • PUSHFD, POPFD 인스트럭션

    이름에 push, pop이 들어가 있으니 뭔가 런타임 스택과 관련있는 인스트럭션이라는 감이 올 것이다. f는 flags라는 뜻이고, d는 더블워드(32bit)라는 뜻이다. push doubleword flags 라고 풀어보면 무슨 뜻인지 이해가 갈 것이다. 32bit의 EFLAGS레지스터의 내용을 모두 스택에 push하라는 뜻이다. popfd는 마찬가지로 스택에서 32bit를 pop하여 EFLAGS레지스터에 넣으라는 뜻이다. 두 인스트럭션 모두 피연산자는 받지 않는다.

    이런 인스트럭션이 어디에 사용될까? 어셈블리어에서는 어떤 인스트럭션을 수행한 후 세팅되는 플래그에 따라 제어가 분기되는 경우가 많다. 허나 그 인스트럭션과 제어가 분기되는 곳 사이에 다른 인스트럭션이 들어가서 플래그를 변경시켜 버린다면 어떻게 될까? 프로그래머가 의도하지 않은 결과가 나올 것이다. 이럴 때, pushfd해 두었다가 제어를 분기하기 전에 popfd하면 문제가 해결될 것이다.