티스토리 뷰

3.7 프로시저

프로시저 호출은 소프트웨어에서의 주요 추상화이다. 이들은 지정된 인자들과 리턴 값으로 특정 기능을 구현하는 코드를 감싸주는 방법을 제공한다. 프로시저는 서로 다른 프로그래밍 언어에서 함수, 메소드, 서브루틴, 핸들러 등 다른 모습으로 사용되지만 이 모두는 일반적인 특징들을 공유한다.
설명을 위해 프로시저 P가 Q를 호출하고, Q가 실행한 후에 다시 P로 리턴한다고 가정하자. 이러한 동작들은 다음과 같은 하나 이상의 메커니즘이 연관된다.

제어권 전달

  • 프로그램 카운터는 입력 시 Q에 대한 코드의 시작 주소로 설정되고, 리턴할 때는 Q에 대한 호출에 이어 P의 인스트럭션으로 설정되어야 한다.

데이터 전달

  • P는 하나 이상의 매개변수를 Q에 제공할 수 있어야하며, Q는 다시 P로 하나의 값을 리턴할 수 있어야 한다.

메모리 할당과 반납

  • Q는 시작할 때 지역변수들을 위한 공간을 할당할 수도 있고, 리턴할 때 이 저장소를 반납할 수 있다.

3.7.1 런타임 스택

프로시저 호출 동작방식의 주요 특징은 스택 자료구조가 제공하는 LIFO 메모리 관리 방식을 활용할 수 있다. 위의 예시를 보면 Q가 실행되고 있는 동안 P는 일시적으로 정지되는 것을 볼 수 있다. Q가 실행되는 동안에는 자신의 지역변수를 위한 새로운 저장공간을 할당할 수 있는 능력이나 다른 프로시저의 호출을 설정하는 능력만을 필요로 한다. 반대로 Q가 리턴할 때는 할당받은 저장소는 반납될 수 있다. 따라서 프로그램은 스택을 사용해서 프로시저들이 요구하는 저장장소를 관리할 수 있으며, 스택과 프로그램 레지스터들은 제어와 데이터를 전송하기 위해, 그리고 메모리를 할당하기 위해 필요한 정보를 저장한다.

프로시저가 레지스터들에 저장할 수 있는 개수 이상의 저장공간을 필요로 할 때 스택에 공간을 할당한다. 이 영역을 프로시저의 스택 프레임이라고 부른다.
위의 그림은 가장 일반적인 형태의 스택 구조를 보여준다. 현재 실행중인 프로시저에 대한 프레임은 항상 스택의 맨 위에 위치한다. 프로시저 P가 프로시저 Q를 호출할 때 리턴 주소를 스택에 푸시해서 Q가 리턴할 때 P에서 프로그램이 재시작해야 하는 위치를 가리킨다. 리턴 주소가 P에 관계된 상태들을 저장하기 때문에 리턴주소는 P의 스택 프레임에 속하는 것으로 간주한다. Q에 대한 코드는 자신의 스택 프레임을 위한 공간을 할당한다. 이 공간 내에서 레지스터 값들을 저장하고, 지역 변수들을 위한 공간을 할당하며, 자신이 호출하는 프로시저들을 위한 인자들을 설정할 수 있다.
시간과 공간 효율성을 위해 많은 프로시저들은 여섯 개 이하의 인자들을 가지며, 이들의 모든 매개변수들은 레지스터로 전달될 수 있다.
함수가 다른 함수를 하나도 호출하지 않을 때 스택 프레임을 요청하지 않는 상황이 발생한다. 이런 경우는 모든 지역변수들을 레지스터에 보관할 수 있다.

3.7.2 제어의 이동 (Control Transfer)

제어(Control)를 함수 P에서 Q로 전달하는 것은 프로그램 카운터를 Q의 시작 주소로 설정하는 것이다. 하지만 Q가 리턴해야 할 때 P의 실행을 다시 시작해야 하는 코드 위치의 일부 기록을 갖고 있어야 한다. 이러한 정보는 인스트럭션 call Q로 프로시저 Q를 호출해서 기록된다. 이 인스트럭션은 주소 A를 스택에 push하고 PC를 Q의 시작으로 설정한다. 푸시된 주소 A는 return address(리턴 주소)라고 불리며, call 인스트럭션 바로 다음 인스트럭션의 주소로 계산된다. 이에 대응하는 인스트럭션 ret는 주소 A를 스택에서 pop해오고 PC를 A로 세팅한다.

3.7.3 데이터 전송

호출 및 리턴할 때 제어를 전달하는 것 뿐 아니라, 프로시저 콜은 데이터를 인자로 전달하는 것과 관련이 있으며, 리턴하는 것도 어떤 값을 리턴하는 것과 관련되어 있을 수 있다.
위에서 최대 여섯개의 인자가 레지스터로 전달 될 수 있다고 했다. 이 레지스터들은 전달되는 데이터 형의 길이에 따라 레지스터 이름을 이용해서 정해진 순서로 이용된다. 이들은 인자 리스트에서 각자의 순서에 따라 이들 레지스터에 할당된다.

함수가 여섯개 이상의 정수형 인자를 가질 때, 다른 인자들은 스택으로 전달된다. 즉, n > 6인 n개의 정수형 인자를 가지면서 호출한다고 하면 인자 7에서 n까지를 위한 충분한 크기의 저장공간을 스택 프레임에 할당해야 한다. 그리고 이 인자들은 스택 탑에 넣는 방법으로 저장한다.

