C++ Segmentation fault | core dump
이 글의 핵심
C++ Segmentation fault에 대한 실전 가이드입니다. core dump 등을 예제와 함께 상세히 설명합니다.
들어가며: “Segmentation fault (core dumped)“가 뜨면
검색해서 들어오는 분들을 위해
Segmentation fault(세그멘테이션 폴트—허용되지 않은 메모리 영역에 접근할 때 OS가 프로세스를 강제 종료하는 오류)는 잘못된 메모리 접근으로 프로세스가 OS에 의해 강제 종료되었을 때 나옵니다. 널 포인터 역참조, 댕글링 포인터(이미 해제된 메모리를 가리키는 포인터), 스택 오버플로우, 버퍼 오버런(할당된 영역을 넘어서 쓰는 것) 등이 원인일 수 있습니다. 이 글은 에러 메시지를 그대로 검색해 들어온 독자를 위해, core dump(프로세스가 비정상 종료될 때 메모리 상태를 파일로 저장한 것—나중에 디버거로 원인 분석 가능)를 켜고, GDB/LLDB로 크래시 위치와 원인을 추적하는 단계별 프로세스를 정리합니다.
이 글에서 다루는 것:
- core dump 활성화: ulimit, systemd·커널 설정
- core 파일로 재현: GDB/LLDB로 core 로드, bt(backtrace), 프레임 이동·변수 확인
- 자주 나오는 원인: null 역참조, use-after-free, 스택 오버플로우
- AddressSanitizer(ASan) 로 사전에 잡기
- 프로덕션 패턴: core 수집·분석 자동화
관련 글: 디버거 GDB/LLDB, AddressSanitizer.
문제 시나리오: “이런 상황에서 막혔다”
시나리오 1: 프로덕션 서버에서 가끔만 크래시
"프로덕션에서 주 1~2회 서버가 죽어요. 로그에는 Segmentation fault만 찍고 끝나요."
"재현이 안 돼서 개발 환경에서는 못 찾겠어요."
상황: HTTP 서버가 24시간 운영 중이며, 특정 요청 패턴에서만 segfault가 발생합니다. 로그에는 Segmentation fault (core dumped)만 나오고 core 파일이 생성되지 않습니다.
원인: ulimit -c unlimited 없음, core_pattern이 /dev/null로 설정되어 있음, 또는 systemd가 core 제한을 0으로 둠.
해결 포인트: core dump 활성화 → coredumpctl 또는 core_pattern으로 core 수집 → GDB로 core 로드 후 bt로 크래시 위치 확인.
시나리오 2: 라이브러리 내부에서 크래시
"제 코드가 아니라 libfoo.so 안에서 segfault가 나요."
"어디서 포인터를 넘긴 건지 모르겠어요."
상황: 서드파티 라이브러리나 JNI 호출 시 라이브러리 내부에서 크래시합니다. 호출자는 자신이 넘긴 인자가 잘못되었는지 확인하고 싶습니다.
원인: 잘못된 포인터·null 전달, 버퍼 크기·형식 불일치, use-after-free로 전달한 포인터.
해결 포인트: bt full로 호출 스택 전체 확인 → frame N으로 상위 프레임으로 넘어가 내가 넘긴 인자 검사 → info sharedlibrary로 라이브러리 심볼 로드 여부 확인.
시나리오 3: 재귀·대형 지역 변수로 스택 오버플로우
"재귀가 깊어지면 죽어요."
"큰 배열을 선언하면 Segmentation fault가 나요."
상황: 재귀 함수가 깊어지거나, 스택에 큰 배열을 선언한 경우 segfault가 발생합니다.
원인: 기본 스택 크기(일반적으로 8MB) 초과. 재귀 깊이 또는 대형 지역 변수가 원인.
해결 포인트: bt에서 같은 함수가 반복되면 무한 재귀, bt가 매우 깊으면 스택 사용량 과다. ulimit -s로 스택 확대 또는 동적 할당으로 전환.
시나리오 4: 특정 입력에서만 발생하는 버퍼 오버런
"일반 입력은 잘 되는데, 긴 문자열을 넣으면 죽어요."
"파일 경로가 길면 segfault가 나요."
상황: 사용자 입력·파일 경로·네트워크 패킷을 고정 크기 버퍼에 복사할 때, 경계 검사 없이 strcpy/memcpy를 사용하면 버퍼 오버런이 발생합니다.
원인: 경계 검사 누락, strcpy 대신 strncpy/sprintf 대신 snprintf 미사용.
해결 포인트: ASan으로 재실행하면 “어디서 얼마나 넘어썼는지” 정확히 보고. Valgrind로도 검출 가능.
시나리오별 해결 방향 요약
| 시나리오 | 특징 | 권장 접근 |
|---|---|---|
| 프로덕션 간헐적 크래시 | core 없음 | ulimit·coredumpctl 활성화 |
| 라이브러리 내부 크래시 | 호출 경로 추적 필요 | bt full → 상위 프레임으로 인자 검사 |
| 스택 오버플로우 | 재귀·대형 지역 변수 | bt 패턴 확인 → ulimit -s 또는 동적 할당 |
| 버퍼 오버런 | 특정 입력에서만 | ASan·Valgrind로 재현 |
Segfault 디버깅 흐름
flowchart TB
subgraph Detect["Segfault 발생"]
A[Segmentation fault] --> B{Core 파일 있음?}
end
subgraph NoCore["Core 없음"]
B -->|아니오| C[ulimit -c unlimited]
C --> D[kernel.core_pattern 확인]
D --> E[재현·재실행]
end
subgraph WithCore["Core 있음"]
B -->|예| F[gdb ./program core]
F --> G[bt backtrace]
G --> H[frame 0]
H --> I[print 변수]
I --> J{원인 파악}
end
subgraph Prevent["사전 예방"]
K[ASan 빌드] --> L[테스트 실행]
L --> M[재현 시 즉시 보고]
end
E --> F
J -->|재현 가능| K
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
목차
- Core dump 활성화
- GDB/LLDB로 core 분석
- 완전한 디버깅 예제
- 자주 나오는 원인과 확인 방법
- 디버깅 전략
- 예방 코딩 패턴
- ASan으로 실행 시 잡기
- 프로덕션 패턴
- 자주 발생하는 에러와 해결법
- 자주 묻는 질문 (FAQ)
1. Core dump 활성화
ulimit
- ulimit -c 로 core 파일 크기 제한을 확인합니다. 0이면 core가 생성되지 않습니다.
- ulimit -c unlimited (쉘 세션에서)로 제한을 해제하면, 해당 터미널에서 실행한 프로그램이 segfault 시 core를 남깁니다.
- 영구 적용하려면 /etc/security/limits.conf 또는 사용자 셸 설정(.bashrc 등)에 ulimit -c unlimited를 넣을 수 있습니다.
# 현재 core 제한 확인
ulimit -c
# 0이면 core가 생성되지 않음
# unlimited로 설정
ulimit -c unlimited
# .bashrc 또는 .zshrc에 추가 (영구 적용)
echo "ulimit -c unlimited" >> ~/.bashrc
Core 파일 위치·이름
- 기본적으로 프로세스의 현재 작업 디렉터리에 core 또는 core.pid 이름으로 생성됩니다.
- /proc/sys/kernel/core_pattern 에서 패턴을 확인·변경할 수 있습니다. 예: core.%e.%p (실행 파일명·PID). sysctl kernel.core_pattern 로 확인합니다.
# Linux: core 패턴 확인
cat /proc/sys/kernel/core_pattern
# 또는
sysctl kernel.core_pattern
# core 패턴 변경 (예: 실행파일명.PID.타임스탬프)
# core.%e.%p.%t → core.myapp.12345.1709876543
echo "core.%e.%p.%t" | sudo tee /proc/sys/kernel/core_pattern
systemd로 실행 중인 경우
- systemd가 관리하는 서비스는 DefaultLimitCORE=infinity 등으로 systemd 설정에서 core 제한을 풀어야 합니다. 그 다음 coredumpctl 로 core를 수집할 수 있습니다.
- coredumpctl list → coredumpctl debug <pid 또는 시간 범위> 로 GDB가 해당 core를 붙어서 열 수 있습니다.
# systemd 서비스에 core 허용
# /etc/systemd/system/myapp.service
[Service]
DefaultLimitCORE=infinity
# coredumpctl로 core 목록 확인
coredumpctl list
# 특정 core로 GDB 실행
coredumpctl debug 12345
# 또는 시간 범위로
coredumpctl debug 2026-03-20 12:00:00 2026-03-20 13:00:00
2. GDB/LLDB로 core 분석
Core 로드
your_program은 크래시가 난 실행 파일과 같은 빌드여야 하고, core는 그 실행 파일이 segfault로 떨어질 때 생성된 core 파일입니다. -g로 빌드했으면 심볼이 있어 bt 시 소스 라인과 변수명이 보이고, frame 0으로 크래시한 프레임으로 이동한 뒤 print ptr로 포인터가 0이거나 이미 해제된 주소인지 확인할 수 있습니다.
# GDB: 실행 파일 + core 파일 함께 로드
gdb ./your_program core
# LLDB (macOS): -c 옵션으로 core 지정
lldb -c core ./your_program
- your_program은 크래시 난 실행 파일과 동일한 빌드여야 합니다. 심볼이 있으면(-g 로 빌드) 소스 라인·변수명이 보입니다.
Backtrace (호출 스택)
- bt (GDB) 또는 bt (LLDB): 크래시 시점의 호출 스택을 봅니다. #0 이 실제로 잘못된 접근이 일어난 프레임입니다.
- bt full: 각 프레임의 지역 변수까지 출력합니다. 포인터 값이 0이거나 이상한 주소인지 확인할 수 있습니다.
프레임 이동·변수 확인
- frame 0 (또는 f 0): 크래시 프레임으로 이동.
- list: 해당 소스 코드 출력.
- print 변수명: 해당 프레임에서 변수 값 확인. *print ptr 로 역참조하면 (이미 크래시한 주소라면 실패할 수 있음) 역참조한 값이 null인지 확인할 수 있습니다.
자주 쓰는 명령 정리
| 목적 | GDB | LLDB |
|---|---|---|
| backtrace | bt | bt |
| backtrace + 지역변수 | bt full | bt -f |
| 프레임 이동 | frame N | frame select N |
| 변수 출력 | print x | frame variable x |
| 디스어셈블리 | disas | disassemble |
| 메모리 맵 | info proc mappings | memory region |
| 공유 라이브러리 | info sharedlibrary | image list |
3. 완전한 디버깅 예제
예제 1: Null 포인터 역참조
문제 코드:
// 복사해 붙여넣은 뒤: g++ -g -o segfault_demo segfault_demo.cpp && ./segfault_demo
#include <iostream>
int main() {
int* p = nullptr;
std::cout << *p << "\n"; // null 역참조 → Segmentation fault
return 0;
}
실행 결과:
Segmentation fault (core dumped)
GDB로 분석:
$ g++ -g -o segfault_demo segfault_demo.cpp
$ ./segfault_demo
Segmentation fault (core dumped)
$ gdb ./segfault_demo core
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
...
Core was generated by `./segfault_demo'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 main () at segfault_demo.cpp:6
6 std::cout << *p << "\n";
(gdb) bt
#0 main () at segfault_demo.cpp:6
(gdb) print p
$1 = (int *) 0x0
(gdb) list
1 #include <iostream>
2
3 int main() {
4 int* p = nullptr;
5 std::cout << *p << "\n"; // null 역참조 → Segmentation fault
6 return 0;
7 }
결론: p가 0x0(null)이므로 null 역참조. 초기화 누락 또는 실패한 반환값 무시 등이 원인일 수 있음.
예제 2: Use-after-free
문제 코드:
// g++ -g -o uaf_demo uaf_demo.cpp && ./uaf_demo
#include <iostream>
int main() {
int* p = new int(42);
delete p;
std::cout << *p << "\n"; // use-after-free → Segmentation fault
return 0;
}
GDB로 분석:
$ gdb ./uaf_demo core
(gdb) bt
#0 main () at uaf_demo.cpp:8
(gdb) print p
$1 = (int *) 0x5555555592a0
(gdb) info proc mappings
# 해당 주소가 "해제된 힙" 영역인지 확인
# 0x555555559000 0x55555557a000 0x21000 [heap]
# delete 후에는 해당 영역이 "free" 상태
ASan으로 재실행 (정확한 위치):
$ g++ -g -fsanitize=address -fno-omit-frame-pointer -o uaf_asan uaf_demo.cpp
$ ./uaf_asan
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
...
READ of size 4 at 0x602000000010 thread T0
#0 0x401234 in main uaf_demo.cpp:8
...
freed by thread T0 here:
#0 0x7f1234 in operator delete
#1 0x401220 in main uaf_demo.cpp:7
결론: ASan이 “어디서 해제되었는지”, “어디서 접근했는지” 모두 보고. delete 후 포인터를 null로 두거나, 스마트 포인터 사용 권장.
예제 3: 스택 오버플로우 (무한 재귀)
문제 코드:
// g++ -g -o stack_overflow stack_overflow.cpp && ./stack_overflow
#include <iostream>
void recurse(int n) {
int arr[1000]; // 스택에 큰 배열
(void)arr;
if (n > 0) recurse(n - 1);
}
int main() {
recurse(10000); // 깊은 재귀 → 스택 오버플로우
return 0;
}
GDB로 분석:
$ gdb ./stack_overflow core
(gdb) bt
#0 recurse (n=100) at stack_overflow.cpp:5
#1 recurse (n=101) at stack_overflow.cpp:6
#2 recurse (n=102) at stack_overflow.cpp:6
...
#999 recurse (n=1099) at stack_overflow.cpp:6
#1000 recurse (n=1100) at stack_overflow.cpp:6
#1001 main () at stack_overflow.cpp:10
결론: bt에서 같은 함수가 반복되면 무한/깊은 재귀. ulimit -s로 스택 확대 또는 동적 할당으로 전환.
예제 4: 버퍼 오버런
문제 코드:
// g++ -g -o buffer_overflow buffer_overflow.cpp && ./buffer_overflow
#include <cstring>
#include <iostream>
int main() {
char buf[8];
strcpy(buf, "123456789012345"); // 8바이트 버퍼에 15바이트 복사
std::cout << buf << "\n";
return 0;
}
ASan으로 재실행:
$ g++ -g -fsanitize=address -fno-omit-frame-pointer -o buf_asan buffer_overflow.cpp
$ ./buf_asan
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc12345678
WRITE of size 16 at 0x7ffc12345678 thread T0
#0 0x401234 in main buffer_overflow.cpp:7
결론: ASan이 “어디서 얼마나 넘어썼는지” 정확히 보고. strncpy/snprintf 사용 권장.
예제 5: 이중 해제 (Double free)
문제 코드:
// g++ -g -o double_free double_free.cpp && ./double_free
#include <iostream>
int main() {
int* p = new int(42);
delete p;
delete p; // 이중 해제 → Segmentation fault 또는 미정의 동작
return 0;
}
ASan으로 재실행:
$ g++ -g -fsanitize=address -fno-omit-frame-pointer -o df_asan double_free.cpp
$ ./df_asan
=================================================================
==12345==ERROR: AddressSanitizer: attempting double-free on 0x602000000010
...
freed by thread T0 here:
#0 0x7f1234 in operator delete
#1 0x401220 in main double_free.cpp:8
previously allocated by thread T0 here:
#0 0x7f1234 in operator new
#1 0x401210 in main double_free.cpp:6
결론: ASan이 “이전 할당 위치”, “첫 번째 해제”, “두 번째 해제 시도”를 모두 보고. delete 후 ptr = nullptr 또는 스마트 포인터 사용 권장.
예제 6: LLDB (macOS) 사용 예
macOS에서는 GDB 대신 LLDB를 사용합니다. 명령어가 약간 다릅니다.
# LLDB로 core 로드
lldb -c core ./myapp
# LLDB 내부
(lldb) bt
(lldb) bt -f # 지역 변수 포함 (GDB의 bt full)
(lldb) frame select 0
(lldb) frame variable # 현재 프레임 변수
(lldb) p ptr # print
LLDB에서 core 생성: macOS는 기본적으로 core를 ~/Library/Logs/DiagnosticReports/ 에 저장합니다. ulimit -c unlimited 후 실행하면 됩니다.
4. 자주 나오는 원인과 확인 방법
Null 포인터 역참조
- bt 에서 #0 이 포인터를 역참조하는 라인이고, print ptr 이 0x0 이면 null 역참조입니다. “왜 null이 되었는지”를 호출 경로(상위 프레임)에서 추적합니다. 초기화 누락·실패한 반환값 무시 등이 있을 수 있습니다.
확인 방법:
print ptr→0x0(null)print *ptr→ 접근 시 에러 (GDB에서 이미 크래시한 프레임이면 시도 가능)
Use-after-free / 댕글링 포인터
- 포인터가 이미 해제된 영역을 가리키고 있을 때. bt 에서 접근한 주소를 보고, info proc mappings (GDB)로 그 주소가 어떤 영역인지 확인할 수 있습니다. “해제된 힙” 영역이면 use-after-free 가능성이 큽니다. AddressSanitizer 로 재실행하면 정확히 “어디서 해제되었는지”까지 알 수 있습니다.
확인 방법:
print ptr→0x...(유효해 보이는 주소)info proc mappings→ 해당 주소가 heap 영역인데 이미 free된 상태- ASan 재실행 →
freed by thread T0 here:메시지로 해제 위치 확인
스택 오버플로우
- 재귀가 깊거나 큰 지역 변수(대형 배열 등)가 스택에 있으면 스택이 넘칩니다. bt 가 반복되는 패턴(같은 함수가 여러 번)이면 무한 재귀, bt 가 매우 깊으면 스택 사용량이 큰 것입니다. 스택 크기 확대(ulimit -s) 또는 동적 할당으로 옮기는 방식으로 해결합니다.
확인 방법:
bt→ 같은 함수가 수십~수백 번 반복ulimit -s→ 현재 스택 제한 확인 (기본 8MB)
버퍼 오버런
- 배열/버퍼 경계를 넘어 쓴 경우. ASan 이나 Valgrind 로 실행하면 “어디서 얼마나 넘어썼는지” 감지할 수 있습니다.
확인 방법:
- ASan:
stack-buffer-overflow또는heap-buffer-overflow메시지 - Valgrind:
Invalid write of size N메시지
이중 해제 (Double free)
- 같은 포인터를 두 번
delete/free한 경우. ASan이 정확히 보고합니다.
확인 방법:
- ASan:
attempting double-free메시지 - Core 분석:
info proc mappings에서 해당 주소가 이미 free된 힙 영역
잘못된 포인터 산술 (Invalid pointer arithmetic)
- 배열 경계 밖 포인터, 정렬되지 않은 포인터 접근 등.
확인 방법:
print ptr로 주소 확인print ptr - base로 오프셋 계산- ASan:
heap-buffer-overflow등
원인별 대응 요약표
| 원인 | GDB/bt 확인 | ASan 메시지 | 해결 방향 |
|---|---|---|---|
| Null 역참조 | print ptr → 0x0 | (즉시 크래시) | null 체크, 초기화 |
| Use-after-free | info proc mappings | heap-use-after-free | 스마트 포인터, null 대입 |
| 스택 오버플로우 | bt 반복 패턴 | stack-overflow | ulimit -s, 동적 할당 |
| 버퍼 오버런 | (추적 어려움) | stack/heap-buffer-overflow | strncpy, snprintf, bounds check |
| 이중 해제 | (추적 어려움) | attempting double-free | 스마트 포인터, null 대입 |
5. 디버깅 전략
전략 1: Core 우선 → 없으면 재현 환경 구축
- ulimit -c unlimited 확인
- core_pattern 확인 (core가 어디에 저장되는지)
- Core 없으면 ASan으로 재실행해 재현 시 즉시 보고
전략 2: bt → frame 0 → print 변수
- bt (또는 bt full)로 크래시 프레임 확인
- frame 0으로 이동
- print로 포인터·인자 값 확인
- null이면 상위 프레임으로 올라가 “왜 null이 되었는지” 추적
전략 3: 라이브러리 내부 크래시 시
- bt로 호출 경로 확인
- frame select N으로 호출자 프레임으로 이동
- print로 넘긴 인자(포인터·크기·형식) 검사
- info sharedlibrary로 라이브러리 심볼 로드 여부 확인 (디버그 심볼 있으면 더 정확)
전략 4: 재현 어려운 버그
- ASan 빌드로 CI/테스트에 포함
- Valgrind로 메모리 오류 검사
- 코드 리뷰로 포인터 사용·초기화 검토
- 스마트 포인터·RAII로 수동 메모리 관리 최소화
flowchart LR
subgraph Strategy["디버깅 전략"]
A[Core 있음?] --> B[GDB/bt/print]
A --> C[ASan 재실행]
B --> D[원인 특정]
C --> D
D --> E[수정]
end
디버깅 시퀀스 (Core 분석)
sequenceDiagram
participant Dev as 개발자
participant GDB as GDB/LLDB
participant Core as Core 파일
Dev->>GDB: gdb ./program core
GDB->>Core: Core 로드
Core->>GDB: 메모리 스냅샷
GDB->>Dev: (gdb) 프롬프트
Dev->>GDB: bt
GDB->>Dev: 호출 스택 (#0 = 크래시 프레임)
Dev->>GDB: frame 0
GDB->>Dev: 프레임 이동 완료
Dev->>GDB: print ptr
GDB->>Dev: ptr = 0x0 (null 등)
Dev->>GDB: list
GDB->>Dev: 소스 코드
Dev->>Dev: 원인 파악 → 수정
예방 코딩 패턴
Segfault를 예방하기 위한 코딩 습관입니다.
1. 포인터 사용 전 null 체크
// ❌ 나쁜 예
void process(int* p) {
*p = 42; // p가 null이면 segfault
}
// ✅ 좋은 예
void process(int* p) {
if (!p) return; // 또는 assert(p)
*p = 42;
}
2. delete 후 null 대입
// ❌ 나쁜 예
delete p;
// ... 나중에 ...
*p = 10; // use-after-free
// ✅ 좋은 예
delete p;
p = nullptr;
3. 스마트 포인터 사용
// ✅ std::unique_ptr: 소유권 단일
std::unique_ptr<int> p = std::make_unique<int>(42);
// ✅ std::shared_ptr: 공유 소유권
std::shared_ptr<Widget> w = std::make_shared<Widget>();
4. 경계 검사 버전 사용
// ❌ 나쁜 예
char buf[64];
strcpy(buf, user_input); // buffer overflow
// ✅ 좋은 예
char buf[64];
strncpy(buf, user_input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// 또는 C++11+
std::string buf;
buf.assign(user_input, 0, 63);
5. RAII로 리소스 관리
// ✅ 파일, 락, 메모리 등 RAII로 자동 해제
void process() {
std::ifstream f("data.txt");
std::lock_guard<std::mutex> lock(mtx);
std::vector<int> data = loadData();
// f, lock, data 자동 해제
}
6. ASan으로 실행 시 잡기
- 컴파일: -fsanitize=address -g, 링크에도 -fsanitize=address.
- 실행 시 같은 버그가 나면 ASan 이 “할당된 위치”, “해제된 위치”, “접근한 스택” 등을 로그로 출력합니다. core 없이도 재현만 되면 원인을 좁히기 쉽습니다.
- 자세한 사용법은 AddressSanitizer·ThreadSanitizer 참고.
# g++ 예시
g++ -g -fsanitize=address -fno-omit-frame-pointer -o myapp myapp.cpp
# CMake 예시
cmake -DCMAKE_BUILD_TYPE=Debug ..
# CMakeLists.txt에 추가:
# add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
# add_link_options(-fsanitize=address)
# CMakeLists.txt: 테스트 타겟에만 ASan 적용
add_executable(myapp_test test.cpp)
target_compile_options(myapp_test PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(myapp_test PRIVATE -fsanitize=address)
7. 프로덕션 패턴
패턴 1: Core 수집 자동화
systemd 서비스에서 core 허용:
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/local/bin/myapp
DefaultLimitCORE=infinity
LimitCORE=infinity
Docker에서 core 수집:
# Dockerfile
RUN echo "ulimit -c unlimited" >> /etc/profile
# 또는 core_pattern을 볼륨으로 마운트
패턴 2: Core 분석 스크립트
#!/bin/bash
# analyze_core.sh: core 파일 + 실행 파일로 자동 backtrace
PROGRAM=$1
CORE=$2
if [ -z "$PROGRAM" ] || [ -z "$CORE" ]; then
echo "Usage: $0 <program> <core>"
exit 1
fi
gdb -batch -ex "bt full" -ex "quit" "$PROGRAM" "$CORE"
패턴 3: coredumpctl로 최근 core 분석
# 최근 1시간 내 core 목록
coredumpctl list --since "1 hour ago"
# 가장 최근 core로 GDB 실행
coredumpctl debug
# core를 파일로 저장
coredumpctl dump -o /tmp/core.dump
패턴 4: CI에서 ASan 빌드
# GitHub Actions 예시
- name: Build with ASan
run: |
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
..
make
- name: Run tests
run: ctest --output-on-failure
패턴 5: 프로덕션 체크리스트
| 항목 | 확인 |
|---|---|
| ulimit -c unlimited | ulimit -c 확인 |
| kernel.core_pattern | core 저장 위치 확인 |
| systemd DefaultLimitCORE | 서비스 설정 확인 |
| 디버그 심볼 보관 | -g 빌드 또는 separate debug info |
| ASan CI job | 테스트 전용 빌드에 포함 |
패턴 6: Valgrind로 메모리 오류 검사
ASan이 불가능한 환경(레거시 라이브러리 호환 문제 등)에서는 Valgrind로 메모리 오류를 검사할 수 있습니다.
# Valgrind로 메모리 오류 검사
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# 오류만 보고 (간결한 출력)
valgrind --error-exitcode=1 ./myapp
Valgrind vs ASan:
- Valgrind: 바이너리 수준 계측, ASan 미지원 환경에서 사용, 느림(10~30배)
- ASan: 컴파일 시 계측, 빠름(2~3배), Linux/macOS 권장
패턴 7: 디버그 심볼 분리 보관
프로덕션 빌드에서 바이너리 크기를 줄이면서 core 분석 시 심볼을 사용하려면 debug info를 별도 파일로 보관합니다.
# objcopy로 심볼 분리 (Linux)
objcopy --only-keep-debug myapp myapp.debug
objcopy --strip-debug myapp
objcopy --add-gnu-debuglink=myapp.debug myapp
# GDB에서 심볼 로드
# gdb ./myapp core
# (gdb) symbol-file myapp.debug
자주 발생하는 에러와 해결법
에러 1: “Core file is not in the expected format”
원인: core 파일이 해당 아키텍처/실행 파일과 맞지 않음. 32비트/64비트 불일치, 다른 머신에서 생성된 core 등.
해결법:
- core와 실행 파일이 같은 머신·같은 빌드에서 나왔는지 확인
file core로 core 아키텍처 확인file ./myapp로 실행 파일 아키텍처 확인
$ file core
core: ELF 64-bit LSB core file, x86-64
$ file ./myapp
./myapp: ELF 64-bit LSB executable, x86-64
에러 2: “No symbol table / No source file”
원인: -g 없이 빌드했거나, 실행 파일과 core가 다른 빌드에서 나옴.
해결법:
g++ -g -O0로 재빌드RelWithDebInfo또는Debug빌드 사용- core 생성 시점과 실행 파일 빌드가 동일한지 확인
에러 3: “Cannot access memory at address 0x”
원인: GDB에서 해당 주소를 읽으려 할 때 이미 매핑이 해제된 경우. core 분석 시에는 크래시한 시점의 메모리 스냅샷이므로, 해제된 영역은 읽기 실패할 수 있음.
해결법:
print ptr로 포인터 값만 확인 (역참조*ptr는 실패할 수 있음)info proc mappings로 해당 주소가 어떤 영역인지 확인
에러 4: “ulimit: core file size: cannot modify limit”
원인: systemd 등 상위 프로세스가 제한을 걸어 둠. 일반 사용자가 limits.conf를 수정할 권한이 없음.
해결법:
- systemd 서비스:
DefaultLimitCORE=infinity설정 /etc/security/limits.conf에* soft core unlimited추가 (root 권한 필요)- Docker:
--ulimit core=-1옵션 사용
자주 묻는 질문 (FAQ)
Q. Core 파일이 생성되지 않아요.
A. ulimit -c가 0이면 core가 생성되지 않습니다. ulimit -c unlimited로 설정하고, systemd 서비스라면 DefaultLimitCORE=infinity를 추가하세요. cat /proc/sys/kernel/core_pattern이 /dev/null이면 core가 버려지므로, core.%e.%p 같은 패턴으로 변경하세요.
Q. GDB에서 소스 라인이 안 보여요.
A. -g 옵션으로 빌드했는지 확인하세요. Release 빌드에서는 디버그 심볼이 제거될 수 있으므로, RelWithDebInfo 또는 Debug 빌드로 core를 재현해 보세요. 실행 파일과 core가 같은 빌드에서 나왔는지도 확인하세요.
Q. 라이브러리 내부에서 크래시하는데, 제가 넘긴 인자가 문제인가요?
A. bt로 호출 스택을 확인한 뒤, frame select N으로 호출자 프레임으로 이동해 print로 넘긴 포인터·크기·형식을 검사하세요. null이거나 잘못된 크기면 호출자 측 버그입니다. info sharedlibrary로 라이브러리 심볼이 로드되었는지 확인하면 더 정확한 분석이 가능합니다.
Q. ASan과 프로덕션 빌드를 같이 쓸 수 있나요?
A. ASan은 메모리·실행 시간 오버헤드가 있어 프로덕션에는 사용하지 않습니다. 테스트·CI 전용 빌드로 두고, 프로덕션에서는 core dump + GDB로 분석하는 방식을 권장합니다.
Q. 이 내용을 실무에서 언제 쓰나요?
A. Segmentation fault 발생 시 core dump 확보, GDB/LLDB로 원인 추적하는 단계별 디버깅 프로세스를 다룹니다. 프로덕션에서 간헐적 크래시, 라이브러리 내부 크래시, 스택 오버플로우, 버퍼 오버런 등 다양한 시나리오에서 위 본문의 예제와 전략을 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기
이 글에서 다루는 키워드 (관련 검색어)
Segmentation fault, core dumped, GDB, LLDB, core dump, 디버깅, 세그폴트, use-after-free, null pointer 등으로 검색하시면 이 글이 도움이 됩니다.
정리
- ulimit -c unlimited 로 core 생성 가능하게 함.
- Segfault 재현 후 gdb ./program core (또는 coredumpctl debug) 로 core 로드.
- bt → frame 0 → print 변수 로 크래시한 위치·원인 후보 확인.
- null 역참조·use-after-free·스택 오버플로우·버퍼 오버런 순으로 의심.
- 재현 가능하면 -fsanitize=address 로 빌드해 실행해 보면 원인 특정에 도움이 됩니다.
- 프로덕션에서는 core 수집 자동화·coredumpctl·CI ASan 을 활용.
한 줄 요약: core dump·GDB·ASan으로 Segfault 원인을 체계적으로 추적할 수 있습니다. 다음으로 CMake 링크 에러(#49-2)를 읽어보면 좋습니다.
다음 글: [에러 해결·트러블슈팅 #49-2] CMake 빌드 시 흔한 링크 에러 (LNK2019, undefined reference to) 원인과 해결책
이전 글: [실전 딥다이브 #48-3] 커스텀 메모리 할당자(Memory Pool) 제작기
관련 글
- C++ Asio 데드락 디버깅 | 비동기 콜백 실전 [#49-3]
- C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기
- C++ DB 엔진 기초 완벽 가이드 | 저장 엔진·쿼리 파서·실행기·트랜잭션 실전 [#49-1]
- C++ CMake 링크 에러 LNK2019 | 원인과 해결 [#49-2]
- C++ 쿼리 최적화 완벽 가이드 | 인덱스 선택·실행 계획·통계·비용 모델·프로덕션 패턴 [#49-3]