OS
함수 호출의 리턴 주소는 어떻게 스택에 저장될까?
둠치킨
2025. 6. 23. 20:52
리턴 주소는 어떻게 스택에 저장될까?
C/C++을 포함한 대부분의 언어에서 함수 호출은 단순히 점프(jump)하는 것이 아니라, 나중에 돌아올 위치를 기억해야 하기 때문에 "리턴 주소(return address)"라는 개념이 사용됩니다. 이 리턴 주소는 스택(stack)에 저장되며, 함수가 끝나면 이 주소로 제어가 돌아옵니다.
이번 글에서는 리턴 주소가 스택에 어떻게 저장되고 다시 복원되는지, 그 과정을 함수 호출 단계별로 자세히 정리해보겠습니다.
1. 함수 호출 시: call 명령어의 역할
C++ 함수가 호출되면, 컴파일된 어셈블리 코드에서는 보통 다음과 같은 명령어가 사용됩니다:
call 함수_주소
이 call 명령은 다음 두 가지 일을 동시에 합니다:
- 리턴 주소를 스택에 push 한다
→ 즉, 현재 명령어의 다음 주소(복귀 위치)를 스택에 저장합니다. - 함수로 점프한다
→ 스택에 주소를 저장한 뒤, 해당 함수의 코드로 제어를 이동합니다.
예를 들어:
push rip ; 리턴 주소 저장
jmp 함수_주소 ; 함수로 이동
2. 함수 종료 시: ret 명령어의 역할
함수가 끝나면 ret 명령어가 호출되며, 이는 다음 동작을 수행합니다:
- 스택에서 값을 pop
→ 가장 위에 있는 값, 즉 리턴 주소를 꺼냅니다. - 그 주소로 점프
→ 프로그램의 흐름을 호출했던 함수의 다음 줄로 되돌립니다.
예:
pop rip ; 리턴 주소 복원
jmp rip ; 복귀
3. 예제: 함수 호출 시 스택 프레임 변화
void bar() {
int b = 2;
}
void foo() {
int a = 1;
bar();
}
int main() {
foo();
}
실행 흐름
[ main의 리턴 주소 ] ← main 호출 시 저장됨 (OS에 의해)
[ foo의 리턴 주소 ] ← foo() 호출 시 저장됨
[ bar의 리턴 주소 ] ← bar() 호출 시 저장됨
[ bar의 지역 변수 b ]
함수가 끝날 때마다 ret 명령으로 리턴 주소를 pop하고, 제어를 원래 위치로 되돌립니다.
4. 보안 이슈: 리턴 주소 덮어쓰기
스택에 저장된 리턴 주소는 프로그램의 제어 흐름을 바꾸는 핵심 위치이기 때문에,
과거에는 버퍼 오버플로우 공격의 주요 대상이었습니다.
char buf[8];
gets(buf); // 입력 길이 제한 없음 → 스택의 리턴 주소 덮어쓰기 가능
이런 위험을 막기 위해 현대 시스템은 다음과 같은 보안 장치를 사용합니다:
- Stack Canary: 리턴 주소 앞에 임의값을 두어 손상 여부 감지
- ASLR (Address Space Layout Randomization): 리턴 주소 예측 어렵게 만듦
- NX(Bit): 스택에 저장된 코드를 실행하지 못하게 함