맨위로가기

스택 오버플로

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

스택 오버플로는 함수가 너무 깊이 재귀적으로 호출되거나, 스택에 저장할 수 있는 것보다 더 많은 메모리를 할당하려 할 때 발생하는 오류이다. 이는 주로 무한 재귀, 매우 깊은 재귀, 또는 스택에 큰 변수를 할당하려는 시도로 인해 발생하며, C/C++과 같은 언어에서 흔히 나타난다. 해결 방법으로는 코드 검토 및 수정, 스택 크기 설정, 동적 메모리 할당 활용 등이 있다.

더 읽어볼만한 페이지

  • 컴퓨터 오류 - 블루스크린
    블루스크린은 윈도우 운영체제에서 발생하는 치명적인 오류로, 컴퓨터 작동을 멈추고 파란색 화면에 오류 메시지를 표시하며, 하드웨어 또는 소프트웨어 문제로 인해 발생하고, 시스템 복원, 안전 모드 부팅 등의 방법으로 대처한다.
  • 컴퓨터 오류 - 글리치
    글리치는 예기치 않은 오작동이나 오류를 뜻하며, 전자 공학, 컴퓨터, 비디오 게임, 텔레비전 방송, 대중문화 등 다양한 분야에서 기능 실패, 오류, 그래픽 및 사운드 문제, 신호 오류 등의 이상 현상을 포괄적으로 지칭하는 용어이다.
  • 소프트웨어 버그 - 교착 상태
    교착 상태는 둘 이상의 프로세스가 자원을 점유하고 서로의 자원을 요청하여 더 이상 진행할 수 없는 상태를 의미하며, 상호 배제, 점유 대기, 비선점, 순환 대기 네 가지 조건이 모두 충족되어야 발생하고, 운영 체제는 이를 예방, 회피, 무시, 발견하는 방법으로 관리한다.
  • 소프트웨어 버그 - 글리치
    글리치는 예기치 않은 오작동이나 오류를 뜻하며, 전자 공학, 컴퓨터, 비디오 게임, 텔레비전 방송, 대중문화 등 다양한 분야에서 기능 실패, 오류, 그래픽 및 사운드 문제, 신호 오류 등의 이상 현상을 포괄적으로 지칭하는 용어이다.
스택 오버플로
기본 정보
Stack Overflow 로고
Stack Overflow 로고
종류질의응답 웹사이트
언어다국어
소유주프로서스 (2021년 ~ 현재)
이전 소유주조엘 스폴스키와 제프 앳우드 (2008년 ~ 2019년)
프로서스 (2019년 ~ 2021년)
설립일2008년 9월 15일
슬로건개발자가 답을 찾거나 공유하거나 경력을 쌓을 수 있도록 도와주기
URLstackoverflow.com
설명
개요프로그래밍 관련 질문과 답변을 제공하는 웹사이트
특징질문과 답변에 대한 사용자의 투표 시스템
높은 평점을 받은 답변은 위로 올라감
낮은 평점을 받은 답변은 아래로 내려가거나 삭제됨
역사
설립 배경조엘 스폴스키와 제프 앳우드가 설립
프로그래밍에 대한 더 나은 질의응답 웹사이트를 만들고자 함
기능
주요 기능질문 및 답변 게시
사용자 투표
태그 시스템
배지 및 평판 시스템
코드 스니펫 공유
기술
사용 기술ASP.NET
C#
SQL Server
Redis
Elasticsearch
운영
커뮤니티프로그래머, 개발자, IT 전문가 등
콘텐츠 관리커뮤니티에 의한 자율 관리 시스템
평판 시스템을 통한 사용자 권한 부여
기타
영향프로그래밍 학습 및 문제 해결에 기여
개발자 커뮤니티 활성화

2. 원인

스택 오버플로는 주로 다음 두 가지 원인으로 발생한다.


  • 무한 재귀: 함수가 자기 자신을 반복적으로 호출하면서 종료 조건에 도달하지 못하는 경우이다. 재귀 호출이 계속되면 콜 스택이 가득 차서 스택 오버플로가 발생한다. 꼬리 재귀 최적화를 지원하는 언어에서는 꼬리 재귀 형태로 작성된 함수는 스택을 사용하지 않고 반복문처럼 동작하므로 이 문제를 방지할 수 있다.
  • 매우 큰 스택 변수: 함수 내에서 매우 큰 배열과 같은 지역 변수를 선언하면 콜 스택 공간이 부족해져 스택 오버플로가 발생할 수 있다. 이 경우, 힙 영역에 변수를 동적으로 할당하면 문제를 해결할 수 있다.


