3.7) 지역 변수
CIL이 추가적인 C 변수 선언을 허용하지 않는다고 하였다. 하지만 우리는 프로시저 내에 지역 변수를 만들어 사용할 수 있는데, 그 방법은 다음과 같다.
- 지역 변수로 사용할 공간을 확보한다.
- 확보한 공간의 주소를 기억해놓고, 필요할 때마다 해당 주소에 접근한다.
다음은 이를 구현하는 코드이다. 이전에 사용하지 않던 기본 변수를 사용하므로 주의 깊게 봐야 한다.
LocalVariable.c |
#include "CIL.h" STRING sNewLine = "\n";
// main PROC(main)
// sp는 현재 메모리의 위치를 표시하는 기본 변수입니다. // sp의 값을 정수로 출력하고 개행합니다. PUSH(sp) INVOKE(print_int) PUSH(sNewLine) INVOKE(print_str)
// sp 기본 변수의 값을 4만큼 뺍니다. // sp -= 4; // 4byte는 32bit 정수형 변수의 크기입니다. SUB(sp, 4)
// sp가 가리키는 메모리의 주소 값을 a에 복사합니다. // a = &m[sp]; // CIL은 자료형에 엄격하지 않습니다. LEA(a, m + sp)
// a의 값을 주소 값으로 간주하고 해당 주소에 값을 설정합니다. // *a = 10; SETL(a, 10)
// sp가 가리키는 메모리의 값을 획득하여 a에 복사합니다. GETL(a, m + sp)
// a의 값을 출력합니다. PUSH(a) INVOKE(print_int)
ENDP |
값을 출력하는 코드를 제외하면 사실 4줄밖에 안 되는 단순한 코드다. 다만 여기에서 메모리를 좀 더 명확하게 그림으로 나타내어야 이후에 이야기를 진행할 때 문제가 없을 것 같다.
이미 알고 있겠지만, 메모리는 바이트의 배열이다. CIL에서는 메모리에 접근하기 위한 기본 변수를 제공하는데,이 변수에 대해 먼저 정리하자.
- m: memory. 시스템의 메모리를 나타내는 배열 변수다.
- sp: stack pointer. 프로그램 실행 시에 생성되는 스택 메모리를 가리키는 포인터 변수다.
- bp: base pointer. 프로시저 호출 시에 스택의 시작 주소를 저장하는 포인터 변수다.
bp에 대해서는 좀 더 나중에 다루고, 일단 m과 sp만을 얘기해보자. 스택이라고 하니 1장에서 배웠던 스택을 떠올릴 수 있는데, 맞는 판단이고 실제로 스택 형태로 메모리가 관리되지만, 여기서는 일단 스택 영역이라는 메모리 배열이 선언되어있고 sp가 이 배열을 가리키는 형태라고 생각하는 게 이해하기 편할 것 같다(물론 후에 왜 이것이 스택인지를 설명할 것이다). 독자의 편의를 위해 위에 제시한 코드에서 값을 출력하는 등의 쓸모없는 부분을 제외한 코드를 보이겠다.
LocalVariable.c |
#include "CIL.h" PROC(main) SUB(sp, 4) LEA(a, m + sp) SETL(a, 10) GETL(a, m + sp) ENDP |
프로시저가 호출되면 메모리는 다음 상태가 된다.
이제 스택 포인터에서 값을 빼는 SUB 연산을 수행한다.
그리고 sp가 가리키는 메모리의 주소를 LEA(load effective address) 명령으로 획득한다.
SETL 명령을 이용하여 획득한 주소에 10을 저장한다.
마지막으로 GETL 명령을 이용하여 기본 변수 a에, sp가 가리키는 메모리의 값을 획득하여 저장한다.
지역 변수를 두 개 이상 사용해야 하는 경우에는 스택의 시작 주소를 기준으로 변수의 크기만큼을 뺀다. 여기서 용어를 하나 정의하자. 어떤 두 대상의 수치적 거리를 오프셋(offset)이라고 한다. 따라서 위 그림에서 기본 변수sp와 bp의 오프셋은 4바이트가 된다. 이는 아주 중요한 내용인데, 이 예제에서는 만든 지역 변수에 접근하기 위해LEA 명령을 이용할 때 sp 기본 변수를 이용했지만, 일반적으로는 기본 변수 bp와 해당 지역 변수의 오프셋의 합을 이용한다. 예를 들어 이 예제에서는 다음이 성립한다.
만든 지역 변수의 위치 == sp == (bp - 4)
따라서 LEA 명령을 다음과 같이 수행해도 문제되지 않는다.
이것이 왜 중요하냐면, 지역 변수의 위치를 계산할 때는 반드시 bp를 이용해야 하기 때문이다. 지역 변수를 두 개 만드는 상황을 가정하자.
LocVarSp.c |
#include "CIL.h" STRING sNewLine = "\n";
// main PROC(main)
// 4바이트 지역 변수를 생성합니다. 임시로 var1이라고 합시다. // int var1; // 현재 var1과 sp의 오프셋은 0byte입니다. SUB(sp, 4)
// var1 = 10; // sp는 현재 var1을 가리킵니다. SETL(m + sp, 10);
// 4바이트 지역 변수를 더 생성합니다. 임시로 var2라고 합시다. // int var2; // 현재 var2와 sp의 오프셋은 0byte입니다. // 현재 var1과 sp의 오프셋은 4byte입니다. SUB(sp, 4)
// var2 = 20; // sp는 현재 var2를 가리킵니다. SETL(m + sp, 20);
// sp를 기준으로 var1과 var2의 값을 획득합니다. GETL(a, m + sp); // (sp)가 가리키는 값을 획득합니다. GETL(b, m + sp + 4); // (sp+4)가 가리키는 값을 획득합니다.
ENDP |
이 코드의 문제점이 무엇인지 알겠는가? 바로 변수가 새롭게 생성됨에 따라, 같은 변수를 가리키는 데 sp와의 오프셋이 계속 달라진다는 점이다. C는 변수 선언이 함수의 앞에 모두 위치하므로 크게 문제되지 않는다고 할지 모르나, sp는 지역 변수를 선언하는 데만 사용되는 변수가 아니다. 프로시저 호출을 위한 인자를 전달할 때, 프로시저를 호출할 때도 sp를 사용한다. 즉 sp를 이용하여 지역 변수에 접근하려면 프로시저 내에서 언제 그 값이 변하는지를 몽땅 추적해서 그 오프셋을 이용해야 한다.
이 문제는 스택의 시작 주소를 보관하는 bp 기본 변수를 이용하면 해결할 수 있다. 다음은 위 코드를 bp 기본 변수를 이용하여 변경한 것이다.
LocVarBp.c |
#include "CIL.h" STRING sNewLine = "\n";
// main PROC(main)
// 4바이트 지역 변수를 생성합니다. 임시로 var1이라고 합시다. // int var1; // 현재 var1과 bp의 오프셋은 4byte입니다. SUB(sp, 4)
// var1 = 10; // (bp-4)는 var1을 가리킵니다. SETL(m + bp - 4, 10);
// 4바이트 지역 변수를 더 생성합니다. 임시로 var2라고 합시다. // int var2; // 현재 var2와 bp의 오프셋은 8byte입니다. // 현재 var1과 bp의 오프셋은 4byte입니다. SUB(sp, 4)
// var2 = 20; // (bp-8)은 var2를 가리킵니다. SETL(m + bp - 8, 20);
// sp를 기준으로 var1과 var2의 값을 획득합니다. GETL(a, m + bp - 4); // var1 값을 획득합니다. GETL(b, m + bp - 8); // var2 값을 획득합니다.
ENDP |
이는 프로시저에서 임의의 위치에 변수를 선언하더라도, 해당 변수의 위치와 스택 메모리의 시작 지점의 값이 바뀌지 않음을 이용한 것이다. (bp-4)는 언제나 var1이며, (bp-8)은 언제나 var2이다. 이로써 같은 변수를 가리키기 위해 매번 sp를 추적해야 할 필요가 말끔히 사라졌다. 따라서 우리는 지역 변수에 접근할 때 sp 변수가 아닌 bp 변수를 이용해서 접근하는 것이 바람직하다.