C언어 함수 호출과 스택 프레임 구조 분석
>2022.02.01.
C언어와 같은 고급수준 프로그래밍 언어는 함수를 정의하고 수시로 호출하여 사용할 수 있습니다.
함수는 인수들을 받고 이들을 활용하여 지정된 동작을 실행하며, 그 결과 값을 반환하는 것으로 정의됩니다.
함수 호출 반환의 예)
int x, y;
int main()
{
...
x = abs(x);
y = abs(y);
...
}
int abs(int a)
{
if (a < 0)
return -a;
return a;
}위 C언어의 함수를 컴파일러에 의해 어셈블리로 구현하려면, 함수를 호출하였을 때 해당 함수로 이동하는 방법과 함수 실행 이후 원래 위치로 이동하는 것, 원래 위치로 이동한 후에 다음 명령어들을 실행하는 방법이 필요합니다. 따라서 함수를 호출할 때에 다시 돌아올 주소를 기억해 두었다가 함수 실행이 끝나면 기억해 둔 주소로 이동해야 합니다. 또한 함수를 호출할 때 인수를 전달하는 방법과 함수 실행이 끝났을 때의 결과 값을 받아올 방법이 필요합니다.
함수 호출을 위한 명령어
위 코드에서 main() 함수를 실행하던 중 abs() 함수를 호출한다는 것은, 함수 실행 중 abs() 함수에 해당하는 부분으로 이동하였다가 abs() 함수의 실행이 끝이 나면 다시 main() 함수 부분으로 이동해야 합니다.
링크 레지스터
TOY 프로세서에서 특정 위치로의 이동은 BR(branch) 명령어를 활용합니다.
abs() 함수를 완료한 상태에 다시 함수를 호출했었던 위치로 돌아오는 방법을 찾아야 합니다. 이러한 목적으로 TOY 프로세서에서 제공하는 함수가 LINK와 RET 명령어입니다. LINK 명령어는 위에 언급했듯이 함수를 호출할 때 다시 돌아올 주소를 링크 레지스터에 기록합니다. RET 명령어는 이 링크 레지스터에 기록된 주소로 실행 위치를 복귀합니다. 링크 레지스터는 프로세서마다 다른데 TOY 프로세서에서는 R6 레지스터를 주로 사용합니다.
LINK : 되돌아올 주소를 링크 레지스터(R6)에 저장 /
R6 <- PC+1
BR nzp, addr : 무조건 addr 부분으로 다음 실행 위치 이동 / PC <- addr
RET : 링크 레지스터(R6)에 저장된 주소로 다음 실행 위치 이동 / PC <- R6LINK 명령어는 LINK 명령어 사용 이후에 BR 명령어를 연달아 쓰도록 합니다.
따라서 LINK 명령어가 있는 메모리 주소로부터 2를 더한 결과(되돌아올 주소)가 R6에 기록됩니다. LINK 명령어를 읽어온 후에 이것을 실행할 시점에 PC 값은 이미 1만큼 증가해 있으므로 현재의 PC 값에 1이 증가한 값이 R6에 저장할 주소가 됩니다.
RET 명령어는 링크 레지스터 R6의 값을 PC에 저장하는 것으로 처리됩니다.
RET 명령어를 실행한 후에는 PC에 복귀 주소가 기록되어 있으므로 복귀할 주소로 이동하여 다음 명령어를 읽어오게 됩니다.
함수의 인수와 반환 값 전달
C언어 컴파일러는 함수 호출을 일정한 규칙에 따라 어셈블리 언어의 함수 호출 기능을 이용하여 처리합니다. 함수를 호출할 때에는 인수들을 전달하는 방법과 반환 값을 전달하는 방법도 정의되어야 합니다. 밑의 프로그램은 2개의 인수를 갖는 함수를 이용하는 C언어 프로그램입니다. 함수 min은 전달된 2개의 인수 중에서 작은 값을 선택하여 반환하는 함수입니다.
int val, x;
main()
{
...
val = min(x, 3);
...
}
int min(int a, int, b)
{
if (a < b)
return a;
return b;
}우선 단순한 방법으로, 함수 호출 시에 인수 전달은 레지스터를 이용한다고 가정하겠습니다. 첫 번째 인수는 R0, 두 번째 인수는 R1, 세 번째 인수는 R2에 저장하는 식입니다. 함수의 반환 값은 R0 레지스터를 이용합니다. 아래의 프로그램은 위의 C언어 프로그램을 어셈블리 언어 프로그램으로 표현한 예입니다.
main:
...
LOAD R0, X ; 첫번째 인수
COPY R1, 3 ; 두번째 인수
LINK
BR nzp, min ; 함수 min 호출
STORE R0, val ; 반환 값 변수 val에 저장
...
min: ; 함수 min
CMP R0, R1 ; 두 인수 비교
BR pz, _L0 ; R0이 R1보다 크거나 같으면 이동
RET ; R0 값을 그대로 반환
_L0:
COPY R0, R1 ; R0에 두번째 인수 복사
RET ; R0 값 반환
val: .BLOCK 1 ; val 변수의 메모리 영역
x: .BLOCK 1 ; x 변수의 메모리 영역LOAD 명령어로 첫 번째 인수인 x의 값을 읽어 R0 레지스터에 저장하고, R1 레지스터에는 두 번째 인수인 3을 복사하여 R1 레지스터에 저장한 후 min 함수를 호출합니다. min 함수에서는 R0, R1 레지스터에 저장된 두 인수를 비교하여 a에 해당하는 R0이 더 크거나 같으면 _L0로 이동하여 3에 해당하는 b를 반환하고, R0이 더 작다면 a에 해당하는 R0를 그대로 반환합니다. 반환 값은 필요에 따라 val 변수에 STORE로 저장하면 됩니다.
스택을 이용한 함수의 인수 전달
특정 컴파일러마다 함수의 인수와 반환 값을 전달할 때에 각각 고유한 방식을 사용합니다. 반환 값의 전달은 레지스터를 이용하는 것이 일반적이나 함수의 인수는 스택을 이용하여 인수를 전달하도록 하는 컴파일러가 일반적입니다. 레지스터를 이용하면 속도가 빠른 장점이 있지만 레지스터의 개수가 한정되어 있으므로 전달할 인수의 개수에 제한이 있습니다. 스택을 이용하면 인수의 개수에 대한 제한이 없어진다고 보시면 됩니다.
스택을 이용하여 함수의 인수를 전달하는 방식을 사용한다면 위의 프로그램을 아래와 같이 변경할 수 있습니다. 전달할 인수들을 스택에 쌓은 후에 해당 함수로 이동합니다. 함수에서 복귀한 이후에는 스택 포인터 값을 스택에 쌓는 과정에서 감소한 만큼을 복구해 주어야 (두 번 PUSH 했으므로 2만큼 더해주어야) 함수를 호출하기 이전 상태의 스택 포인터 값으로 복구됩니다. 인수들을 스택에 쌓는 순서는 제일 마지막 인수부터 역순으로 쌓는 것이 일반적인 방식이므로 여기서도 이 방식을 적용했습니다. 함수 내에서는 스택 포인터를 이용하여 전달된 인수들을 복사해 낼 수 있습니다. 가장 마지막에 PUSH한 인수는 R5+1의 주소에 있으므로 'LDR R1, R5, 1'로 R1에 복사해 내도록 했습니다.
스택을 이용하여 함수의 인수를 전달하는 방식을 사용한다면 위의 프로그램을 아래와 같이 변경할 수 있습니다. 전달할 인수들을 스택에 쌓은 후에 해당 함수로 이동합니다. 함수에서 복귀한 이후에는 스택 포인터 값을 스택에 쌓는 과정에서 감소한 만큼을 복구해 주어야 (두 번 PUSH 했으므로 2만큼 더해주어야) 함수를 호출하기 이전 상태의 스택 포인터 값으로 복구됩니다. 인수들을 스택에 쌓는 순서는 제일 마지막 인수부터 역순으로 쌓는 것이 일반적인 방식이므로 여기서도 이 방식을 적용했습니다. 함수 내에서는 스택 포인터를 이용하여 전달된 인수들을 복사해 낼 수 있습니다. 가장 마지막에 PUSH한 인수는 R5+1의 주소에 있으므로 'LDR R1, R5, 1'로 R1에 복사해 내도록 했습니다.
main:
...
COPY R0, 3 ; 두번쨰 인수
SUB R5, R5, 1 ; 'PUSH R0' (두번째 인수 PUSH)
STR R0,R5,0
LOAD R0, x ; 첫번째 인수
SUB R5, R5, 1 ; 'PUSH R0' (첫번째 인수 PUSH)
STR R0, R5, 0
LINK
BR nzp, min ; 함수 min 호출
ADD R5, R5, 2 ; 2번의 PUSH에서 감소한 만큼 복구
STORE R0, val ; 반환 값을 변수 val에 기록
...
min: ; 함수 min
LDR R0, R5, 0 ; 첫번째 인수 가져오기
LDR R1, R5, 1 ; 두번째 인수 가져오기
CMP R0, R1 ; 두 인수 비교
BR pz, _LO ; R0이 크거나 같으면 이동
RET ; R0 값 그대로 반환
_L0:
COPY R0, R1 ; R0에 두번째 인수 복사하여
RET ; R0 값 반환
val: .BLOCK 1 ; 변수 val의 메모리 영역
x: .BLOCK 1 ; 변수 x의 메모리 영역C언어 함수에서 지역변수의 처리
C언어에서 함수의 지역변수는 이 함수가 실행되고 있는 기간 동안만 임시로 메모리 공간을 확보하고(일반적으로 스택 영역에 확보합니다), 함수에서 복귀하면 해당 변수의 영역은 없어집니다. 2개의 지역 변수를 갖는 함수인 아래의 프로그램은 인수로 받은 2개의 수를 곱한 결과를 반환하는 함수이고, 반복적으로 더하는 알고리즘을 적용했습니다.
int mult(int a, int b)
{
int x, sum;
sum = 0;
x = b;
while (x > 0) {
sum = sum + a;
x = x - 1;
}
return sum;
}스택 프레임
컴파일러마다 함수에서 전달받은 인수와 지역 변수를 위한 메모리 영역을 할당하는 방식들이 서로 다른데, 일반적으로 함수 호출 시에 해당 함수를 위한 메모리 프레임을 스택 상단에 할당하는 방법을 많이 사용합니다. (여기서 인수는 스택을 통하여 전달하는 것으로 가정합니다) 함수별 스택 프레임은 함수가 실행되는 동안에 이 함수의 인수 및 지역 변수를 위해 스택에 할당한 메모리 영역을 의미합니다.
프레임 포인터
프레임 포인터는 현재 실행 중인 함수의 스택 프레임의 기준이 되는 주소를 저장하고 있는 레지스터를 지칭합니다. 스택 포인터의 값은 수시로 PUSH나 POP 동작에 의해서 변할 수 있으므로, 프레임 포인터를 두어서 특정 함수를 실행하는 동안 변하지 않는 고정된 값을 유지하도록 합니다. 여기서는 프레임 포인터보다 높은 주소(스택의 아래쪽) 부분은 전달받은 인수들의 영역이고, 프레임 포인터보다 낮은 주소(스택의 위쪽) 부분은 함수에서 사용하는 지역 변수들을 위한 영역이 되도록 합니다.