C 등에서는 무한 루프에 빠진 재귀 호출이나, 스택에 너무 큰 배열을 확보하려는 경우 스택 오버플로가 발생할 수 있다.[18] 일반적인 프로그램 실행 환경에서는 서브루틴(함수, 프로시저, 메서드) 호출 정보를 저장하기 위해 스택(콜 스택)이 사용된다. 서브루틴이 호출될 때마다 데이터가 스택에 쌓이고(푸시), 종료되면 제거된다(팝). 운영 체제나 실행 옵션에 따라 콜 스택 크기는 제한되어 있으며, 이 한계를 넘어서면 스택 오버플로가 발생하여 프로그램이 비정상적으로 종료될 수 있다.

2. 1. 무한 재귀

스택 오버플로의 가장 흔한 원인은 무한 루프에 빠진 재귀 호출이다.[17] 함수가 자신을 너무 많이 호출하면 각 호출과 관련된 변수와 정보를 저장하는 데 필요한 공간이 스택의 용량을 초과하게 된다.[2]

일반적인 프로그램 실행 환경에서는 서브루틴(함수, 프로시저, 또는 메서드) 호출 정보를 저장하기 위해 스택 메모리 영역(콜 스택)이 할당된다. 서브루틴 호출 시 데이터가 스택에 쌓이고(푸시), 서브루틴이 끝나면 데이터가 제거된다(팝). 또한, 콜 스택은 지역 변수를 저장하는 데에도 사용된다. 운영 체제나 실행 옵션에 따라 콜 스택의 용량에는 제한이 있으며, 이 제한을 초과하면 스택 오버플로가 발생하여 정의되지 않은 동작이 발생한다.

스택 오버플로의 주요 원인 중 하나는 서브루틴의 재귀 호출로 인한 무한 루프(무한 재귀)이다. 설계상 유한 재귀였더라도 재귀 호출의 깊이가 콜 스택의 한계를 넘으면 스택 오버플로가 발생할 수 있다. 그러나 꼬리 재귀 최적화를 지원하는 프로그래밍 언어에서는 꼬리 재귀 형태로 작성된 서브루틴을 루프로 변환하여 스택 오버플로를 방지할 수 있는데, 이는 꼬리 재귀 호출이 추가적인 스택 공간을 차지하지 않기 때문이다.[3] 재귀가 아닌 서브루틴 호출도 호출 깊이가 너무 깊으면 스택 오버플로가 발생할 수 있다.

2. 1. 1. C/C++ 에서의 무한 재귀 예시

C/C++에서 무한 재귀의 예시는 다음과 같다.[18] foo영어 함수는 호출될 때마다 자기 자신을 다시 호출하는데, 이 과정에서 스택에 추가적인 공간을 할당하게 된다. 이 과정이 반복되면서 스택 오버플로가 발생하고, 결국 세그멘테이션 오류가 발생한다.[2]



int foo() {

return foo();

}



일부 C 컴파일러는 꼬리 호출 최적화를 통해 꼬리 재귀 형태의 무한 재귀에서 스택 오버플로가 발생하지 않도록 한다.[3] 예를 들어, gcc에서 위의 코드를 `-O1` 옵션으로 컴파일하면 세그멘테이션 오류가 발생하지만, `-O2` 또는 `-O3` 옵션을 사용하면 꼬리 호출 최적화가 적용되어 오류가 발생하지 않는다.[4]



/* 함수의 반환값이 int인 경우, C언어에서는 전방 선언이나 전방 정의가 없어도 컴파일이 가능하지만, C++에서는 전방 선언 또는 전방 정의가 필요하다. */

int g(void);

int f(void) {

return g();

}

int g(void) {

return f();

}



위의 예시처럼 함수 f()영어가 함수 g()영어를 호출하고, g()영어가 다시 f()영어를 호출하는 상호 재귀 호출도 무한 재귀의 일종이다. 이러한 상호 호출은 종료 조건이 없으므로 결국 스택 오버플로를 발생시킨다. 마찬가지로, 아래와 같이 종료 조건이 없는 자기 재귀 호출도 무한 재귀가 된다.



int foo(void) {

return foo();

}


2. 1. 2. 꼬리 재귀 최적화

무한 루프에 빠진 재귀 호출은 스택 오버플로의 흔한 원인이다. 테일 콜 최적화 기능이 있는 스킴과 같은 언어들은 꼬리 반복을 통해 스택 오버플로 없이 무한 반복을 일으킬 수 있는데, 이는 테일 콜이 추가적인 스택 공간을 차지하지 않기 때문이다.[17]

일부 C 컴파일러는 꼬리 호출 최적화를 구현하여 꼬리 재귀가 스택 오버플로 없이 발생하도록 허용한다. 이는 꼬리 재귀 호출이 추가 스택 공간을 차지하지 않기 때문이다.[3]

