프로시저(Procedures)는 코딩 영역 내에서 다양한 개념으로 통용되고 있는 용어이지만, 이 문서에서는 인텔에서 개발한 아키텍쳐에 최적화된 코드 중 x86-64 isa 어셈블리 언어 내에서 사용되는 용어로써 기술하겠다.
프로시저는 한 마디로 어셈블리 언어에서 쓰는 함수이며, 사용 문법은 다음과 같다.
P_procedure:
subq $16, %rsp ; 16바이트 스택 프레임 P 할당
movl $6, %edi ; 첫 번째 인자를 %edi 레지스터에 저장
movl $12, %esi ; 두 번째 인자를 %esi 레지스터에 저장
call Q_procedure ; Q_procedure 프로시저 호출
addq $16, %rsp ; 스택 프레임 해제
ret
Q_procedure:
subq $16, %rsp ; 16바이트 스택 프레임 Q 할당
addq %edi, %esi ; %edi와 %esi 값을 더하여 %esi에 저장
; 여기서부터는 필요에 따라 다른 작업 수행 또는 결과를 활용
addq $16, %rsp ; 스택 프레임 해제
ret
어셈블리에서는 한 줄에 한 명령만 수행하기 때문에 함수(프로시저)를 call 명령문을 사용하여 딱 한 줄로 호출한다. 하지만 개발 커뮤니티에서 굳이 함수가 아닌 프로시저라고 부르는 이유는 이 한 줄에 어떤 인자(argument 혹은 parameter)도 받지 않으며, P와 Q가 한 함수로써 기능(Function)하기 위해 수행해야 할 명령이 너무 저수준(low-level)이기 때문이다 라고 받아들였다.

위는 프로시저(Procedures)와 함수(Function) 사이의 차이를 정리한 표이다.

프로시저를 호출하면 스택 구조를 따라 메모리 관리가 된다. 주의점은 자료구조에서의 스택과 방향이 반대라는 점이다. 스택 포인터로부터 값을 빼주어야 메모리 할당(subq $16, %rsp)이 되고, 반대로 값을 더하면 메모리 반납(addq $16, %rsp)이 된다.
또한 Q 프로시저에 대한 return address는 P 스택 프레임에 쌓인다는 것이다. 왜냐하면 Q 프로시저에서의 작업이 끝나고 프로그램 카운터가 ret을 만나면, 원래 호출했던 P에서 기억했던 위치로 되돌아가야 하기 때문이다.

