프로시져는 함수와 비슷한 의미로 이해할 수 있다.
프로그램을 실행하기 위한 하나의 부분으로 ‘인자’를 받아서 특정 기능을 수행하고 ‘반환값’을 반환하는 역할을 한다.

이러한 역할은 일종의 추상화(abstraction)으로 볼 수 있다.
프로시저가 담당하는 역할을 실제로 구현하기 위해서는 프로시저 내에서 여러 복잡한 구현이 이루어져 있을 수 있지만, 이를 사용하는 프로그래머는 그것의 기능만 생각하고 사용할 수 있다. 즉, 여러 복잡한 단계들을 하나의 기능을 담당하는 역할로 추상화했다고 보는 것이다.
(프로시저는 여러 언어에서 함수, 메소드, 서브루틴, 핸들러 등으로 불릴 수 있지만 특징들은 비슷하다)

우리는 기계 수준에서 프로시져를 어떻게 다루는지 보려는 것이다. 이걸 보기 위해 다음과 같이 프로시져 PQ를 호출하고 이로 인해 Q가 실행되고 반환하여 다시 P로 돌아오는 상황을 가정해본다. 이러한 상황은 다음의 메커니즘을 통해 이루어진다(각각의 메커니즘이 여러 번 이루어질 수도 있다).

  • 제어권 전달(Passing control) : 프로그램 카운터(다음 실행할 명령어의 주소가 기록된 곳)가 Q코드의 주소로 설정되어 Q로 진입할 수 있게 하고, Q가 반환(종료)할 때는 Q를 호출하는 바로 다음 명령의 주소로 프로그램 카운터가 설정되어야 한다.
  • 데이터 전달(Passing data) : P는 한 개 이상의 매개변수를 Q에게 넘길 수 있어야 하며, QP에게 값을 반환할 수 있어야 한다.
  • 메모리 할당과 반납(Allocating and deallocating memory) : Q는 지역 변수를 위해 시작할 때 공간을 할당하고 반환하기 직전에 공간을 반납해야 한다.

이를 x86-64프로세서에서는 레지스터와 프로그램 메모리 등을 활용하여 효율적으로 이를 구현한다.

여기서는 제어, 데이터 전달, 메모리 관리의 순서로 이를 구체적으로 볼 것이다.


3.7.1 런타임 스택(The Run-Time Stack)

여러 언어에서 프로시져를 호출하는 메커니즘의 특징은 스택으로 대표되는 후입선출(LIFO; Last-In, First-Out)의 메모리 관리 원칙이다.

앞에서 보았듯이 PQ를 호출하였을 때 P는 잠시 일시정지한다. 그리고 Q는 전달받은 인자만을 가지고 Q만의 지역변수를 위한 공간을 할당받아 일을 처리하고 프로시져를 종료할 때는 메모리를 다시 반납한다. 즉, Q와 관련된 일을 처리할 때는 가장 마지막(후입)에 Q를 위해 할당된 공간만을 사용하고 P보다 먼저 메모리를 반납(선출)하게 된다.
\(\therefore\) 후입선출의 스택 자료 구조를 통해 프로시져 메모리 관리를 할 수 있다. PQ를 호출하면 스택의 끝에 데이터 정보가 추가되고 Q가 반환할 때 정보는 스택에서 해제된다.

3.4.4에서 보았듯이 x86-64에서 스택은 낮은 메모리 주소의 방향으로 자라고, 스택 포인터 %rsp는 스택의 최상위 원소(가장 낮은 주소)를 가리킨다.
스택에 데이터를 저장하는 행위는 pushqpopq명령을 통해 실행된다. 아직 데이터 값을 알지 못해서 초기화를 못하지만 공간만 할당하려면 스택 포인터의 값을 낮춰서 할당된 공간을 늘리는 효과를 볼 수 있고, 반대로 공간을 해제할 때는 스택 포인터의 값을 높여서 할당된 공간을 줄이는 효과를 볼 수 있다.

x86-64의 프로시져에서 레지스터보다 더 많은 공간을 요구하면 스택에 공간을 할당하고 이를 프로시져의 스택 프레임이라 부른다.

다음은 일반적인 스택 프레임 구조이다.
stack_frame_structure
현재 실행하는 프로시져의 프레임은 항상 스택의 꼭대기(top of the stack)에 있다(그림에서는 아래를 가리키고 있어서 헷갈리지 않게 주의. 스택의 꼭대기(가장 나중에 들어온 원소)는 메모리 상에서는 보통 더 낮은(작은) 주소).

우선 Q의 지역 변수들을 Q에 할당된 스택 프레임에 저장한다.
또한 Q를 실행하면서 P에서 썼던 레지스터의 값을 덮어 씌울 수 있기 때문에 몇몇 레지스터 값은 Q의 스택 프레임에 저장한다.

