사전 SIGSEGV
에러코드

SIGSEGV

gabury1고친 사람 github-actions[bot]

SIGSEGV 는 프로그램이 건드리면 안 되는 메모리를 건드렸다고 운영체제가 알리는 신호입니다. 이 신호를 받은 프로그램은 손쓰지 않으면 곧바로 끝납니다. 서버에서는 프로세스가 종료 코드 139 를 남기고 사라지는 모습으로 자주 만납니다.

쉽고 빠른 이해

SIGSEGV 는 「방금 잘못된 메모리를 건드렸으니 여기서 멈춥니다」라는 통보입니다. 아무것도 안 가리키는 포인터로 값을 쓰려는 C 프로그램이 이 통보를 받고 죽습니다.

읽거나 쓰려던 칸에 닿지 못했으니 프로그램은 그다음으로 넘어갈 수 없습니다. 통보를 보내 프로그램을 끝내면 틀린 값 위에서 계산이 이어지지 않습니다.

어떻게 도는가:

  1. 프로그램이 허락되지 않은 번지(메모리 주소)를 읽거나 씁니다
  2. 하드웨어가 이를 막고 운영체제에 넘깁니다
  3. 운영체제가 그 프로그램에 SIGSEGV 를 보냅니다. 프로그램은 끝납니다

대가가 있습니다. 프로그램이 하던 일을 전부 잃고 죽습니다. 남는 것은 종료 코드 139 와 짧은 한 줄뿐이라 원인은 따로 찾아야 합니다.

상세

이 절은 SIGSEGV 가 어떤 값이고, 언제 오고, 받은 프로세스에 무슨 일이 생기는지를 봅니다. 널 포인터를 따라간 C 프로그램 하나로 처음부터 끝까지 봅니다.

뒤쪽에서는 백엔드 개발자가 이 값을 만나는 장면을 봅니다. 종료 코드 139 로 끝난 프로세스와, 이 신호를 직접 받아 보려는 코드입니다.

시그널이라는 통로

시그널은 커널이 실행 중인 프로세스에 보내는 짧은 알림입니다. 커널은 운영체제의 핵심부로, 메모리와 프로세스를 관리하는 쪽입니다. 프로세스는 실행 중인 프로그램 하나를 뜻합니다.

시그널이 필요한 것은 프로세스가 커널에 먼저 묻지 않기 때문입니다. 프로세스는 자기 코드를 돌리느라 바쁩니다. 바깥에서 무슨 일이 생겼는지 스스로 살피지 않습니다. 그래서 커널이 끼어들어 알릴 통로가 있어야 합니다.

시그널마다 번호와 이름이 붙어 있습니다. 키보드에서 Ctrl+C 를 누르면 오는 SIGINT(interrupt, 끼어들기), 프로세스를 강제로 끝내는 SIGKILL이 그런 이름입니다. SIGSEGV 는 그 가운데 메모리 접근이 잘못됐을 때 오는 시그널입니다.

이름과 번호

SIGSEGV 의 SEGV 는 segmentation violation 의 줄임입니다. 메모리 구역을 어겼다는 뜻입니다. 이름 앞의 SIG 는 시그널 이름에 공통으로 붙는 머리말입니다.

칸 값
이름 SIGSEGV
번호 11 (x86 · ARM 리눅스 기준)
뜻 Invalid memory reference, 잘못된 메모리 참조
기본 동작 프로세스를 끝내고 코어 덤프(끝날 때의 메모리 사본)를 남긴다

번호 11 은 리눅스가 도는 대부분의 아키텍처에서 같습니다. 다만 시그널 번호는 운영체제와 아키텍처마다 갈릴 수 있습니다. 이름은 유닉스 계열이 함께 따르는 표준(POSIX, Portable Operating System Interface)으로 묶여 있지만 번호는 그렇지 않습니다. 코드에서 숫자 대신 SIGSEGV 라는 이름으로 쓰는 것은 이 때문입니다.

시그널이 오는 순서

이 소절은 SIGSEGV 한 번이 만들어지는 과정을 따라갑니다. 아래 C 코드의 둘째 줄이 출발점입니다.

C
int *p = NULL;  // 아무 데도 안 가리킨다
*p = 1;         // 여기서 SIGSEGV

포인터는 값이 아니라 값이 놓인 번지(메모리 주소)를 담는 변수입니다. NULL 은 아무 데도 안 가리킨다는 표시로, 보통 0번지로 나타냅니다. 둘째 줄은 「p 가 가리키는 칸에 1 을 넣어라」라는 뜻인데, 가리키는 칸이 없습니다.

