3.8) 스택 포인터와 프로시저
방금 스택 포인터 변수 sp는 프로시저와도 관계가 있다고 했다. 정확히 어떤 관계일까? 이를 이해하기 위해 먼저 스택 메모리가 왜 스택 메모리인지부터 알아야겠다. 이를 위해 먼저 스택 메모리에 관한 명령을 보이겠다.
- PUSH(param): param 값을 스택 메모리에 푸시 한다.
- POP(param): 스택 메모리에서 팝 한 값을 param에 저장한다.
이전까지 우리는 PUSH를 함수를 호출하기 위해 값을 인자로 전달할 때 사용하는 명령으로 알고 있었다. 하지만 위에서 말했듯 실제로 PUSH는 스택 메모리에 값을 푸시 하는 역할을 하는데, 이것이 어떤 식으로 수행되는지를 알아보자. 다음은 이를 설명하기 위한 예제 코드다.
PushPop.c |
#include "CIL.h"
// main PROC(main)
PUSH(10) PUSH(20) PUSH(30) POP(a) POP(b) POP(c)
ENDP |
별도로 주석을 달지 않았지만, 스택을 이해하고 있는 여러분이라면 전혀 어렵다고 느끼지 않았을 것이다. 먼저main 프로시저가 호출되면 다음 상태가 된다.
10을 푸시 한다.
20을 푸시 한다.
30을 푸시 한다.
팝 한 값을 a에 저장한다.
팝 한 값을 b에 저장한다.
팝 한 값을 c에 저장한다.
그림으로 보면 아주 간단한데, 혹시 스택 포인터의 움직임을 자세히 보았는가? 그렇다면 이것이 왼쪽 방향으로 향하는 배열 기반의 스택임을 알 수 있을 것이다. 우리가 앞으로 구현할 컴파일러는 메모리를 바이트 배열로 생각하고, 스택 영역은 위와 같이 배열 스택으로 간주한다. 위 과정에 익숙해져야 앞으로 프로젝트를 진행할 수 있다.
이제 지역 변수와 스택 메모리 사이의 관계를 이해했으니 다음을 보자. 다음은 10을 출력하는 단순한 프로그램의 코드다.
SpProc.c |
#include "CIL.h"
// main PROC(main)
// get_sum2(10, 20)을 호출합니다. PUSH(20) PUSH(10) INVOKE(get_sum2)
// 반환된 값을 출력합니다. PUSH(a) INVOKE(print_int)
ENDP
// get_sum2 PROC(get_sum2)
// d에 두 번째 인자의 값을 대입합니다. GETL(d, m+bp+12) // a에 첫 번째 인자의 값을 대입합니다. GETL(a, m+bp+8) // a += d; ADD(a, d) // 함수 종료 시에는 a의 값이 항상 반환됩니다. // return a; RETURN()
ENDP |
참고로 꽤 길다. 프로시저가 호출되면 다음 상태가 된다.
인자를 스택에 모두 푸시하고 프로시저를 호출한다.
기본적으로 프로시저를 호출하면, 프로시저가 종료되었을 때 어느 위치부터 프로그램을 다시 실행해야 하는가, 즉 다음 명령의 주소 값을 넣어야 한다. 또한, 프로시저 시작 시에 새롭게 스택의 시작 지점이 정의되도록 bp 기본 변수의 값을 수정해야 한다. 일단 복귀 주소로 어떻게 돌아가는지에 대해서는 지금 설명하지 않겠다. 다음은get_sum2 프로시저 내부에서 일어나는 루틴이다.
이 과정이 모두 끝나면 RETURN 명령을 이용해 get_sum2 프로시저를 호출한 main 프로시저로 복귀해야 한다.
먼저 마지막으로 스택에 들어간 원소에서 팝 연산을 수행하여 bp를 스택의 복귀 주소로 맞춘다. 그 다음 다시 팝 연산을 수행하여 복귀 주소를 획득하고 해당 주소로 점프한다. 따라서 팝 연산을 2번 수행하므로 sp는 4바이트 * 2 = 8바이트만큼 이동하게 된다.
나머지는 반환 값을 출력하는 부분에 대한 것이며, 위의 그림을 이해하는 데 도움이 될 것이다.
이와 같이 INVOKE 매크로와 RETURN 매크로로 프로시저를 호출하고 원래 주소로 복귀하는 방법을 알아보았다.다만 완전히 설명이 끝난 것은 아닌데, 지금은 너무 많은 내용을 배웠으므로 단원을 잘라서 한 번 심호흡을 한 다음에 학습을 계속하자.
이제 심호흡이 끝났을 테니 다음으로 넘어갈 수 있겠다.