예를 들어, gcc에서 `-O1` 옵션으로 컴파일하면 세그멘테이션 오류가 발생하지만, `-O2` 또는 `-O3` 옵션을 사용하면 꼬리 호출 최적화가 활성화되어 오류가 발생하지 않는다. 이 최적화 수준은 `-foptimize-sibling-calls` 컴파일러 옵션을 암시한다.[4] Scheme과 같은 다른 언어는 모든 구현이 꼬리 재귀를 언어 표준의 일부로 포함하도록 요구한다.[5]

2. 2. 매우 깊은 재귀

소스 코드 설계상으로는 유한 횟수로 종료되는 유한 재귀라도, 재귀 호출의 계층 수가 너무 깊어지면 실행 환경에서의 콜 스택 상한을 넘어 스택 오버플로우가 발생할 수 있다. 꼬리 재귀 최적화를 구현한 프로그래밍 언어에서는 꼬리 재귀 형태로 작성된 서브루틴을 루프로 전개할 수 있어 스택 오버플로우가 발생하지 않는다. 이는 재귀 호출이 플랫한 루프 처리로 변환되어 스택을 소비하지 않기 때문이다.

2. 2. 1. 재귀 함수와 반복문 비교

이론상으로는 종료되지만 실제로는 호출 스택 버퍼 오버플로우를 일으키는 재귀 함수는 재귀를 루프로 변환하고 함수 인수를 명시적 스택에 저장함으로써 해결할 수 있다(호출 스택의 암시적 사용 대신). 원시 재귀 함수 클래스가 LOOP 계산 가능 함수 클래스와 동일하기 때문에 항상 가능하다. C++와 유사한 의사 코드에서 다음 예시를 보자.



왼쪽과 같은 원시 재귀 함수는 항상 오른쪽과 같은 루프로 변환될 수 있다.

왼쪽의 위 예시와 같은 함수는 꼬리 호출 최적화를 지원하는 환경에서는 문제가 되지 않지만, 이러한 언어에서도 스택 오버플로우를 일으킬 수 있는 재귀 함수를 만드는 것이 여전히 가능하다. 다음은 두 개의 간단한 정수 지수 함수의 예시이다.



위의 두 `pow(base, exp)` 함수는 동일한 결과를 계산하지만, 왼쪽에 있는 함수는 꼬리 호출 최적화가 불가능하기 때문에 스택 오버플로우를 일으키기 쉽다. 실행 중에 이러한 함수에 대한 스택은 다음과 같다.



왼쪽에 있는 함수는 스택에 `exp` 개수의 정수를 저장해야 하며, 재귀가 종료되고 함수가 1을 반환할 때 곱해진다. 반대로 오른쪽에 있는 함수는 언제든지 3개의 정수만 저장하면 되고, 후속 호출에 전달되는 중간 결과를 계산한다. 현재 함수 호출 외에 다른 정보를 저장할 필요가 없으므로 꼬리 재귀 최적화기는 이전 스택 프레임을 "삭제"하여 스택 오버플로우의 가능성을 제거할 수 있다.

```c

/* 함수의 반환값이 int인 경우, C언어에서는 전방 선언이나 전방 정의가 없어도 컴파일이 가능하지만, C++에서는 전방 선언 또는 전방 정의가 필요하다. */

int g(void);

int f(void) {

return g();

}

int g(void) {

return f();

}

```

함수 `f()`는 함수 `g()`를 호출하고 있지만, `g()` 역시 `f()`를 호출하고 있다. 상호 호출은 끝나지 않고, 최종적으로는 스택 오버플로가 발생한다. 다음과 같은 종료 조건이 없는 자기 재귀도 무한 재귀가 된다.