그런데 프로그램이 보는 번지는 실제 메모리 칸의 위치와 같지 않습니다. 운영체제는 프로세스마다 제 번지를 매겨 줍니다. 그 번지가 실제로 어느 칸인지는 실행할 때마다 하드웨어가 찾아 줍니다.

이 번지를 실제 칸으로 바꾸는 것은 CPU 안의 메모리 관리 장치입니다. 메모리 관리 장치(MMU, Memory Management Unit)는 바꾸는 김에 이 프로세스가 그 칸을 건드려도 되는지도 봅니다. 허락되지 않은 칸이면 그 자리에서 접근을 멈춥니다.

sequenceDiagram
    participant 프로그램
    participant 메모리 관리 장치
    participant 커널
    프로그램->>메모리 관리 장치: 0번지에 1을 써라
    메모리 관리 장치->>커널: 허락 안 된 번지다
    Note over 커널: 고칠 수 있는 경우인지 본다
    커널->>프로그램: SIGSEGV

그림처럼 메모리 관리 장치는 막힌 접근을 커널로 넘깁니다. 이때 CPU 는 실행하던 명령을 멈춥니다. 여기서 명령은 코드 한 줄이 아니라 CPU 가 실제로 실행하는 기계어 한 단위입니다. 이렇게 하드웨어가 명령을 멈추고 커널을 부르는 일을 하드웨어 예외라고 합니다.

메모리 접근에서 나는 하드웨어 예외는 페이지 폴트라고 부릅니다. 커널은 이것을 받으면 먼저 자기가 고칠 수 있는 경우인지 봅니다.

모든 페이지 폴트가 SIGSEGV 가 되지는 않습니다. 메모리가 모자라 잠시 디스크에 옮겨 둔 데이터를 건드린 경우가 그렇습니다. 커널이 그 데이터를 메모리로 다시 올리면 프로그램은 이어서 돕니다. 커널이 고칠 수 없을 때만 SIGSEGV 를 보냅니다.

SIGSEGV 는 잘못된 명령을 실행한 스레드에 갑니다. 스레드는 한 프로세스 안에서 따로 도는 실행 흐름입니다. 여러 스레드 중 누가 잘못했는지가 분명하기 때문입니다.

잘못된 번지의 두 갈래

커널이 고칠 수 없는 번지는 두 갈래로 나뉩니다. 앞의 코드는 첫째 갈래입니다.

갈래 무엇을 했나 흔한 예
없는 번지 어떤 메모리에도 이어지지 않은 번지를 건드림 널 포인터를 따라감 · 해제한 메모리를 다시 씀
허락 안 된 방식 메모리는 있지만 그 방식이 막혀 있음 읽기만 되는 칸에 값을 씀

두 갈래 모두 같은 SIGSEGV 로 옵니다. 어느 쪽인지는 뒤의 「시그널 핸들러로 받기」 소절에서 보는 부가 정보로 가립니다.

이런 잘못이 프로세스를 끝내는 일 자체를 세그멘테이션 폴트라고 부릅니다. 세그멘테이션 폴트가 사건의 이름이라면, SIGSEGV 는 그 사건을 프로세스에 알리는 값입니다.

받은 프로세스에 생기는 일

SIGSEGV 의 기본 동작은 프로세스를 끝내는 것입니다. 프로그램이 준비해 둔 것이 없으면 이 동작이 일어납니다. 끝나기 직전의 메모리 상태를 파일로 남기기도 합니다.

그 파일을 코어 덤프라고 부릅니다. 코어 덤프를 디버거에 물리면 어느 함수의 어느 줄에서 끝났는지 볼 수 있습니다. 실제로 파일이 남는지는 시스템 설정에 따라 다릅니다.

경고만 하고 이어서 돌리지 않는 데는 이유가 둘 있습니다. 첫째, CPU 는 실패한 명령을 마치지 못하면 다음 명령으로 넘어가지 않는데, 그 명령은 마칠 방법이 없습니다. 둘째, 엉뚱한 번지를 부른 프로그램은 이미 자기 상태를 믿을 수 없습니다. 그대로 두면 틀린 값 위에서 계산이 계속됩니다.

셸에서 프로그램을 돌렸다면 터미널에 한 줄이 찍힙니다.

터미널
./app      # Segmentation fault
echo $?    # 139

