사전 세그멘테이션 폴트
문제

세그멘테이션 폴트

gabury1고친 사람 github-actions[bot]

세그멘테이션 폴트는 프로그램이 건드릴 권한이 없는 메모리를 건드렸을 때 운영체제가 그 프로그램을 즉시 끝내 버리는 일입니다. 잘못된 번지를 읽거나 쓰려는 순간에 걸리고, 프로그램은 다음 줄로 넘어가지 못합니다. 오류를 알리고 계속 도는 것이 아니라 아예 멈추는 쪽을 고른 것입니다.

쉽고 빠른 이해

세그멘테이션 폴트는 잘못된 메모리를 건드린 프로그램을 운영체제가 강제로 끝내는 일입니다. 값이 놓인 번지를 담아 두는 변수를 포인터라고 하는데, 아무 데도 안 가리키는 포인터로 값을 쓰려 할 때 흔히 납니다.

이렇게 끊는 까닭은 두 가지입니다. 없는 칸을 읽는 명령은 끝낼 방법이 없고, 엉뚱한 번지를 부른 프로그램은 더 믿을 수 없습니다. 그냥 두면 틀린 값 위에서 계산이 이어집니다.

어떻게 도는가:

  1. 프로그램이 번지 하나를 읽거나 쓰려 합니다
  2. 하드웨어가 그 번지를 이 프로그램이 건드려도 되는지 봅니다
  3. 안 되면 운영체제로 넘어가고, 운영체제가 프로그램을 끝냅니다

대가가 있습니다. 프로그램이 통째로 죽고, 잘못 쓴 줄과 죽는 줄이 서로 멀 수 있어 원인을 찾기 어렵습니다.

상세

큰 사무실 건물을 떠올려 보겠습니다. 출입증에는 들어가도 되는 층이 정해져 있어서, 다른 층 문에 카드를 대면 문이 안 열립니다. 그러면 경비가 와서 왜 그랬는지 묻지 않고 그 사람을 건물 밖으로 내보냅니다.

세그멘테이션 폴트는 이 내보내는 일에 해당합니다. 프로세스는 실행 중인 프로그램 하나를 뜻하는데, 프로세스마다 쓸 수 있는 메모리 범위가 따로 정해져 있습니다. 그 범위 밖을 건드리면 운영체제가 프로세스를 끝냅니다.

경고만 하고 이어서 돌리지 않는 까닭은 둘입니다. 하나는 이어갈 방법이 없다는 것입니다. 읽으려던 칸이 아예 없으니 그 명령을 끝낼 수가 없습니다.

다른 하나는 이 시점의 프로그램을 믿을 수 없다는 것입니다. 엉뚱한 번지를 부른 프로그램은 이미 자기가 무엇을 다루는지 놓친 상태입니다. 그대로 두면 틀린 값 위에서 계산이 이어지고, 잘못된 결과가 파일이나 데이터베이스까지 내려갑니다.

이 절은 먼저 「건드려도 된다」가 무엇을 뜻하는지 보고, 그다음 끊기는 순서를 따라갑니다. 마지막으로 어떤 코드가 이 오류를 부르는지, 그리고 왜 매번 같은 곳에서 안 터지는지를 봅니다.

허락된 번지와 허락된 방식

프로그램이 쓰는 번지는 대개 가상 메모리의 번지입니다. 가상 메모리는 프로세스마다 가짜 번지를 따로 주고, 그것을 실제 메모리 칸으로 바꿔 주는 구조입니다. 이 덕분에 프로세스는 옆 프로세스의 번지를 아예 부를 수 없습니다.

바꿔치기는 페이지 단위로 합니다. 페이지는 메모리를 나누는 고정 크기 덩어리입니다. 페이지마다 이 프로세스가 읽어도 되는지, 써도 되는지가 표에 적혀 있습니다.

그래서 「잘못된 번지」는 두 갈래입니다. 하나는 어떤 페이지에도 이어지지 않은 번지이고, 다른 하나는 이어져 있지만 허락되지 않은 방식으로 건드린 번지입니다. 문자열 상수를 담아 둔 쓰기 금지 페이지에 값을 넣으려는 것이 뒤쪽 갈래입니다.

끊기는 순서

번지를 검사하는 것은 운영체제가 아니라 하드웨어입니다. CPU(Central Processing Unit, 중앙처리장치) 안에는 MMU(Memory Management Unit, 메모리 관리 장치)라는 부품이 있어서, 가짜 번지를 실제 번지로 바꾸면서 권한도 같이 봅니다. 명령어마다 검사가 도는데도 프로그램이 안 굼떠지는 까닭이 이것입니다.

아래 그림은 번지 하나가 검사를 지나는 길입니다. 두 갈래 중 커널로 넘어가는 쪽이 세그멘테이션 폴트입니다.

flowchart TD
    A["프로그램이 번지를 건넨다"] --> B["메모리 관리 장치가 권한 표를 본다"]
    B -->|건드려도 되는 번지| C["그대로 읽고 쓴다"]
    B -->|건드리면 안 되는 번지| D["커널로 넘어간다"]
    D --> E["커널이 종료 신호를 보낸다"]
    E --> F["프로세스가 끝난다"]

그림의 커널은 운영체제의 핵심부로, 메모리와 프로세스를 관리하는 쪽입니다. 커널은 잘못을 저지른 프로세스에게 종료 신호를 보내고, 프로그램이 그 신호를 따로 받아 처리하지 않으면 프로세스가 끝납니다.