```c

int foo(void) {

return foo();

}

2. 2. 2. 꼬리 재귀와 비 꼬리 재귀 비교

이론적으로는 종료되어야 하지만 실제로는 호출 스택 버퍼 오버플로우를 일으키는 재귀 함수는 재귀를 루프로 변환하고 함수 인수를 명시적 스택에 저장함으로써 해결할 수 있다(호출 스택의 암시적 사용 대신). 이는 원시 재귀 함수 클래스가 LOOP 계산 가능 함수 클래스와 동일하기 때문에 항상 가능하다.

C++(와)과 유사한 의사 코드에서 다음 예시를 보자.

width="50%"왼쪽width="50%"오른쪽



왼쪽과 같은 원시 재귀 함수는 항상 오른쪽과 같은 루프로 변환될 수 있다.

왼쪽의 위 예시와 같은 함수는 꼬리 호출 최적화를 지원하는 환경에서는 문제가 되지 않지만, 이러한 언어에서는 스택 오버플로우를 일으킬 수 있는 재귀 함수를 만드는 것이 여전히 가능하다. 두 개의 간단한 정수 지수 함수의 아래 예시를 보자.

width="50%"비 꼬리 재귀width="50%"꼬리 재귀



위의 두 `pow(base, exp)` 함수는 동일한 결과를 계산하지만, 왼쪽에 있는 함수는 꼬리 호출 최적화가 불가능하기 때문에 스택 오버플로를 일으키기 쉽다. 실행 중에 이러한 함수에 대한 스택은 다음과 같다.

width="50%"비 꼬리 재귀width="50%"꼬리 재귀



왼쪽에 있는 함수(비 꼬리 재귀)는 스택에 `exp` 개수의 정수를 저장해야 하며, 재귀가 종료되고 함수가 1을 반환할 때 곱해진다. 반대로 오른쪽에 있는 함수(꼬리 재귀)는 언제든지 3개의 정수만 저장하면 되며, 후속 호출에 전달되는 중간 결과를 계산한다. 현재 함수 호출 외에 다른 정보를 저장할 필요가 없으므로 꼬리 재귀 최적화기는 이전 스택 프레임을 "삭제"하여 스택 오버플로우의 가능성을 제거할 수 있다.

2. 3. 매우 큰 스택 변수

스택 오버플로는 스택에 너무 큰 변수를 할당할 때 발생할 수 있다. 지역 배열 변수를 크게 생성하면 스택 공간이 부족해져 문제가 발생한다.[6] 이를 방지하기 위해 몇 킬로바이트보다 큰 배열은 동적 메모리 할당을 사용하는 것이 좋다.[6]

일반적으로 프로그램 실행 시 서브루틴 호출 정보와 지역 변수는 스택(콜 스택)에 저장된다. 서브루틴 호출과 종료에 따라 데이터가 스택에 쌓이고 제거되는 방식이다. 콜 스택 크기는 제한되어 있어, 데이터가 너무 많으면 스택 오버플로가 발생해 프로그램이 비정상 종료될 수 있다.

배열을 스택에 확보하는 것도 스택 오버플로의 흔한 원인이다. 콜 스택 용량 제한 때문에 큰 지역 변수 대신 힙 영역을 명시적으로 사용하는 것이 안전하다.

2. 3. 1. C/C++ 에서의 큰 스택 변수 예시

C/C++에서 매우 큰 스택 변수의 예는 다음과 같다.[6]

```c

int foo()

{

double x[1048576];

}

```

8바이트 배정밀도 부동 소수점을 사용하는 C 구현에서 선언된 배열은 8 메가바이트의 데이터를 소비한다. 스택에서 사용 가능한 메모리보다 많은 경우(스레드 생성 매개변수 또는 운영 체제 제한에 의해 설정됨) 스택 오버플로가 발생한다.[6]

C/C++에서 함수 내에 정의된 지역 변수는 기본적으로 자동 변수가 되며, 블록을 벗어나면 자동으로 메모리가 해제된다.[8] 일반적으로 콜 스택을 이용하여 구현된다. 각 프로그램의 스택 영역 크기는 스레드마다 기본적으로 최대 수 MiB 정도[9][10][11][12][13][14]이며, 대용량 배열을 확보하기에는 적합하지 않다. 스택 크기를 늘리면 프로세스 내에서 시작 가능한 스레드의 최대 수가 감소한다.

다음은 스택 오버플로우를 발생시키는 코드 예시이다.

```c

#include

int main(void) {

int ary[1000 * 1000 * 1000]; /* 배열이 너무 큼 */

printf("Hello!\n"); /* &"Hello!\n"의 푸시가 스택 오버플로우가 됨 */

}

```

이러한 스택 오버플로우는 배열을 정적 영역으로 이동하거나, `malloc` 등을 사용하여 힙 영역에 동적 할당하면 피할 수 있다.

```c

#include

int ary[1000 * 1000 * 1000];

int main(void) {

printf("Hello!\n");

}

```

또는

```c

#include

int main(void) {

static int ary[1000 * 1000 * 1000];

printf("Hello!\n");

}

```

32비트 환경에서는 주소 공간의 상한이 4 GiB이므로, 위와 같이 거대한 배열을 정적으로 할당하려 하면 프로그램의 시작이 실패할 가능성이 있다. 일반적으로, 64비트 이상의 환경에서도 실행 시의 동적 메모리 할당과 성패 검사를 하는 것이 바람직하다.

```c

#include

#include

