스택의 유령 (1부): 초기화되지 않은 변수가 이전 데이터를 기억하는 이유

발행: (2026년 5월 23일 PM 09:26 GMT+9)
6 분 소요
원문: Dev.to

출처: Dev.to

C 프로그램을 작성하고 실행했을 때, 직접 할당하지 않은 값을 출력하는 것을 본 적이 있나요?
스택 프레임 재사용
컴파일러 동작
메모리 영속성
그리고 현대 시스템에 내장된 성능 트레이드오프

이 시리즈에서는 고수준 C 코드에서 스택 프레임, 어셈블리 명령어, 그리고 궁극적으로 실제 보안 이슈까지 살펴볼 것입니다.

#include <stdio.h>
void Subtraction()
{
    int a = 100;
    int b = 35;
    int c = a - b;
}
void PrintValues()
{
    int i, j, k;
    printf("First value is %d\nSecond value is %d\nThird value is %d\nThese are the values for PrintValues function\n", i, j, k);
}
int main()
{
    Subtraction();
    PrintValues();
    return 0;
}

최적화 없이 컴파일하면(gcc -O0 main.c) 다음과 같은 출력이 나타날 수 있습니다.

First value is 100
Second value is 35
Third value is 65
These are the values for PrintValues function

처음 보면 불가능해 보입니다. PrintValues는 변수를 전혀 초기화하지 않는데, 왜 바로 직전에 Subtraction에서 사용한 동일한 값들을 출력할까요?

중요 경고: 이것은 정의되지 않은 동작(undefined behaviour)이며, C 표준은 어떤 일이 일어날지 보장하지 않습니다.

  • 다른 컴파일러는 서로 다르게 동작할 수 있습니다.
  • 최적화 레벨에 따라 결과가 달라질 수 있습니다.
  • 앞으로 실행할 때마다 다른 출력이 나올 수도 있습니다.

더러운 화이트보드 비유

무슨 일이 일어나고 있는지 이해하려면 코드를 잠시 제쳐두세요. RAM을 공유 교실의 화이트보드에 비유해 봅시다.

첫 번째 교사(Subtraction())가 교실에 들어와서 화이트보드에 100, 35, 65를 적습니다. 수업이 끝난 뒤, 지우지 않은 채 떠나는데, 지우는 데 시간이 걸리기 때문입니다. 잠시 방이 비어 있어도 보드에 적힌 내용은 그대로 남아 있습니다.

두 번째 교사(PrintValues())가 바로 그 뒤에 교실에 들어옵니다. 새로운 내용을 적지 않고, 이미 보드에 남아 있는 것을 그대로 읽어옵니다.

고수준에서 보면, 이것이 스택 메모리에서 일어나는 일과 유사합니다. 함수가 반환될 때, 일반적으로 컴파일된 코드는 로컬 변수에 사용된 스택 바이트를 자동으로 지우지 않습니다. 대신

  • 스택 공간이 재사용 가능해집니다.
  • 오래된 데이터가 일시적으로 남아 있습니다.
  • 다른 함수가 같은 스택 오프셋을 재사용할 수 있습니다.

SUBTRACTION() 함수의 어셈블리 살펴보기

아래는 x86‑64 환경에서 GDB가 출력한 Subtraction() 함수의 디스어셈블리입니다.

push %rbp
mov %rsp,%rbp
sub $0x30,%rsp
movl $0x64,-0x4(%rbp)
movl $0x23,-0x8(%rbp)
mov -0x4(%rbp),%eax
sub -0x8(%rbp),%eax
mov %eax,-0xc(%rbp)
mov -0xc(%rbp),%ecx
mov -0x8(%rbp),%edx
mov -0x4(%rbp),%eax
add $0x30,%rsp
pop %rbp
ret
어셈블리설명
push %rbp베이스 포인터를 스택에 저장
mov %rsp,%rbp스택 포인터를 베이스 포인터로 설정
sub $0x30,%rsp스택에 48바이트 할당
movl $0x64,-0x4(%rbp)100을 스택의 첫 4바이트에 저장
movl $0x23,-0x8(%rbp)35를 두 번째 슬롯에 저장
mov -0x4(%rbp),%eax100을 eax 레지스터에 로드
sub -0x8(%rbp),%eax35를 빼고 결과를 eax에 남김
mov %eax,-0xc(%rbp)결과를 메모리(스택)로 이동
mov -0xc(%rbp),%ecx레지스터로 이동
mov -0x8(%rbp),%edx레지스터로 이동
mov -0x4(%rbp),%eax레지스터로 이동
add $0x30,%rsp할당했던 스택 공간 정리
pop %rbp이전 스택 프레임 복원
ret호출자에게 제어 반환

PrintValues() 함수의 시작 어셈블리

다음은 PrintValues() 함수가 생성한 초기 어셈블리입니다.

push %rbp
mov %rsp,%rbp
sub $0x30,%rsp
mov -0xc(%rbp),%eax
mov -0x4(%rbp),%edx
mov -0x8(%rbp),%ecx

여기서 눈에 띄는 점은 PrintValues()도 거의 동일한 스택 레이아웃을 할당한다는 것입니다. 따라서

mov -0x4(%rbp),%eax

와 같은 명령은 “여기에 현재 존재하는 바이트를 읽어라”는 의미입니다. 만약 그 바이트가 아직 Subtraction()에서 남은 데이터라면, PrintValues()는 이전 값을 “기억”하고 있는 것처럼 보일 수 있습니다.

이 현상은 초기화되지 않은 변수가 위험하다는 사실을 넘어, 메모리는 자동으로 청소되지 않고 재사용된다는 시스템 원칙을 보여줍니다.

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.