3.7.4 스택에서의 지역저장공간

지금까지는 레지스터에 저장할 수 있는 것 이상의 로컬 저장소를 요구하지 않았지만 때로는 로컬 데이터가 메모리에 저장되어야 하는 경우가 있다. 이런 경우는 다음의 경우를 포함한다.

  • 로컬 데이터 모두를 저장하기에는 레지스터의 수가 부족하다.
  • 지역변수에 연산자 &가 사용되었으며, 이 변수의 주소를 생성할 수 있어야 한다.
  • 일부 지역변수들이 배열 또는 구조체여서 이들이 배열이나 구조체 참조로 접근되어야 한다.

일반적으로 프로시저는 스택 포인터를 감소시켜 스택 프레임에 공간을 할당한다. 이렇게 하면 Local Variables로 명명된 스택 프레임의 일부분이 생겨난다.

스택 포인터를 감소시키는 것은 다음의 예시로 확인할 수 있다.

long swap_add(long *xp, long *yp)  
{  
   long x = *xp;  
   long y = *yp;  
   *xp = y;  
   *yp = x;  
   return x + y;  
}  

long caller(){  
    long arg1 = 534;  
    long arg2 = 1057;  
    long sum = swap_add(&arg1, &arg2);  
    long diff = arg1 - arg2;  
    return sum * diff;  
}

위 코드를 컴파일 하면 다음과 같은 내용을 포함한 어셈블리 코드를 확인할 수 있다.

_caller:                                ## @caller  
   .cfi_startproc  
## %bb.0:  
   pushq  %rbp  
   .cfi_def_cfa_offset 16  
   .cfi_offset %rbp, -16  
   movq   %rsp, %rbp  
   .cfi_def_cfa_register %rbp  
   subq   $16, %rsp            ## Allocate 16 bytes for stack frame
   movq   $534, -16(%rbp)
   movq   $1057, -8(%rbp)
   leaq   -16(%rbp), %rdi  
   leaq   -8(%rbp), %rsi  
   callq  _swap_add  
   movq   -16(%rbp), %rcx  
   subq   -8(%rbp), %rcx  
   imulq  %rcx, %rax  
   addq   $16, %rsp             ## Deallocate stack frame
   popq   %rbp  
   retq

주석 친 내용을 보면 subq 인스트럭션을 통해 16바이트의 스택 포인터를 감소시켜 스택 프레임에 공간을 할당받고 연산 마지막에는 addq 인스트럭션을 통해 16바이트의 스택 포인터를 다시 반환하는 것을 확인할 수 있다.

3.7.5 레지스터를 이용하는 지역저장소

프로그램 레지스터들은 모든 프로시저들이 공유하는 단일 자원의 역할을 한다. 어떤 한 순간에는 오직 하나의 프로시저만이 활성화될 수 있지만, 하나의 프로시저(호출자)가 다른 프로시저(피호출자)를 호출할 때, 피호출자는 호출자가 나중에 사용할 계획인 일부 레지스터 값은 덮어쓰지 않는다.

long P(long x, long y)
{
    long u = Q(y);
    long v = Q(x);
    return u + v;
}

이 코드의 함수 P에 대해 생각해보자. 이 함수는 Q를 두번 호출한다. 첫 번째 호출 동안에 x 값을 다음에 사용하기 위해 유지해야 한다. 그리고 두 번째 호출 동안에 Q(y)를 위해 계산된 값을 유지해야 한다.
개념적으로는 헷갈릴 수 있으나 이렇게 생각하면 이해하기 쉬운 것 같다.

3.7.6 재귀 프로시저

레지스터와 스택을 사용하는 것에 대한 컨벤션으로 이들을 재귀적으로 호출하는 것을 설명할 수 있다. 각 프로시저 콜은 스택상에 자신만의 공간을 가지기 때문에 다수의 다른 호출에서 사용되는 지역변수들은 서로 간섭하지 않는다. 뿐만아니라 스택 운영 방식은 프로시저가 호출될 때 로컬 저장소를 할당하고, 리턴하기 전에 이것을 반환하는 적절한 정책을 자연스럽게 제공한다. 다음의 예시를 보자.

long rfact(long n)  
  {  
      long result;  
      if (n <= 1)  
          result = 1;  
      else  
          result = n * rfact(n-1);  
      return result;  
}
_rfact:                                 ## @rfact  
   .cfi_startproc  
## %bb.0:  
   movl   $1, %eax  
   cmpq   $2, %rdi  
   jl LBB0_2  
## %bb.1:  
   pushq  %rbp  
   .cfi_def_cfa_offset 16  
   .cfi_offset %rbp, -16  
   movq   %rsp, %rbp  
   .cfi_def_cfa_register %rbp  
   pushq  %rbx  
   pushq  %rax  
   .cfi_offset %rbx, -24  
   movq   %rdi, %rbx  
   addq   $-1, %rdi  
   callq  _rfact             ## Call rfact(n-1)
   imulq  %rbx, %rax  
   addq   $8, %rsp  
   popq   %rbx  
   popq   %rbp  
LBB0_2:  
   retq

이 예제로부터 함수를 재귀적으로 호출하는 것도 다른 함수의 호출과 마찬가지로 진행되는 것을 알 수 있다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함