int main(void) {

/* malloc() 함수가 실패한 경우 NULL 포인터가 반환되므로, 실제로 메모리 영역을 이용하기 전에 NULL 검사를 해야 함 */

int *ary = (int *)malloc(sizeof(int) * (size_t)(1000 * 1000 * 1000));

printf("Hello!\n");

/* NULL에 대해 free() 함수를 실행해도 아무 일도 일어나지 않는다는 것이 C/C++ 규격으로 보장되므로, NULL 검사는 불필요 */

free(ary);

}

2. 3. 2. 동적 메모리 할당

일부 저자들은 몇 킬로바이트보다 큰 배열은 지역 변수 대신 동적으로 할당해야 한다고 권장한다.[6]

C에서 매우 큰 스택 변수의 예시는 다음과 같다.



int foo()

{

double x[1048576];

}



8바이트 배정밀도 부동 소수점을 사용하는 C 구현에서 선언된 배열은 8 메가바이트의 데이터를 소비한다. 스택에서 사용 가능한 메모리보다 많은 경우(스레드 생성 매개변수 또는 운영 체제 제한에 의해 설정됨) 스택 오버플로가 발생한다.

C/C++(C++)에서 함수 내에서 정의되는 지역 변수(블록 스코프를 가진 변수)는 기본적으로 자동 변수가 되며, 블록을 벗어남으로써 자동으로 변수의 수명이 다해 메모리가 해제되는 자동 기억 영역 기간을 가진다.[8]。 일반적으로 콜 스택을 이용하여 구현된다.

시스템에 구현되어 있는 메인 메모리의 용량에 관계없이, 각 프로그램의 스택 영역의 크기는 스레드마다 기본적으로 기껏해야 수 MiB 정도이며,[9][10][11][12][13][14] 대용량의 배열을 확보하는 데에는 적합하지 않다.

다음과 같이 배열이 너무 큰 경우,



#include

int main(void) {

int ary[1000 * 1000 * 1000]; /* 배열이 너무 큼 */

printf("Hello!\n"); /* &"Hello!\n"의 푸시가 스택 오버플로우가 됨 */

}



배열을 정적 영역으로 이동하거나,



#include

int ary[1000 * 1000 * 1000];

int main(void) {

printf("Hello!\n");

}



malloc 등을 사용하여 힙 영역에 동적 할당하면 스택 오버플로우를 피할 수 있다.



#include

#include

int main(void) {

/* malloc() 함수가 실패한 경우 NULL 포인터가 반환되므로, 실제로 메모리 영역을 이용하기 전에 NULL 검사를 해야 함 */

int *ary = (int *)malloc(sizeof(int) * (size_t)(1000 * 1000 * 1000));

printf("Hello!\n");

/* NULL에 대해 free() 함수를 실행해도 아무 일도 일어나지 않는다는 것이 C/C++ 규격으로 보장되므로, NULL 검사는 불필요 */

free(ary);

}



다만 32비트 환경에서는 주소 공간의 상한이 4 GiB이므로, 위와 같이 거대한 배열을 정적으로 할당하려 하면 프로그램의 시작이 실패할 가능성이 있다. 일반적으로, 시스템이 이용할 수 있는 빈 메모리의 양은 반드시 확실하지 않으므로, 64비트 이상의 환경에서도 실행 시의 동적 메모리 할당과 성패 검사를 하는 것이 바람직하다.

Java나 C#/VB.NET과 같은 가상 머신 기반의 실행 환경을 가진 언어에서는, OS가 아닌 가상 머신에 의해 관리되는 힙 영역을 사용함으로써 동적 메모리 할당의 오버헤드를 줄였다. 이러한 언어에서는 일반적인 (언어 내장) 배열은 항상 힙 영역에 할당되므로, 실행 시에 메모리 부족 예외를 던질 수는 있어도 스택 오버플로우를 일으키는 일은 없다. 한편, C/C++의 동적 메모리 할당은 그러한 언어와 비교해 오버헤드가 크므로, 모든 배열을 힙에 할당하려 하는 것은 큰 속도 손실을 수반한다.

2. 4. 제한된 환경

스택 오버플로는 주어진 프로그램의 유효 스택 크기를 줄이는 모든 요인에 의해 악화된다. 예를 들어, 여러 스레드 없이 실행되는 동일한 프로그램은 정상적으로 작동할 수 있지만, 멀티 스레딩이 활성화되는 즉시 프로그램이 충돌한다. 이는 스레드를 사용하는 대부분의 프로그램이 스레딩을 지원하지 않는 프로그램보다 스레드당 스택 공간이 적기 때문이다. 커널은 일반적으로 멀티 스레드이므로, 커널 개발을 처음 접하는 사람은 일반적으로 재귀 알고리즘이나 큰 스택 버퍼의 사용을 자제한다.[7]

3. 해결 방법 및 예방