유닉스 계열에서는 이런 신호를 시그널이라고 부르고, 지금 경우에 오는 시그널의 이름이 SIGSEGV입니다. 셸에는 대개 Segmentation fault 한 줄만 남습니다. 어느 줄에서 났는지는 그 한 줄에 안 적힙니다.

이 오류를 부르는 흔한 코드

실무에서 보는 원인은 몇 가지로 모입니다. 아래는 그중 자주 나오는 것들이고, 각각 자기 이름을 가진 별도의 실수입니다.

무엇을 했나 어떤 번지를 건드리나
널 포인터를 따라감 어떤 페이지도 없는 0번지 언저리
해제 후 사용 돌려준 뒤 회수된 힙의 한 조각
배열 범위를 넘김 원래 쓰려던 것에서 한참 벗어난 번지
재귀가 안 끝남 스택 끝을 넘어간 번지
문자열 상수에 값을 씀 쓰기가 막힌 페이지

이 중 널 포인터를 따라가는 것이 제일 흔합니다. 포인터는 값이 아니라 값이 놓인 번지를 담는 변수인데, 아무 데도 안 가리키는 상태를 널이라고 부릅니다.

C
int *p = NULL;   // 아무 데도 안 가리킨다
*p = 1;          // 이 줄에서 끊긴다

둘째 줄은 「p 가 가리키는 칸에 1 을 넣어라」는 뜻입니다. 가리키는 칸이 없으므로 프로그램은 셋째 줄로 못 갑니다.

잘못한 줄과 죽는 줄이 다르다

검사가 페이지 단위라, 범위를 조금 넘긴 것은 안 걸릴 때가 있습니다. 배열 끝을 몇 칸 넘어 쓴 값이 같은 페이지 안에 떨어지면 아무 일도 안 납니다. 그 값은 옆에 있던 다른 변수를 조용히 덮어씁니다.

그래서 세그멘테이션 폴트는 잘못을 저지른 줄이 아니라 한참 뒤의 멀쩡해 보이는 줄에서 터지기도 합니다. 같은 프로그램이 어떤 날은 죽고 어떤 날은 안 죽는 것도 같은 까닭입니다. 무엇이 어느 번지에 놓이는지가 실행할 때마다 달라지기 때문입니다.

C와 C++ 표준은 이런 접근을 정의되지 않은 동작으로 둡니다. 무엇이 일어날지 정해 두지 않았다는 뜻이고, 프로세스가 죽는 것은 그 결과 중 하나일 뿐입니다.

끊긴 지점을 찾는 법

프로세스가 세그멘테이션 폴트로 끝날 때 운영체제는 그 순간의 메모리 상태를 파일로 남길 수 있습니다. 이 파일을 코어 덤프라고 부릅니다. 디버거에 이 파일을 물리면 어느 함수의 어느 줄에서 끊겼는지가 나옵니다.

다만 앞 소절에서 본 대로 끊긴 줄이 잘못한 줄이 아닐 수 있습니다. 그래서 메모리 접근을 하나하나 지켜보는 검사 도구를 함께 씁니다. 이런 도구는 프로그램을 굼뜨게 만드는 대신 잘못 쓴 줄을 바로 짚어 줍니다.

이 오류를 거의 안 보는 쪽

Java나 Python처럼 메모리를 직접 다루지 않는 언어에서는 이 오류를 거의 못 봅니다. 실행기가 모든 접근을 대신 검사해서, 잘못을 언어 차원의 예외로 바꿔 주기 때문입니다. 널을 따라가도 프로세스가 죽는 대신 예외 하나가 납니다.

그래도 완전히 사라지지는 않습니다. 실행기 자신과 속도를 위해 C 로 쓰인 확장 모듈은 번지를 직접 다룹니다. 그쪽에 잘못이 있으면 언어와 상관없이 프로세스가 통째로 죽습니다.

관련 항목

세그멘테이션 폴트를 일으키는 코드의 실수

널 포인터 · 널 포인터 역참조 · 해제 후 사용 · 이중 해제 · 허상 포인터 · 버퍼 오버플로 · 스택 오버플로 · 정의되지 않은 동작

세그멘테이션 폴트를 가려내는 하드웨어와 운영체제 구조

가상 메모리 · 주소 공간 · 페이지 · 페이지 테이블 · MMU · 메모리 보호 · 커널 · 사용자 공간

세그멘테이션 폴트가 프로세스에 전달되는 경로

SIGSEGV · 시그널 · 트랩 · 하드웨어 예외 · 페이지 폴트 · 코어 덤프

세그멘테이션 폴트가 자주 나는 메모리 구역

스택 · 힙 · 스택 프레임 · 세그먼트 · 데이터 영역 · 텍스트 영역

끊긴 지점을 뒤쫓는 도구

디버거 · gdb · Valgrind · AddressSanitizer · 스택 트레이스 · 심볼 테이블

이 오류를 내는 쪽에 자주 서는 언어

C · C++ · Rust · 포인터 · 수동 메모리 관리 · 메모리 안전성

프로세스가 끝나는 다른 방식

커널 패닉 · 메모리 부족 킬러 · 런타임 에러 · NullPointerException · 버스 오류 · SIGKILL

다른 이름: segmentation fault · segfault · 세그먼테이션 폴트 · 세그먼트 폴트