첫 줄의 Segmentation fault 는 죽은 프로그램이 찍은 것이 아닙니다. 자식 프로세스가 시그널로 끝난 것을 보고 셸이 대신 찍습니다. 코어 덤프가 남았으면 뒤에 (core dumped) 가 붙습니다.

종료 코드 139

둘째 줄의 $? 는 바로 앞 명령의 종료 코드입니다. 종료 코드는 프로세스가 끝나며 남기는 숫자입니다. 0 이면 성공입니다. 부모 프로세스는 이 숫자를 보고 자식이 어떻게 끝났는지 압니다.

시그널로 끝난 프로세스에는 128 에 시그널 번호를 더한 값이 붙습니다. SIGSEGV 의 번호가 11 이니 128 + 11 = 139 입니다. 셸이 정한 이 셈을 컨테이너 런타임과 프로세스 관리 도구도 대개 따릅니다.

그래서 컨테이너가 종료 코드 139 로 재시작을 되풀이한다면, 그 안의 프로세스가 SIGSEGV 로 죽고 있는 것입니다. 애플리케이션 로그에는 아무것도 안 남는 경우가 많습니다. 프로그램이 로그를 쓸 틈도 없이 끝나기 때문입니다.

Java 나 Python 처럼 메모리를 직접 다루지 않는 언어에서도 139 를 볼 수 있습니다. 실행기(자바 가상 머신·파이썬 인터프리터처럼 코드를 돌려 주는 프로그램) 자신이나, 속도를 위해 C 로 쓰인 확장 모듈은 번지를 직접 다룹니다. 그쪽에 잘못이 있으면 언어와 상관없이 프로세스가 SIGSEGV 로 끝납니다.

시그널 핸들러로 받기

프로세스는 시그널이 왔을 때 돌릴 함수를 미리 걸어 둘 수 있습니다. 이 함수를 시그널 핸들러라고 부릅니다. 핸들러는 sigaction 호출로 겁니다. 핸들러를 걸면 기본 동작 대신 그 함수가 돕니다.

SIGSEGV 에 핸들러를 걸면 대개 죽기 전에 무언가를 남깁니다. 어느 함수들을 거쳐 왔는지 찍거나, 오류 보고 서버로 보낼 기록을 쓰고 끝냅니다.

핸들러는 부가 정보도 받습니다. 그중 si_addr 는 문제가 된 번지입니다. si_code 는 앞에서 본 두 갈래를 가립니다.

si_code 값 갈래
SEGV_MAPERR 없는 번지
SEGV_ACCERR 허락 안 된 방식

핸들러가 그냥 돌아올 때 생기는 되풀이

핸들러가 할 일을 마치고 그냥 돌아오면 문제가 생깁니다. 프로그램은 실패했던 그 명령으로 돌아가 다시 실행합니다. 번지는 여전히 잘못돼 있으니 SIGSEGV 가 또 옵니다.

flowchart TD
    A["SIGSEGV 도착"] --> B{"핸들러가 걸려 있나"}
    B -->|아니다| C["프로세스가 끝난다 · 종료 코드 139"]
    B -->|그렇다| D["핸들러가 돈다"]
    D --> E{"핸들러가 어떻게 끝나나"}
    E -->|스스로 프로세스를 끝낸다| F["기록을 남기고 끝난다"]
    E -->|그냥 돌아온다| G["같은 명령을 다시 실행한다"]
    G --> A

그림의 맨 아래 화살표가 그 되풀이입니다. 핸들러가 원인을 고치지 않는 한 프로그램은 같은 곳에서 끝없이 걸립니다. 그래서 SIGSEGV 핸들러는 기록을 남긴 뒤 프로세스를 직접 끝내는 꼴로 씁니다.

스택이 넘쳐서 온 SIGSEGV

스택은 함수를 부를 때마다 지역 변수를 쌓아 두는 메모리 구역입니다. 재귀가 끝나지 않으면 스택이 바닥납니다. 그 너머 번지를 건드리면 SIGSEGV 가 옵니다.

스택이 넘쳐서 생긴 이 경우를 스택 오버플로라고 부릅니다. 이때는 핸들러도 돌지 못합니다. 핸들러도 함수라서 돌려면 스택이 필요한데, 스택이 이미 다 찼기 때문입니다.

이를 피하려면 핸들러만 쓸 예비 스택을 마련해 둡니다. 예비 스택은 sigaltstack 호출로 등록합니다. 그다음 sigaction 으로 핸들러를 걸 때 SA_ONSTACK 표시를 주면 핸들러가 그 예비 스택에서 돕니다.

막거나 무시할 수 없는 경우