일반적인 프로그램 실행 환경에서는 서브루틴(함수, 프로시저, 메서드) 호출 관련 정보를 저장하는 스택 메모리 영역(콜 스택)이 확보된다. 서브루틴 호출 시마다 데이터가 스택에 쌓이고(푸시), 서브루틴이 끝나고 제어 흐름이 호출한 쪽으로 돌아오면 스택에서 데이터가 꺼내진다(팝). 콜 스택은 서브루틴 내에서만 사용되는 지역 변수를 위한 영역이기도 하다. 운영 체제나 실행 옵션에 따라 콜 스택에 저장할 수 있는 정보량에는 상한이 있다. 콜 스택에 축적되는 데이터량이 한계를 넘으면 "오버플로우"되어 정의되지 않은 동작을 일으킨다.

스택 오버플로우의 가장 흔한 원인은 서브루틴의 재귀 호출에 의한 무한 루프(무한 재귀)이다. 유한 재귀라도 재귀 호출이 너무 깊어지면 스택 오버플로우가 발생할 수 있다. 꼬리 재귀 최적화를 구현한 프로그래밍 언어에서는 꼬리 재귀를 루프로 전개하여 스택 오버플로우를 방지한다.

흔한 다른 원인은 스택에 거대한 배열을 확보하려는 것이다. 콜 스택의 용량 제한 때문에 거대한 지역 변수 대신 힙 영역 등을 명시적으로 사용해야 한다.

3. 1. 코드 검토 및 수정

스택 오버플로의 가장 흔한 원인은 무한 루프에 빠진 재귀 호출이다.[17] 테일 콜 최적화 기능이 있는 스킴 같은 언어는 테일 콜이 추가적인 스택 공간을 차지하지 않으므로 스택 오버플로 없이 테일 반복으로 무한 반복을 일으킬 수 있다.[17]

C에서 무한 루프의 예시는 다음과 같다.

```c

int foo() {

return foo();

}

```

`foo` 함수가 호출되면 자신을 다시 호출하여 스택 오버플로가 발생할 때까지 스택에 추가 공간을 사용하며, 결국 세그멘테이션 오류가 발생한다.[18]

일반적인 프로그램 실행 환경에서는 서브루틴(함수, 프로시저, 메서드) 호출 정보를 저장하는 스택 메모리 영역(콜 스택)이 확보된다. 서브루틴 호출 시마다 데이터가 스택에 쌓이고(푸시), 서브루틴이 끝나면 호출한 쪽으로 제어 흐름이 돌아오면서 데이터가 스택에서 제거된다(팝). 콜 스택은 서브루틴 내에서만 사용되는 지역 변수를 저장하는 영역이기도 하다. 운영 체제나 실행 옵션에 따라 콜 스택에 저장 가능한 정보량에 상한이 있다. 이 한계를 넘으면 스택은 "오버플로우"되어 정의되지 않은 동작을 일으킨다. 보통 프로그램이 충돌하지만, 스택 오버플로우를 유발하는 코드 패턴은 비교적 한정적이다.

스택 오버플로우의 주요 원인 중 하나는 서브루틴의 재귀 호출로 인한 무한 루프(무한 재귀)이다. 설계상 유한 재귀여도 재귀 호출 횟수가 너무 많으면 콜 스택 상한을 넘어 스택 오버플로우가 발생할 수 있다. 그러나 꼬리 재귀 최적화를 구현한 프로그래밍 언어는 꼬리 재귀 형태의 서브루틴을 루프로 바꿔 스택 오버플로우를 방지한다. 재귀 호출 대신 플랫한 루프 처리로 변환되어 스택을 소비하지 않기 때문이다. 재귀가 아닌 서브루틴 호출도 호출 횟수가 너무 많으면 스택 오버플로우가 발생할 수 있다.

흔한 다른 원인은 스택에 큰 배열을 할당하려는 것이다. 콜 스택의 정보량에는 상한이 있으므로, 큰 지역 변수 대신 힙 영역을 명시적으로 사용해야 한다. 배열 하나의 크기가 크지 않더라도 재귀 호출 등으로 서브루틴 호출이 많아지면 콜 스택을 소모하여 스택 오버플로우가 발생할 수 있다.

```c

/* 함수의 반환값이 int인 경우, C언어에서는 전방 선언이나 전방 정의가 없어도 컴파일이 가능하지만, C++에서는 전방 선언 또는 전방 정의가 필요하다. */

int g(void);

int f(void) {

return g();

}

int g(void) {

return f();

}

```

함수 `f()`는 `g()`를 호출하고, `g()`는 다시 `f()`를 호출한다. 이러한 상호 호출은 끝나지 않고 결국 스택 오버플로를 일으킨다. 종료 조건이 없는 다음 자기 재귀도 무한 재귀가 된다.