포탈(PORTAL) 게임으로 비유하자면 다음과 같다. 한 챕터 내의 한 방(P_procedure)에 상자를 놓는 버튼이 있고 상자를 구하기 위해 다른 방(Q_procedure)으로 가려고 하는데, 그 방으로 가는 길이 유리로 막혀 있다. 이 때 P 방에 파란 포탈을 뚫고, 유리에 뚫린 창문으로 Q 방에 주황 포탈을 뚫는다. 이제 주인공은 포탈을 통해 P에서 Q로 점프했고, P 방에는 여전히 파란 포탈이 남아있다. 주인공이 상자를 집고 다시 포탈을 통해 P 방으로 점프한 뒤, P 방에서 계속해야 되는 작업(상자를 버튼 위에 놓음)을 수행한다. 여기서 파란 포탈이 Q 프로시저에 대한 return address인 것이다. 물론 Q가 리턴하면 P 스택 프레임 내의 return address도 자동으로 반납된다.
이제 다음 어셈블리 코드를 보자.
P_procedure:
subq $16, %rsp
call Q_procedure ; **Q_procedure 프로시저 호출**
addq $16, %rsp
ret
Q_procedure:
subq $16, %rsp
; 작업 수행 혹은 결과를 활용
ret
우리가 알던 파이썬의 def나 c의 function에 비교하면 Q 프로시저 호출 시 주고받는 인자가 없어보인다. 어셈블리 언어에서는 레지스터를 활용하여 인자를 인풋해주고, 결과값을 리턴한다.
다시 아래의 어셈블리 코드를 봐보자. 해당 코드는 P 프로시저에서 상수 6과 12를 Q 프로시저로 인자로써 넘겨주고 Q 프로시저 내부에서는 넘겨받은 두 상수 간을 더하고 뺀 각 두 값을 다시 P 프로시저로 넘기고 있다.
P_procedure:
movl $6, %edi ; 첫 번째 인자를 %edi 레지스터에 저장
movl $12, %esi ; 두 번째 인자를 %esi 레지스터에 저장
call Q_procedure ; Q_procedure 프로시저 호출
; 이 부분에서 %rax와 %rdx에 저장된 값을 사용
ret
Q_procedure:
movl %esi, %edx ; %esi의 값을 복사하여 %edx에 저장
addq %edi, %esi ; %edi와 %esi 값을 더하여 %esi에 저장
subq %edi, %edx ; %edx에 %edi 값을 뺀 후 %edx에 저장
movq %esi, %rax ; %esi의 값을 복사하여 %rax에 저장
movq %edx, %rdx ; %edx의 값을 복사하여 %rdx에 저장
ret
위처럼 %뒤에 알파벳이 붙으면서 소괄호()로 감싸여있지 않은 명령문이 모두 레지스터에 접근하는 명령문이다. e로 시작하는 명령문은 32비트 레지스터 저장소에 접근하도록 하고, r로 시작하는 명령문은 64비트 레지스터 저장소에 접근하도록 한다.
movl $머시기 %e저시기 명령문은 머시기 상수를 32비트 레지스터의 저시기 메모리 주소에 저장하라는 명령문이고, call 명령문으로 Q_procedure 를 호출하여 스택 포인터를 감소시킨다. 제어권이 Q_procedure로 넘어오고 프로그램 카운터가 Q로 넘어가면서 레지스터의 저시기 메모리 주소에 접근해 두 머시기 상수를 받아오는 작업을 수동으로 구현한다. (이 부분은 최소 c언어에서도 자동으로 처리해주기 때문에 c언어가 저수준이 아니라는 방증이다.)
구한 두 값이 각각 %esi와 %edx에 갱신되어 있을텐데, 그 두 값을 64비트 레지스터 메모리 주소인 r어쩌구에 각각 저장한다. 그리고 ret을 만나면 Q의 지역저장변수가 포함된 Q 스택 프레임이 팝핑되고, P 스택 프레임 내의 Q에 대한 return address가 팝핑되면서 동시에 call 명령을 받은 직후의 줄로 프로그램 카운터도 돌아온다. 이제 P_procedure에서도 %rax에 저장된 6 + 12 = 18 상수와 %rdx에 저장된 12 - 6 = 6 상수에 접근할 수 있다. 하지만 아직 P 스택 프레임 내부에는 저장되어 있지 않고, 따로 P 내에 저장하는 메모리 명령문을 더 써줘야 한다. (정말 귀찮다!)
이번에는 인수를 6, 12, 18, 24, 30, 36, 42, 48로 총 8개를 넘겨주고 8개 인자의 총합만 받아오는 어셈블리 코드를 짜보겠다.
P_procedure:
movl $6, %rdi ; 첫 번째 인자를 %rdi 레지스터에 저장
movl $12, %rsi ; 두 번째 인자를 %rsi 레지스터에 저장
movl $18, %rdx ; 세 번째 인자를 %rdx 레지스터에 저장
movl $24, %rcx ; 네 번째 인자를 %rcx 레지스터에 저장
movl $30, %r8 ; 다섯 번째 인자를 %r8 레지스터에 저장
movl $36, %r9 ; 여섯 번째 인자를 %r9 레지스터에 저장
subq $16 %rsp ; 나머지 두 인자를 전달할 16바이트 메모리 스택 추가 할당 (Argument Build Area에 해당)
movl $42, %rsp ; 일곱 번째 인자를 Argument Build Area에 지역 변수로써 저장
movl $48, 8%rsp ; 여덟 번째 인자를 Argument Build Area에 지역 변수로써 저장
call Q_procedure ; Q_procedure 프로시저 호출
; 이 부분에서 %rax와 %rdx에 저장된 값을 사용
ret
Q_procedure:
addq %rdi, %rsi
addq %rsi, %rsi
addq %rdi, %rdx
addq %rdx, %rcx
addq %rcx, %r8
addq %r8, %r9
addq %r9, -(Q스택프레임)%rsp
addq -(Q스택프레임)%rsp, (8-Q스택프레임)%rsp
movq (8-Q스택프레임)%rsp, %rax
ret
주석에서 보는 것처럼 6개까지는 레지스터에 저장할 수 있지만, 7개 인자부터는 스택을 수동으로 따로 할당하여 8바이트 단위로 쪼개 저장하고 있다. 위 프로그램 카운터가 한 개씩 돌면서 이 그림의 스택 프레임과 스택 포인터가 어떻게 변할지 직접 상상해보는 것이 좋다.