OS

함수 호출의 리턴 주소는 어떻게 스택에 저장될까?

둠치킨 2025. 6. 23. 20:52

리턴 주소는 어떻게 스택에 저장될까?

C/C++을 포함한 대부분의 언어에서 함수 호출은 단순히 점프(jump)하는 것이 아니라, 나중에 돌아올 위치를 기억해야 하기 때문에 "리턴 주소(return address)"라는 개념이 사용됩니다. 이 리턴 주소는 스택(stack)에 저장되며, 함수가 끝나면 이 주소로 제어가 돌아옵니다.

이번 글에서는 리턴 주소가 스택에 어떻게 저장되고 다시 복원되는지, 그 과정을 함수 호출 단계별로 자세히 정리해보겠습니다.


1. 함수 호출 시: call 명령어의 역할

C++ 함수가 호출되면, 컴파일된 어셈블리 코드에서는 보통 다음과 같은 명령어가 사용됩니다:

call 함수_주소

이 call 명령은 다음 두 가지 일을 동시에 합니다:

  1. 리턴 주소를 스택에 push 한다
    → 즉, 현재 명령어의 다음 주소(복귀 위치)를 스택에 저장합니다.
  2. 함수로 점프한다
    → 스택에 주소를 저장한 뒤, 해당 함수의 코드로 제어를 이동합니다.

예를 들어:

push rip ; 리턴 주소 저장
jmp 함수_주소 ; 함수로 이동

2. 함수 종료 시: ret 명령어의 역할

함수가 끝나면 ret 명령어가 호출되며, 이는 다음 동작을 수행합니다:

  1. 스택에서 값을 pop
    → 가장 위에 있는 값, 즉 리턴 주소를 꺼냅니다.
  2. 그 주소로 점프
    → 프로그램의 흐름을 호출했던 함수의 다음 줄로 되돌립니다.

예:

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): 스택에 저장된 코드를 실행하지 못하게 함