```c

int foo(void) {

return foo();

}

```

C/C++에서 함수 내 지역 변수(블록 스코프 변수)는 기본적으로 ( `static` 키워드가 없으면) 자동 변수이며, 블록을 벗어나면 자동으로 메모리가 해제되는 자동 기억 영역 기간을 가진다.[8] 언어 사양은 자동 변수 구현을 특별히 규정하지 않지만, 일반적으로 콜 스택을 이용한다.

시스템 메인 메모리 용량과 관계없이, 각 프로그램 스택 영역 크기는 스레드마다 보통 수 MiB 정도[9][10][11][12][13][14]이며, 큰 배열을 확보하기에는 부적합하다. 컴파일 옵션이나 스레드 시작 옵션으로 스택 크기 기본값을 변경할 수 있지만, 스택 크기를 늘리면 프로세스 내에서 시작 가능한 스레드 최대 수가 줄어든다.

```c

#include

int main(void) {

int ary[1000 * 1000 * 1000]; /* 배열이 너무 큼 */

printf("Hello!\n"); /* &"Hello!\n"의 푸시가 스택 오버플로우가 됨 */

}

```

이 경우,

```c

#include

int ary[1000 * 1000 * 1000];

int main(void) {

printf("Hello!\n");

}

```

또는

```c

#include

int main(void) {

static int ary[1000 * 1000 * 1000];

printf("Hello!\n");

}

```

처럼 배열을 정적 영역으로 옮기거나, malloc 등을 사용하여 힙 영역에 동적 할당하면 스택 오버플로우를 피할 수 있다. 단, 32비트 환경에서는 주소 공간 상한이 4 GiB이므로, 위와 같이 큰 배열을 정적으로 할당하면 프로그램 시작이 실패할 수 있다. 일반적으로 시스템의 빈 메모리 양은 불확실하므로, 64비트 이상 환경에서도 실행 시 동적 메모리 할당과 성공 여부 검사가 권장된다.

```c

#include

#include

int main(void) {

/* malloc() 함수가 실패한 경우 NULL 포인터가 반환되므로, 실제로 메모리 영역을 이용하기 전에 NULL 검사를 해야 함 */

int *ary = (int *)malloc(sizeof(int) * (size_t)(1000 * 1000 * 1000));

printf("Hello!\n");

/* NULL에 대해 free() 함수를 실행해도 아무 일도 일어나지 않는다는 것이 C/C++ 규격으로 보장되므로, NULL 검사는 불필요 */

free(ary);

}

```

Java나 C#/VB.NET처럼 가상 머신 기반 실행 환경을 가진 언어는 OS가 아닌 가상 머신이 관리하는 힙 영역을 사용하여 동적 메모리 할당 오버헤드를 줄였다. 이러한 언어는 일반 배열이 항상 힙 영역에 할당되므로, 실행 시 메모리 부족 예외는 발생해도 스택 오버플로우는 발생하지 않는다. 반면 C/C++ 동적 메모리 할당은 오버헤드가 커서 모든 배열을 힙에 할당하면 속도가 크게 저하된다. Microsoft Visual C++ 런타임 라이브러리는 임계값에 따라 스택과 힙을 구분하여 사용하는 `_malloca()` 함수를 제공한다.[15]

3. 2. 스택 크기 설정

C와 같은 언어에서는 프로그램 실행 환경에서 서브루틴(함수, 프로시저, 메서드) 호출 정보를 저장하는 스택 메모리 영역(콜 스택)이 할당된다. 서브루틴 호출 시 데이터가 스택에 쌓이고(푸시), 종료 후 제어 흐름이 호출 측으로 돌아오면 데이터가 제거된다(팝). 콜 스택은 지역 변수 저장에도 사용된다. 운영 체제나 실행 옵션에 따라 콜 스택 정보량에 상한이 있으며, 한계를 넘으면 스택 오버플로우가 발생하여 정의되지 않은 동작을 일으킨다. 대책이 없으면 일반적으로 프로세스가 충돌한다.[18]

스택 오버플로우의 흔한 원인 중 하나는 스택에 큰 배열을 할당하려는 것이다. 콜 스택 정보량에는 상한이 있으므로, 큰 지역 변수 대신 힙 영역 등을 명시적으로 사용해야 한다. 배열 크기가 크지 않더라도 재귀 호출 등으로 호출 계층이 깊어지면 콜 스택을 소모하여 스택 오버플로우가 발생할 수 있다.[18]

3. 3. 동적 메모리 할당 활용