시그널은 대개 막아 두거나 무시하도록 정할 수 있습니다. 막아 두면 시그널이 와도 전달이 잠시 미뤄집니다. 무시하면 시그널이 와도 아무 일 없이 버려집니다.

하지만 메모리 접근에서 생긴 SIGSEGV 를 막거나 무시했을 때 무슨 일이 생길지는 정해져 있지 않습니다. 리눅스는 이 경우 설정을 기본 동작으로 되돌려 프로세스를 끝냅니다.

이유는 「받은 프로세스에 생기는 일」에서 본 것과 같습니다. 실패한 명령을 건너뛸 방법이 없으니 프로세스를 살려 둘 수 없습니다.

다른 프로세스가 보낸 SIGSEGV

SIGSEGV 는 메모리 잘못이 없어도 받을 수 있습니다. kill -SEGV <프로세스 번호> 로 다른 프로세스가 직접 보내는 경우입니다.

이때도 기본 동작은 같습니다. 프로세스가 끝나고 종료 코드 139 가 남습니다. 오류 보고 도구가 제대로 기록을 남기는지 시험할 때 이렇게 일부러 보냅니다. 이 경우에는 문제가 된 번지가 없으므로 si_addr 가 의미 없는 값입니다.

헷갈리는 이웃 시그널

하드웨어 예외에서 오는 시그널은 SIGSEGV 말고도 있습니다. SIGBUS(bus error, 버스 오류) · SIGILL(illegal instruction, 잘못된 명령) · SIGFPE(floating-point exception, 부동소수점 예외)가 그렇습니다. SIGFPE 는 이름과 달리 정수 연산 오류에서도 옵니다.

서버에서 함께 보게 되는 종료 코드와 묶어 보면 이렇습니다. 번호는 x86 · ARM 리눅스 기준입니다.

시그널 번호 종료 코드 언제 오나
SIGSEGV 11 139 허락 안 된 메모리 접근
SIGBUS 7 135 정렬(번지가 정해진 배수에 맞아야 하는 규칙)이 어긋난 접근 · 메모리 번지에 이어 둔 파일이 줄어든 뒤 사라진 끝을 읽음
SIGILL 4 132 CPU 가 모르는 명령을 실행함
SIGFPE 8 136 정수를 0 으로 나눔 같은 연산 오류
SIGABRT 6 134 프로그램이 스스로 abort() 를 부름
SIGKILL 9 137 다른 프로세스나 커널이 강제로 끝냄

가장 자주 헷갈리는 것은 137 입니다. 메모리를 너무 많이 쓰면 커널의 OOM 킬러(Out Of Memory Killer)가 프로세스를 끝냅니다. OOM 킬러는 메모리가 바닥나면 도는 커널 기능입니다. 이때 SIGKILL 이 오고 137 이 남습니다. 둘 다 「메모리 문제로 죽었다」로 들리지만, 139 는 잘못된 번지를 건드린 것이고 137 은 메모리를 너무 많이 쓴 것입니다.

SIGBUS 와 SIGSEGV 는 경계가 흐립니다. 같은 잘못된 접근이 어떤 아키텍처에서는 SIGSEGV 로, 다른 아키텍처에서는 SIGBUS 로 옵니다. 그래서 둘 중 무엇이 왔는지보다 어느 번지를 건드렸는지를 봅니다.

관련 항목

SIGSEGV 가 속하는 시그널 체계

시그널 · POSIX · signal.h · 커널 · 프로세스 · 스레드

SIGSEGV 를 낳는 하드웨어와 메모리 구조

하드웨어 예외 · 페이지 폴트 · MMU · 가상 메모리 · 페이지 테이블 · 메모리 보호 · 트랩

SIGSEGV 가 알리는 사건과 그 원인이 되는 실수

세그멘테이션 폴트 · 널 포인터 · 널 포인터 역참조 · 해제 후 사용 · 허상 포인터 · 버퍼 오버플로 · 스택 오버플로 · 정의되지 않은 동작

SIGSEGV 를 받아 처리하는 호출

시그널 핸들러 · sigaction · signal(2) · sigaltstack · siginfo_t · kill · raise

SIGSEGV 로 끝난 뒤 남는 흔적

코어 덤프 · 종료 코드 · 디버거 · 스택 트레이스 · 컨테이너

SIGSEGV 와 헷갈리는 이웃 시그널

SIGBUS · SIGILL · SIGFPE · SIGABRT · SIGKILL · SIGTERM · OOM 킬러

다른 이름: segmentation violation · 시그널 11 · signal 11