프로시저와 스택 메모리 할당
프로시저(procedure) 는 특정 작업을 수행하는 코드 블록이다. 가상 메모리의 스택 영역에서 스택 메모리를 할당하는 기준은 프로시저이며, 각 프로시저는 필요한 만큼만 자기 스택 프레임을 할당한다.
그런데 프로시저인데도 스택 메모리 할당이 일어나지 않는 경우가 있다.
스택 할당이 한 번만 일어나는 경우
책의 그림 3.32 사례를 간단히 하면 다음과 같다.
P함수가Q함수를 호출하므로 프로시저는 2개다.- 하지만 스택 공간을 할당하는 인스트럭션은
P함수 상단에서 단 1회 실행된다.
subq $32, %rsp피호출 함수 Q가 아래 두 조건을 만족하면 이렇게 된다.
- 다른 함수를 호출하지 않는 함수(리프 함수)이다.
- 필요한 데이터를 모두 레지스터에 담을 수 있다.
이 경우 Q에는 별도의 스택 메모리 할당(subq ..., %rsp)이 없다. Q는 레지스터만 사용해 연산을 수행하고 반환한다.
Q에 지역 변수가 있다면?
Q가 자신만의 지역 변수 t를 가지고 있다면 스택 메모리를 사용할까?
결론부터 말하면 컴파일러 최적화 레벨에 따라 달라진다.
long q(long x, long y) {
long t = x + y;
return t * 3;
}gcc -O0로 컴파일한 경우
q:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -24(%rbp) # x를 스택에 저장
movq %rsi, -32(%rbp) # y를 스택에 저장
movq -24(%rbp), %rdx
movq -32(%rbp), %rax
addq %rdx, %rax
movq %rax, -8(%rbp) # t를 스택에 저장
movq -8(%rbp), %rdx
movq %rdx, %rax
addq %rax, %rax
addq %rdx, %rax # t * 3
popq %rbp
retO0에서는 레지스터로 충분하더라도 인자와 지역 변수를 모두 스택 메모리에 저장한다. 변수마다 고정된 메모리 위치가 있어야 디버거에서 언제든 값을 확인할 수 있기 때문이다.
참고: 이 코드에는
subq ..., %rsp가 없다.q가 리프 함수라서%rsp를 내리지 않고 그 아래 영역(red zone)에 바로 저장하기 때문이다. 스택 할당 명령어는 없지만 스택 메모리는 사용하고 있다.
gcc -Og로 컴파일한 경우
q:
leaq (%rdi,%rsi), %rax # t = x + y → t는 %rax
leaq (%rax,%rax,2), %rax # t * 3
rett를 위한 메모리 공간이 따로 없다.
t의 값은 레지스터 %rax에 잠시 머물다가 곧바로 다음 연산에 사용되고, 덮어써지면서 사라진다.
레지스터 승격
이처럼 gcc -Og 이상의 최적화 레벨에서는 지역 변수로 선언한 값이 스택 메모리에 저장되지 않고 레지스터에 잠시 머물다 가는데, 이를 레지스터 승격이라고 한다.
C의 지역 변수는 소스 코드 수준의 이름일 뿐이다. 컴파일러는 이를 스택 메모리에 둘 수도, 레지스터에 둘 수도 있다.
그래도 스택에 저장되는 경우
모든 지역 변수가 레지스터로 승격되는 것은 아니다. CS:APP 3.7.4에서는 지역 변수가 메모리에 있어야 하는 경우를 세 가지로 든다.
| 경우 | 이유 |
|---|---|
| 레지스터가 모자랄 때 | 담을 레지스터가 없음 (register spill) |
지역 변수에 & 연산자를 쓸 때 | 주소가 필요한데 레지스터에는 주소가 없음 |
| 지역 변수가 배열이나 구조체일 때 | 원소·필드를 주소 기반 참조로 접근해야 함 |
앞의 t는 이 중 어디에도 해당하지 않아서 레지스터로 승격될 수 있었다.
정리
- 스택 메모리는 프로시저 단위로, 필요한 만큼만 할당된다.
O0에서는 모든 지역 변수가 스택 메모리에 저장된다.Og이상에서는 지역 변수를 기본적으로 레지스터에 두고, 위 세 가지 경우에만 스택에 저장한다.- 다른 함수를 호출하지 않고 데이터를 모두 레지스터에 담을 수 있는 함수는 스택 할당 없이 레지스터만으로 실행된다.
어셈블리 결과는 컴파일러 버전에 따라 조금 다를 수 있으니 Compiler Explorer에서 직접 확인해 보는 것을 추천한다.