일반적인 프로그램 실행 환경에서는 서브루틴(함수, 프로시저, 메서드) 호출 정보와 지역 변수를 저장하기 위해 스택 메모리 영역(콜 스택)이 사용된다.[8] 콜 스택의 저장 용량은 제한되어 있어, 너무 많은 데이터가 쌓이면 "스택 오버플로우"가 발생하여 프로그램이 비정상적으로 종료될 수 있다.

스택 오버플로우의 흔한 원인 중 하나는 스택에 너무 큰 배열을 할당하려는 것이다. C/C++(C++)에서 함수 내 지역 변수는 기본적으로 콜 스택에 할당되는데[8], 시스템의 메인 메모리 용량과 관계없이 각 프로그램의 스택 영역 크기는 스레드마다 보통 수 MiB 정도로 제한된다.[9][10][11][12][13][14] 따라서 대용량 배열을 스택에 할당하면 스택 오버플로우가 발생할 수 있다.

이러한 문제를 해결하기 위해 `malloc`, `free` 등의 함수를 사용하여 힙 영역에 동적으로 메모리를 할당할 수 있다. 다음은 C/C++에서 `malloc`과 `free`를 사용하여 동적 메모리 할당을 하는 예시이다.



#include

#include

int main(void) {

/* malloc() 함수가 실패한 경우 NULL 포인터가 반환되므로, 실제로 메모리 영역을 이용하기 전에 NULL 검사를 해야 함 */

int *ary = (int *)malloc(sizeof(int) * (size_t)(1000 * 1000 * 1000));

printf("Hello!\n");

/* NULL에 대해 free() 함수를 실행해도 아무 일도 일어나지 않는다는 것이 C/C++ 규격으로 보장되므로, NULL 검사는 불필요 */

free(ary);

}



`malloc` 함수는 요청한 크기의 메모리 블록을 힙 영역에 할당하고, 할당된 메모리의 시작 주소를 반환한다. 메모리 할당에 실패하면 `NULL` 포인터를 반환하므로, 반환값을 반드시 확인해야 한다. `free` 함수는 `malloc`으로 할당받은 메모리 영역을 해제하여 시스템에 반환한다.

Java나 C# / VB.NET과 같은 가상 머신 기반 언어에서는 가상 머신이 관리하는 힙 영역을 사용하여 동적 메모리 할당 오버헤드를 줄였다. 이러한 언어에서는 일반적인 배열이 항상 힙 영역에 할당되므로 메모리 부족 예외는 발생할 수 있지만, 스택 오버플로우는 발생하지 않는다. 하지만 C/C++에 비해 동적 메모리 할당 오버헤드가 크기 때문에, 모든 배열을 힙에 할당하는 것은 성능 저하를 초래할 수 있다.[15]

참조

[1] 웹사이트 Using and Porting GNU Fortran http://sunsite.ualbe[...] 1991-06-01
[2] 웹사이트 What is the difference between a segmentation fault and a stack overflow? https://stackoverflo[...] 2021-09-13
[3] 웹사이트 An Introduction to Scheme and its Implementation http://www.federated[...] 1997-02-19
[4] 웹사이트 Using the GNU Compiler Collection (GCC): Optimize Options https://gcc.gnu.org/[...] 2017-08-20
[5] 논문 Revised5 Report on the Algorithmic Language Scheme http://www.schemers.[...] 2012-08-09
[6] 웹사이트 Modern Memory Management, Part 2 http://www.onlamp.co[...] 2007-08-14
[7] 웹사이트 Kernel Programming Guide: Performance and Stability Tips https://developer.ap[...] Apple Inc. 2014-05-02
[8] 웹사이트 Storage-class specifiers - cppreference.com https://en.cpprefere[...]
[9] 웹사이트 Thread Stack Size - Win32 apps | Microsoft Learn https://learn.micros[...]
[10] 웹사이트 /F (Set Stack Size) | Microsoft Learn https://learn.micros[...]
[11] 웹사이트 /STACK (Stack allocations) | Microsoft Learn https://learn.micros[...]
[12] 웹사이트 2.4 スタックとスタックサイズ - Oracle Solaris Studio 12.2: OpenMP API ユーザーガイド https://docs.oracle.[...]
[13] 웹사이트 Android ランタイム(ART)でのアプリの動作確認  |  Android デベロッパー  |  Android Developers https://developer.an[...]
[14] 웹사이트 Threading Programming Guide - Thread Management | Apple Developer Documentation Archive https://developer.ap[...]
[15] 웹사이트 _malloca | Microsoft Learn https://learn.micros[...]
[16] 웹인용 Using and Porting GNU Fortran http://sunsite.ualbe[...] 2016-11-25
[17] 웹인용 An Introduction to Scheme and its Implementation http://www.federated[...] 2007-08-14
[18] 웹사이트 What is the difference between a segmentation fault and a stack overflow? http://stackoverflow[...]



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com