반면 Q가 종료하여 반환할 때 P에서 실행하게 될 코드의 위치 반환 주소(Return address)P와 관련된 것이므로 P의 스택 프레임에 저장한다.
또 새 프로시져가 요구하는 7번째 이상의 인자는 P의 스택 프레임에 저장된다. 기본적으로 프로시져는 6개의 인자까지는 레지스터에 저장할 수 있다.

이러한 스택 프레임은 프로시저가 시작될 때 고정된 크기를 할당받는다.
하지만 몇몇 경우(VLA(가변 길이 배열)와 같은 경우)에는 크기가 변할 수 있는 스택 프레임이 필요하고 이에 대해서는 3.10.5에서 다룰 것이다.


3.7.2 제어의 이동

제어의 이동은 가장 익숙한 용어로 설명해보면 어떤 함수 P에서 다른 함수 Q를 호출하는 것이고, 이는 결국 다음 명령을 실행할 주소(프로그램 카운터; PC)를 함수 Q의 시작 주소로 설정하는 것이다(프로세서는 프로그램 카운터를 참고하여 다음 명령을 실행한다).

이러한 동작을 위해 x86-64머신에서는 call Q의 명령을 통해 프로시저 Q를 호출한다.
call Q를 실행하면 현재 call Q를 실행한 주소의 다음 주소 A를 스택에 푸시하고(나중에 Q가 끝났을 때 돌아오기 위해), 프로그램 카운터를 Q의 시작 주소로 설정한다.
여기서 푸시된 주소 A를 리턴주소라 부르고 이는 call명령 바로 다음 명령의 주소이다.

callret 명령의 일반적 형태는 다음과 같다.

명령 설명
call Label 프로시저 호출
call *Operand 프로시저 호출
ret 호출에서 반환
(objdump를 통해 역어셈블하는 경우 보통 callqretq로 써지는데 q가 다른 의미를 갖는 것은 아니고 단순히 IA32가 아닌 x86-64버전의 명령임을 강조할 뿐이다.)  
call명령의 타겟은 호출된 프로시져가 시작하는 명령의 주소이다.  
어셈블리 코드에서 직접 호출의 타겟은 레이블로 주어지고, 간접 호출의 목적지는 ‘*‘과 3단원 초반에 그림 3.3으로 주어졌던 피연산자의 형태가 붙어있는 형태로 주어진다.  

다음은 3.2.2절에 나왔던 mainmultstore함수와 관련하여 callret명령의 실행을 보여준다.
call_ret00
call_ret01
우선 400563에서 multstore를 호출하면 %rsp에 저장되어 있는 주소값이 8바이트 감소하여 스택 크기를 늘려준다. 또한 %ripmultstore첫번째 명령의 위치로 설정하여 다음 명령은 multstore를 실행할 수 있게 해준다. 또한 multstore가 끝났을 때 돌아올 위치를 스택에 푸시해준다.

call and return
새로운 예시를 통해 명령과 스택 프레임이 어떻게 변화하는지 살펴 보자.
(표에서 state values는 명령이 실행되기 직전의 값이다.)

우선 callq를 통해 top(100)을 호출하기 직전에 이미 %rdi에 인자로 넘겨줄 값 100이 들어있다. 이때 %rsp는 아직 늘어나지 않았다.

그 후 callq가 실행되면 스택포인터는 8바이트 감소하여 스택이 증가하고 늘어난 스택의 자리(*%rsp에는 top(100)이 끝나고 실행할 명령의 주소가 저장된다.

그 다음 top(100)에서 sub를 통해 다른 일을 하고 또 다시 callq를 통해 leaf(x - 5)를 호출한다.
여기서도 마찬가지로 스택포인터가 감소하고 그 자리에 반환시 돌아올 명령의 주소가 기록된다.

마지막 leaf에서는 반환하기 전 명령에서 반환할 값을 %rax에 저장하게 되고, 그 다음 번 명령인 ret을 통해 반환을 시행한다. 반환을 수행하고 나면 프로그램 카운터는 스택에 저장되어 있던 명령어의 주소로 바뀌게 되어서 다음 명령은 top에서 callq명령어의 다음 줄에 있던 명령을 실행하게 되고 스택포인터도 증가하여 스택의 크기도 다시 원래대로 감소한다.
그 뒤에 있는 ret도 비슷한 방식으로 동작한다.


3.7.3 데이터 전송

프로시져를 호출할 때 단순히 명령의 제어를 넘기는 것 외에 데이터 전송도 일어날 수 있다.
함수에 인자를 넘기고 함수로부터 반환값을 받는 행위 등은 모두 데이터 전송에 해당한다.
x86-64에서 프로시져 간에 데이터를 전송하는 행위는 보통 레지스터를 이용해서 이루어진다.
data transfer
이 때 사용되는 레지스터는 표에 나온 순서대로 사용되며 인자 크기가 64비트보다 작은 경우 표에 나온 32, 16, 8에 해당하는 레지스터 이름으로 접근된다.

호출하는 프로시져에 넘겨야 할 인자가 6개를 초과하는 경우 호출하는 프로시져의 스택 프레임에 저장해야 한다.
PQ를 호출하는데 n개의 인자를 넘겨야 하는 경우 7번째 인자부터 n번째 인자는 P의 스택에 저장해야 하고 스택의 top에는 7번째 인자를 저장하게 된다.

arguments
위의 예시에서 proc은 6개의 인자를 필요로 하고 스택에는 7, 8번째 인자인 a4pa4가 저장된다.


3.7.4 스택 내의 지역 저장 공간(Local Storage on the Stack)

지역 변수들을 레지스터에 저장하여 처리할 수도 있지만 다음과 같은 경우에는 메모리에 저장하여야 한다.

  • 레지스터에 지역 데이터를 전부 담을 수 없는 경우
  • 지역 변수에 대해 &연산자가 사용되는 경우 메모리 주소를 돌려줘야 하기 때문에 메모리에 저장해야 한다.
  • 배열이나 구조체 같은 경우에 배열/구조체 참조 방식을 통해 접근되어야 하기 때문에 메모리에 저장되어야 함(뒷 절에서 배움)
    local variable
    위의 예시는 caller의 지역변수 arg1arg2의 주소를 넘겨줘야 하는 예시이다.
    이 때 %rsp에 대해 subq를 실행하여 두 지역 변수를 위한 스택 공간을 마련하고 movq를 통해 지역변수의 값을 저장한다.
    그 다음 caller()내의 나머지 명령을 수행하고 나서 %rsp에 대해 addq를 실행해서 지역 변수를 위해 할당된 스택 공간을 해제한다.
    (스택 공간 할당은 함수에서 필요한 순간에 할당하고 필요가 없어지면 해제하는 듯. 흔히 컴파일 단계에서 스택 공간이 결정된다고 말하는 것은 이러한 스택 할당 명령들이 컴파일 단계에서 기계어 코드를 작성하는 순간에 결정되기 때문?)

local variable2
local variable3
위의 예시도 지역변수를 위해 스택 공간을 할당하고, 인자를 레지스터에 넘기는 과정을 보여준다.(7, 8번째 인자는 스택에 저장하는 행위가 인자를 넘기는 행위라 보면 될 것 같다)


3.7.5 레지스터에 있는 지역 저장 공간(Local Storage in

Registers)

레지스터는 모든 프로시져가 공유.
따라서 한 프로시져가 레지스터를 수정하면 다른 프로시져에서도 영향이 간다.
프로시져를 호출한 프로시져(caller)가 피호출된 프로시져(callee)가 레지스터 값을 바꿈으로 인해 필요한 데이터에 접근하지 못하게 되는 경우를 막기 위해 x86-64에서는 레지스터 사용에 대한 몇 가지 관습을 채택하고 있다.

우선 %rbx, %rbp, %r12-%r15callee-saved registers(피호출자 저장 레지스터)로 불린다.
프로시져 PQ를 호출한 경우에 Q가 종료하는 경우에 PQ를 호출했던 때와 동일하게 피호출자 저장 레지스터에 있는 값들을 원상 복구해놓아야 한다.
프로시져 Q

  • 그냥 해당 레지스터의 값을 건들지 않거나
  • 원래 값을 스택에 푸시하였다가 프로시져 반환 직전에 스택에서 다시 팝해오는 방식으로
    피호출자 저장 레지스터의 값을 보존한다.

나머지 레지스터 중 스택 포인터 %rsp를 제외한 모든 레지스터는 caller-saved registers(호출자 저장 레지스터)로 불린다.
호출당하는 프로시져 Q는 이 레지스터를 바꿔도 되기 때문에 P가 해당 레지스터에 있는 값을 Q의 호출 뒤에도 똑같은 값으로 사용하고 싶다면 먼저 저장할 의무가 있다.
register save
예시를 보면 프로시져 P%rbp%rbx를 보존할 의무가 있으므로 스택에 푸시하여 값을 저장한다.
그 후 다음번 Q(x)의 호출을 위해서 x의 값을 보존해야 하므로 %rbp에 저장하여 Q가 이 값을 보존하게 만든다.


3.7.6 재귀적 프로시져(Recursive Procedures)

recursive

재귀도 별 다를 거 없다. 그냥 똑같은 스택 생성 관습을 따를뿐이다. 단지 동일한 프로시져를 호출한다는 점만 특별할 뿐이다.