Core Dump로 크래시 원인 찾기: 생성 설정, GDB 분석, 프로덕션 수집 자동화

이 글의 핵심

코어 덤프는 크래시 순간의 메모리를 그대로 남겨서 재현이 안 되는 프로덕션 크래시를 사후에 분석하게 해 줍니다. 이 글은 ulimit과 core_pattern 설정, GDB로 백트레이스·변수·스레드를 보는 방법, 널 포인터·버퍼 오버플로·double free·use-after-free 사례를 다루고, 실제로 가장 많이 막히는 지점(Ubuntu에서 apport가 코어를 가져가 현재 디렉터리에 아무것도 없는 문제, 컨테이너에서 core_pattern이 호스트 설정을 따르는 문제, 스트립된 바이너리의 심볼 복원, 코어 파일에 남는 비밀번호·토큰)을 함께 정리합니다.

들어가며: Core Dump가 필요한 이유

프로덕션 환경에서 프로그램이 갑자기 크래시하면 어떻게 디버깅할까요? 재현이 어려운 버그, 간헐적인 Segmentation Fault, 메모리 손상 문제를 해결하려면 Core Dump가 필수입니다.

Core Dump의 가치:

  • 크래시 시점의 완전한 메모리 스냅샷

  • 모든 변수 값과 스택 트레이스

  • 재현 없이 사후 분석 가능

  • 프로덕션 환경의 실제 데이터 이 글에서 다룰 내용:

  • Core Dump 생성 설정

  • GDB로 Core Dump 분석

  • 실전 디버깅 시나리오

  • 자동화 및 모니터링


Core Dump 기본 개념

Core Dump란?

Core Dump는 프로그램이 비정상 종료(크래시)될 때 메모리의 내용을 파일로 저장한 것입니다.

flowchart TB
    Program[프로그램 실행] --> Crash["크래시 발생\nSIGSEGV, SIGABRT 등"]
    
    Crash --> Kernel[커널이 신호 수신]
    
    Kernel --> Check{Core Dump\n활성화?}
    
    Check -->|Yes| Dump["메모리 덤프 생성\ncore.12345"]
    Check -->|No| Exit[프로그램 종료]
    
    Dump --> File["Core Dump 파일\n- 스택\n- 힙\n- 레지스터\n- 변수 값"]
    
    File --> Debug[GDB로 분석]

Core Dump에 포함된 정보

1. 스택 메모리 (Stack)
   - 함수 호출 스택
   - 지역 변수
   - 함수 인자
2. 힙 메모리 (Heap)
   - 동적 할당된 메모리
   - malloc/new로 생성된 객체
3. 레지스터 상태
   - PC (Program Counter)
   - SP (Stack Pointer)
   - 범용 레지스터
4. 전역 변수
   - 정적 변수
   - 전역 객체
5. 공유 라이브러리 매핑
   - 로드된 .so 파일 목록
   - 메모리 주소 매핑

크래시를 일으키는 신호

// Core Dump를 생성하는 신호들
SIGQUIT    // Quit (Ctrl+\)
SIGILL     // Illegal Instruction
SIGABRT    // Abort (assert 실패)
SIGFPE     // Floating Point Exception (0으로 나누기)
SIGSEGV    // Segmentation Fault (잘못된 메모리 접근)
SIGBUS     // Bus Error (정렬 오류)
SIGTRAP    // Trace/Breakpoint Trap
SIGSYS     // Bad System Call

Core Dump 생성 설정

ulimit 설정

# 현재 Core Dump 크기 제한 확인
ulimit -c
# 0 → Core Dump 비활성화
# unlimited → 제한 없음
# Core Dump 활성화 (현재 세션)
ulimit -c unlimited
# 영구 설정 (모든 사용자)
echo "* soft core unlimited" | sudo tee -a /etc/security/limits.conf
echo "* hard core unlimited" | sudo tee -a /etc/security/limits.conf
# 특정 사용자만
echo "myuser soft core unlimited" | sudo tee -a /etc/security/limits.conf
# systemd 서비스의 경우
# /etc/systemd/system/myapp.service
[Service]
LimitCORE=infinity

Core Dump 저장 위치 설정

# 현재 설정 확인
cat /proc/sys/kernel/core_pattern
# 커널 기본값은 core (현재 디렉토리에 'core' 파일 생성)지만,
# 대부분의 배포판은 설치 시 이 값을 바꿔 둔다:
#   |/usr/share/apport/apport ...           (Ubuntu)
#   |/usr/lib/systemd/systemd-coredump ...  (Fedora, RHEL, Arch 등)
# 커스텀 패턴 설정
sudo sysctl -w kernel.core_pattern=/var/crash/core.%e.%p.%t
# 영구 설정
echo "kernel.core_pattern=/var/crash/core.%e.%p.%t" | sudo tee -a /etc/sysctl.conf
# 패턴 변수:
# %e - 실행 파일 이름
# %p - PID
# %t - 타임스탬프 (Unix time)
# %s - 신호 번호
# %u - UID
# %g - GID
# %h - 호스트명
# 예시: core.myapp.12345.1234567890

“Segmentation fault (core dumped)“라는데 코어 파일이 없다는 것이 가장 많이 받는 질문입니다. 메시지의 (core dumped)는 커널이 덤프를 넘겼다는 뜻이지 현재 디렉터리에 파일을 썼다는 뜻이 아닙니다. core_pattern이 |로 시작하면 코어는 표준 입력으로 그 프로그램에 전달되고, 저장 위치는 그 프로그램이 정합니다.

  • Ubuntu (apport): apport는 원래 패키지로 설치된 프로그램의 크래시 리포트(/var/crash/*.crash)를 만드는 도구라, 직접 빌드한 바이너리의 코어는 예전 버전에서는 버려지기도 했습니다. 최근 Ubuntu(22.04 이후)의 apport는 패키지 밖 프로그램의 코어를 /var/lib/apport/coredump/에 core.<실행파일>.<uid>.<boot-id>.<pid>.<시각> 형태로 남깁니다. 개발 머신에서는 apport를 끄고(sudo systemctl disable --now apport) 위의 파일 패턴이나 systemd-coredump를 쓰는 편이 덜 헷갈립니다.
  • systemd-coredump: /var/lib/systemd/coredump/에 압축해서 저장하고 coredumpctl로 조회합니다(아래).

저도 처음 Ubuntu 서버에서 크래시를 추적할 때 ulimit -c unlimited까지 해 놓고 현재 디렉터리만 뒤지다가 한참 뒤에야 core_pattern을 확인했습니다. 코어가 안 보이면 ulimit보다 core_pattern을 먼저 보는 습관이 시간을 가장 많이 아껴 줍니다.

systemd-coredump 사용 (권장)

# systemd-coredump 설치 (Ubuntu)
sudo apt install systemd-coredump
# 설정
sudo nano /etc/systemd/coredump.conf
[Coredump]
Storage=external
Compress=yes
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=10G
# Core Dump 저장 위치
# /var/lib/systemd/coredump/
# Core Dump 목록 확인
coredumpctl list
# 특정 Core Dump 정보
coredumpctl info <PID>
# Core Dump 추출
coredumpctl dump <PID> -o core.dump
# GDB로 바로 분석
coredumpctl debug <PID>

ProcessSizeMax(기본 2GB)보다 큰 프로세스의 코어는 저장되지 않거나 잘려서 저장됩니다. 메모리를 수십 GB 쓰는 서버라면 이 값과 ExternalSizeMax를 늘리고 디스크 여유를 확인하세요. 잘린 코어는 GDB가 열기는 하지만 힙 객체를 읽을 때 “Cannot access memory at address”가 나서 분석이 중간에 막힙니다.


GDB로 Core Dump 분석

기본 분석 흐름

# 1. 디버그 심볼과 함께 컴파일
g++ -g -O0 myapp.cpp -o myapp
# 2. Core Dump 생성되도록 설정
ulimit -c unlimited
# 3. 프로그램 실행 (크래시 발생)
./myapp
# Segmentation fault (core dumped)
# 4. GDB로 Core Dump 열기
gdb ./myapp core.12345
# 또는 systemd-coredump 사용
coredumpctl debug 12345

GDB 기본 명령어

# Core Dump 로드 후
# 1. 백트레이스 (스택 트레이스)
(gdb) bt
#0  0x00005555555551a9 in crash_function () at myapp.cpp:10
#1  0x00005555555551c8 in main () at myapp.cpp:15
# 상세 백트레이스
(gdb) bt full
#0  0x00005555555551a9 in crash_function () at myapp.cpp:10
        ptr = 0x0
        value = 42
#1  0x00005555555551c8 in main () at myapp.cpp:15
        argc = 1
        argv = 0x7fffffffe0a8
# 2. 특정 프레임으로 이동
(gdb) frame 0
(gdb) frame 1
# 3. 소스 코드 확인
(gdb) list
5       void crash_function() {
6           int* ptr = nullptr;
7           int value = 42;
8           
9           // Segmentation Fault!
10          *ptr = value;
11      }
# 4. 변수 값 확인
(gdb) print ptr
$1 = (int *) 0x0
(gdb) print value
$2 = 42
# 5. 모든 지역 변수
(gdb) info locals
ptr = 0x0
value = 42
# 6. 레지스터 상태
(gdb) info registers
rax            0x0                 0
rbx            0x555555557d60      93824992247136
rsp            0x7fffffffddd0      0x7fffffffddd0
rip            0x5555555551a9      0x5555555551a9 <crash_function()+15>
# 7. 메모리 내용 확인
(gdb) x/10x $rsp
0x7fffffffddd0: 0xffffddf0      0x00007fff      0x555551c8      0x00005555
# 8. 스레드 정보
(gdb) info threads
  Id   Target Id         Frame
* 1    Thread 0x7ffff7fc1740 (LWP 12345) crash_function () at myapp.cpp:10
  2    Thread 0x7ffff77c0700 (LWP 12346) 0x00007ffff7bc8e2d in poll ()
# 9. 특정 스레드로 전환
(gdb) thread 2
(gdb) bt

실전 예제 1: Null Pointer Dereference

// crash_null.cpp
#include <iostream>
void process_data(int* data) {
    std::cout << "Processing: " << *data << std::endl;  // 크래시!
}
int main() {
    int* ptr = nullptr;
    process_data(ptr);
    return 0;
}
# 컴파일 (디버그 심볼 포함)
g++ -g -O0 crash_null.cpp -o crash_null
# 실행
./crash_null
# Segmentation fault (core dumped)
# GDB 분석
gdb ./crash_null core
(gdb) bt
#0  0x00005555555551b2 in process_data (data=0x0) at crash_null.cpp:4
#1  0x00005555555551e1 in main () at crash_null.cpp:9
(gdb) frame 0
(gdb) print data
$1 = (int *) 0x0
(gdb) list
1       #include <iostream>
2
3       void process_data(int* data) {
4           std::cout << "Processing: " << *data << std::endl;  // 여기서 크래시!
5       }
# 원인: data가 nullptr
# 해결: nullptr 체크 추가

실전 예제 2: Buffer Overflow

// crash_overflow.cpp
#include <cstring>
void copy_string(const char* src) {
    char buffer[10];
    strcpy(buffer, src);  // 버퍼 오버플로우!
    
    printf("Copied: %s\n", buffer);
}
int main() {
    copy_string("This is a very long string that will overflow");
    return 0;
}
# 컴파일
g++ -g -O0 -fno-stack-protector crash_overflow.cpp -o crash_overflow
# 실행
./crash_overflow
# *** stack smashing detected ***: terminated
# Aborted (core dumped)
# GDB 분석
gdb ./crash_overflow core
(gdb) bt
#0  0x00007ffff7a42387 in raise () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007ffff7a43a78 in abort () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007ffff7a851a6 in __libc_message () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x00007ffff7b27d5c in __fortify_fail () from /lib/x86_64-linux-gnu/libc.so.6
#4  0x00007ffff7b27d20 in __stack_chk_fail () from /lib/x86_64-linux-gnu/libc.so.6
#5  0x000055555555521e in copy_string (src=0x555555556004 "This is a very long string...") at crash_overflow.cpp:8
#6  0x0000555555555239 in main () at crash_overflow.cpp:12
(gdb) frame 5
(gdb) print buffer
$1 = "This is a \000ery long string that will overflow"
# 버퍼 크기 10바이트를 초과함
(gdb) print src
$2 = 0x555555556004 "This is a very long string that will overflow"
# 해결: strncpy 사용 또는 std::string 사용

실전 디버깅 시나리오

시나리오 1: Double Free

// crash_double_free.cpp
#include <cstdlib>
#include <iostream>
int main() {
    int* ptr = new int(42);
    
    std::cout << "Value: " << *ptr << std::endl;
    
    delete ptr;
    
    // Double free!
    delete ptr;
    
    return 0;
}
# 컴파일
g++ -g -O0 crash_double_free.cpp -o crash_double_free
# 실행
./crash_double_free
# Value: 42
# free(): double free detected in tcache 2
# Aborted (core dumped)
# GDB 분석
gdb ./crash_double_free core
(gdb) bt
#0  0x00007ffff7a42387 in raise () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007ffff7a43a78 in abort () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007ffff7a8524a in __libc_message () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x00007ffff7a8c89c in malloc_printerr () from /lib/x86_64-linux-gnu/libc.so.6
#4  0x00007ffff7a8e5fc in _int_free () from /lib/x86_64-linux-gnu/libc.so.6
#5  0x0000555555555234 in main () at crash_double_free.cpp:12
(gdb) frame 5
(gdb) list
7       int* ptr = new int(42);
8       
9       std::cout << "Value: " << *ptr << std::endl;
10      
11      delete ptr;
12      
13      // Double free!
14      delete ptr;  // 여기서 크래시!
(gdb) print ptr
$1 = (int *) 0x555555559eb0  # 이미 해제된 주소
# 해결: delete 후 ptr = nullptr 설정

시나리오 2: Use After Free

// crash_use_after_free.cpp
#include <iostream>
#include <vector>
class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}
    void print() { std::cout << "Value: " << value << std::endl; }
};
int main() {
    MyClass* obj = new MyClass(42);
    
    obj->print();
    
    delete obj;
    
    // Use after free!
    obj->print();  // 크래시 또는 이상한 값
    
    return 0;
}
# AddressSanitizer로 컴파일 (권장)
g++ -g -O0 -fsanitize=address crash_use_after_free.cpp -o crash_use_after_free
# 실행
./crash_use_after_free
# Value: 42
# =================================================================
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
# READ of size 4 at 0x602000000010 thread T0
#     #0 0x555555555289 in MyClass::print() crash_use_after_free.cpp:9
#     #1 0x5555555552f8 in main crash_use_after_free.cpp:18
# Core Dump 분석 (ASan 없이 컴파일한 경우)
gdb ./crash_use_after_free core
(gdb) bt
#0  0x0000555555555289 in MyClass::print (this=0x555555559eb0) at crash_use_after_free.cpp:9
#1  0x00005555555552f8 in main () at crash_use_after_free.cpp:18
(gdb) frame 0
(gdb) print this
$1 = (MyClass * const) 0x555555559eb0
(gdb) print *this
$2 = {value = 0}  # 또는 쓰레기 값
(gdb) print this->value
$3 = 0  # 메모리가 이미 해제됨
# 해결: delete 후 obj = nullptr 설정

시나리오 3: Stack Overflow

// crash_stack_overflow.cpp
#include <iostream>
void recursive_function(int depth) {
    char buffer[10000];  // 큰 지역 변수
    
    std::cout << "Depth: " << depth << std::endl;
    
    // 무한 재귀
    recursive_function(depth + 1);
}
int main() {
    recursive_function(0);
    return 0;
}
# 컴파일
g++ -g -O0 crash_stack_overflow.cpp -o crash_stack_overflow
# 실행
./crash_stack_overflow
# Depth: 0
# Depth: 1
# ...
# Depth: 850
# Segmentation fault (core dumped)
# GDB 분석
gdb ./crash_stack_overflow core
(gdb) bt
#0  0x0000555555555189 in recursive_function (depth=851) at crash_stack_overflow.cpp:4
#1  0x00005555555551c4 in recursive_function (depth=850) at crash_stack_overflow.cpp:9
#2  0x00005555555551c4 in recursive_function (depth=849) at crash_stack_overflow.cpp:9
...
#850 0x00005555555551c4 in recursive_function (depth=1) at crash_stack_overflow.cpp:9
#851 0x00005555555551c4 in recursive_function (depth=0) at crash_stack_overflow.cpp:9
#852 0x00005555555551d9 in main () at crash_stack_overflow.cpp:13
# 스택 프레임 개수 확인
(gdb) bt | wc -l
853
# 각 프레임의 buffer 크기
(gdb) print sizeof(buffer)
$1 = 10000
# 총 스택 사용량: 853 × 10,000 ≈ 8.5MB
# 스택 크기 제한 초과
# 스택 크기 확인
ulimit -s
# 8192 (8MB)
# 해결: 재귀 깊이 제한 또는 힙 사용

시나리오 4: 멀티스레드 크래시

// crash_thread.cpp
#include <iostream>
#include <thread>
#include <vector>
int shared_counter = 0;
void increment() {
    for (int i = 0; i < 100000; i++) {
        shared_counter++;  // Race condition!
    }
}
int main() {
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 10; i++) {
        threads.emplace_back(increment);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Counter: " << shared_counter << std::endl;
    
    // 메모리 손상으로 인한 크래시 가능
    return 0;
}
# ThreadSanitizer로 컴파일
g++ -g -O0 -fsanitize=thread crash_thread.cpp -o crash_thread -pthread
# 실행
./crash_thread
# ==================
# WARNING: ThreadSanitizer: data race (pid=12345)
#   Write of size 4 at 0x555555558010 by thread T2:
#     #0 increment() crash_thread.cpp:8
#   Previous write of size 4 at 0x555555558010 by thread T1:
#     #0 increment() crash_thread.cpp:8
# Core Dump 분석 (TSan 없이)
gdb ./crash_thread core
(gdb) info threads
  Id   Target Id         Frame
* 1    Thread 0x7ffff7fc1740 (LWP 12345) 0x00007ffff7bc8e2d in poll ()
  2    Thread 0x7ffff77c0700 (LWP 12346) increment () at crash_thread.cpp:8
  3    Thread 0x7ffff6fbf700 (LWP 12347) increment () at crash_thread.cpp:8
# 각 스레드의 백트레이스 확인
(gdb) thread apply all bt
Thread 1 (Thread 0x7ffff7fc1740 (LWP 12345)):
#0  0x00007ffff7bc8e2d in poll () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x0000555555555456 in main () at crash_thread.cpp:22
Thread 2 (Thread 0x7ffff77c0700 (LWP 12346)):
#0  increment () at crash_thread.cpp:8
#1  0x00007ffff7e8e6df in execute_native_thread_routine ()
# 해결: std::mutex 사용

자동화 및 수집

Core Dump 자동 수집 스크립트

#!/bin/bash
# /usr/local/bin/core-collector.sh
CORE_DIR="/var/crash"
MAX_CORES=50
NOTIFY_EMAIL="[email protected]"
# Core Dump 정보 추출
EXECUTABLE="$1"
PID="$2"
TIMESTAMP="$3"
SIGNAL="$4"
CORE_FILE="$CORE_DIR/core.$EXECUTABLE.$PID.$TIMESTAMP"
# 디렉토리 생성
mkdir -p "$CORE_DIR"
# Core Dump 저장 (stdin으로 전달됨)
cat > "$CORE_FILE"
# 압축
gzip "$CORE_FILE"
CORE_FILE="$CORE_FILE.gz"
# 메타데이터 저장
cat > "$CORE_FILE.info" <<EOF
Executable: $EXECUTABLE
PID: $PID
Timestamp: $(date -d @$TIMESTAMP)
Signal: $SIGNAL
Hostname: $(hostname)
User: $(id -un)
Kernel: $(uname -r)
EOF
# 백트레이스 추출 (디버그 심볼 있는 경우)
if [ -f "/usr/bin/$EXECUTABLE" ]; then
    gdb -batch -ex "bt" -ex "quit" "/usr/bin/$EXECUTABLE" <(gunzip -c "$CORE_FILE") \
        > "$CORE_FILE.backtrace" 2>&1
fi
# 오래된 Core Dump 정리
find "$CORE_DIR" -name "core.*" -mtime +7 -delete
# 개수 제한
CORE_COUNT=$(find "$CORE_DIR" -name "core.*.gz" | wc -l)
if [ $CORE_COUNT -gt $MAX_CORES ]; then
    find "$CORE_DIR" -name "core.*.gz" -type f -printf '%T+ %p\n' | \
        sort | head -n $(($CORE_COUNT - $MAX_CORES)) | \
        cut -d' ' -f2- | xargs rm -f
fi
# 알림 전송
echo "Core dump collected: $CORE_FILE" | \
    mail -s "Core Dump Alert: $EXECUTABLE" "$NOTIFY_EMAIL"
# 로그
logger -t core-collector "Core dump: $EXECUTABLE (PID: $PID, Signal: $SIGNAL)"
# 스크립트 등록
sudo chmod +x /usr/local/bin/core-collector.sh
# core_pattern 설정
sudo sysctl -w kernel.core_pattern="|/usr/local/bin/core-collector.sh %e %p %t %s"

자동 분석 스크립트

#!/usr/bin/env python3
"""
Core Dump 자동 분석 도구
"""
import subprocess
import sys
import re
import json
from pathlib import Path
class CoreDumpAnalyzer:
    def __init__(self, executable, core_file):
        self.executable = executable
        self.core_file = core_file
        self.analysis = {}
    
    def analyze(self):
        """Core Dump 분석"""
        print(f"🔍 Analyzing {self.core_file}...")
        
        # 1. 백트레이스
        self.analysis['backtrace'] = self.get_backtrace()
        
        # 2. 크래시 위치
        self.analysis['crash_location'] = self.get_crash_location()
        
        # 3. 스레드 정보
        self.analysis['threads'] = self.get_threads()
        
        # 4. 레지스터 상태
        self.analysis['registers'] = self.get_registers()
        
        # 5. 메모리 맵
        self.analysis['memory_map'] = self.get_memory_map()
        
        return self.analysis
    
    def get_backtrace(self):
        """백트레이스 추출"""
        cmd = [
            'gdb', '-batch',
            '-ex', 'bt',
            '-ex', 'quit',
            self.executable,
            self.core_file
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.stdout
    
    def get_crash_location(self):
        """크래시 위치 파싱"""
        bt = self.get_backtrace()
        
        # #0 프레임 추출
        match = re.search(r'#0\s+(.+?) at (.+?):(\d+)', bt)
        if match:
            return {
                'function': match.group(1),
                'file': match.group(2),
                'line': int(match.group(3))
            }
        
        return None
    
    def get_threads(self):
        """스레드 정보"""
        cmd = [
            'gdb', '-batch',
            '-ex', 'info threads',
            '-ex', 'quit',
            self.executable,
            self.core_file
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        threads = []
        for line in result.stdout.split('\n'):
            if 'Thread' in line:
                threads.append(line.strip())
        
        return threads
    
    def get_registers(self):
        """레지스터 상태"""
        cmd = [
            'gdb', '-batch',
            '-ex', 'info registers',
            '-ex', 'quit',
            self.executable,
            self.core_file
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.stdout
    
    def get_memory_map(self):
        """메모리 맵"""
        cmd = [
            'gdb', '-batch',
            '-ex', 'info proc mappings',
            '-ex', 'quit',
            self.executable,
            self.core_file
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.stdout
    
    def generate_report(self):
        """분석 리포트 생성"""
        report = []
        report.append("=" * 70)
        report.append("CORE DUMP ANALYSIS REPORT")
        report.append("=" * 70)
        
        # 크래시 위치
        if self.analysis.get('crash_location'):
            loc = self.analysis['crash_location']
            report.append(f"\n🔴 CRASH LOCATION:")
            report.append(f"   Function: {loc['function']}")
            report.append(f"   File: {loc['file']}:{loc['line']}")
        
        # 백트레이스
        report.append(f"\n📚 BACKTRACE:")
        report.append(self.analysis['backtrace'])
        
        # 스레드
        if self.analysis.get('threads'):
            report.append(f"\n🧵 THREADS ({len(self.analysis['threads'])}):")
            for thread in self.analysis['threads']:
                report.append(f"   {thread}")
        
        return '\n'.join(report)
# 사용
if __name__ == '__main__':
    if len(sys.argv) < 3:
        print("Usage: analyze_core.py <executable> <core_file>")
        sys.exit(1)
    
    analyzer = CoreDumpAnalyzer(sys.argv[1], sys.argv[2])
    analyzer.analyze()
    
    report = analyzer.generate_report()
    print(report)
    
    # JSON으로 저장
    with open('analysis.json', 'w') as f:
        json.dump(analyzer.analysis, f, indent=2)
    
    print("\n✅ Analysis saved to analysis.json")

프로덕션 환경 설정

systemd 서비스 설정

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# Core Dump 설정
LimitCORE=infinity
LimitNOFILE=65536
# 환경 변수
Environment="NODE_ENV=production"
# 실행
ExecStart=/opt/myapp/bin/myapp
# 재시작 정책
Restart=on-failure
RestartSec=5s
# 로깅
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

Docker 컨테이너에서 Core Dump

컨테이너의 코어 덤프는 두 가지 이유로 헷갈립니다. 첫째, kernel.core_pattern은 호스트 전역 설정이라 컨테이너별로 다르게 둘 수 없고, 컨테이너 안에서 바꾸면(특권 컨테이너라면 가능하지만) 호스트 전체의 설정이 바뀝니다. 둘째, Dockerfile에서 /etc/security/limits.conf를 고쳐도 아무 효과가 없습니다. 이 파일은 PAM 로그인 세션에서만 적용되는데, 컨테이너의 프로세스는 로그인 과정을 거치지 않기 때문입니다. 코어 크기 제한은 컨테이너를 실행할 때 --ulimit으로 줍니다.

동작은 호스트의 core_pattern에 따라 달라집니다.

  • 파일 경로 패턴(예: /var/crash/core.%e.%p): 경로가 크래시한 프로세스의 파일 시스템 기준으로 해석되므로 컨테이너 안의 /var/crash에 써집니다. 그 경로를 볼륨으로 마운트해 두면 호스트에서 꺼낼 수 있습니다.
  • 파이프 패턴(systemd-coredump, apport): 코어가 호스트의 핸들러로 넘어가 호스트에 저장됩니다. 컨테이너 안에서는 아무것도 보이지 않고, 호스트에서 coredumpctl list로 찾아야 합니다.
# Dockerfile
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends gdb && rm -rf /var/lib/apt/lists/*
COPY myapp /usr/local/bin/
CMD ["myapp"]
# Docker 실행 (Core Dump 활성화)
docker run -d \
  --name myapp \
  --ulimit core=-1 \
  -v /var/crash:/var/crash \
  myapp:latest
# Core Dump 확인
docker exec myapp ls -lh /var/crash/
# Core Dump 추출
docker cp myapp:/var/crash/core.12345 ./
# 컨테이너 내에서 GDB 실행
docker exec -it myapp gdb /usr/local/bin/myapp /var/crash/core.12345

Kubernetes에서 Core Dump

Kubernetes에는 Pod 스펙으로 코어 크기 제한(RLIMIT_CORE)을 지정하는 필드가 없습니다. 인터넷에 도는 “initContainer에서 ulimit -c unlimited를 실행하는” 예제는 동작하지 않습니다. ulimit은 그 셸 프로세스와 자식에게만 적용되고, 셸이 끝나면 사라지기 때문입니다. 예제에 함께 나오는 SYS_PTRACE는 디버거가 실행 중인 다른 프로세스에 붙을 때 필요한 권한이라 코어 생성과는 관계가 없습니다.

현실적인 방법은 세 가지입니다.

  1. 노드의 컨테이너 런타임 기본값: containerd·CRI-O 설정에서 기본 rlimit을 정할 수 있고, 많은 환경에서 core 제한이 이미 unlimited입니다. 먼저 컨테이너 안에서 cat /proc/self/limits | grep core로 확인하세요.
  2. 엔트리포인트에서 soft limit 올리기: hard limit 범위 안에서라면 command: ["sh", "-c", "ulimit -c unlimited && exec /usr/local/bin/myapp"]처럼 앱을 실행하기 직전에 올릴 수 있습니다. exec으로 셸을 앱으로 교체해야 신호 처리도 정상입니다.
  3. 노드 수준 수집: core_pattern이 노드 전역이므로, 노드에 systemd-coredump나 DaemonSet으로 배포한 수집기를 두고 파이프 패턴으로 모든 컨테이너의 코어를 모읍니다. 수집기는 코어를 받은 뒤 Pod 이름·이미지 태그 같은 메타데이터를 붙여 오브젝트 스토리지로 올리는 구조가 일반적입니다.

어느 방법이든 메모리 제한에 걸려 OOMKilled된 컨테이너는 코어를 남기지 않는다는 점을 기억하세요. OOM 킬러는 SIGKILL을 보내고, SIGKILL은 코어 덤프를 만들지 않습니다. kubectl describe pod에서 OOMKilled가 보이면 코어가 아니라 메모리 사용 추이를 봐야 합니다.


고급 분석 기법

메모리 덤프 분석

# GDB에서 메모리 내용 확인
# 1. 특정 주소의 메모리 (16진수)
(gdb) x/10x 0x7fffffffddd0
0x7fffffffddd0: 0xffffddf0  0x00007fff  0x555551c8  0x00005555
0x7fffffffdde0: 0x00000000  0x00000001  0xffffe0a8  0x00007fff
# 2. 문자열로 해석
(gdb) x/s 0x555555556004
0x555555556004: "Hello, World!"
# 3. 명령어 (어셈블리)
(gdb) x/10i $rip
=> 0x5555555551a9 <crash_function+15>:  mov    %eax,(%rdx)
   0x5555555551ab <crash_function+17>:  nop
   0x5555555551ac <crash_function+18>:  pop    %rbp
# 4. 배열 내용
(gdb) print array[0]@10
$1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
# 5. 구조체 멤버
(gdb) print *my_struct
$2 = {
  id = 42,
  name = 0x555555559eb0 "Test",
  value = 3.14
}
# 6. 포인터 체인 따라가기
(gdb) print *node
$3 = {data = 10, next = 0x555555559ec0}
(gdb) print *node->next
$4 = {data = 20, next = 0x555555559ed0}
(gdb) print *node->next->next
$5 = {data = 30, next = 0x0}

조건부 분석

# 특정 조건의 스택 프레임만 출력
(gdb) bt full
# 출력이 너무 많으면
# Python 스크립트로 필터링
(gdb) python
import gdb
frame = gdb.newest_frame()
while frame:
    func = frame.name()
    if func and 'process' in func:
        print(f"Frame: {func}")
        # 지역 변수 출력
        block = frame.block()
        for symbol in block:
            if symbol.is_variable:
                print(f"  {symbol.name} = {symbol.value(frame)}")
    frame = frame.older()
end

Core Dump 비교

# 두 개의 Core Dump 비교 (재현 가능한 크래시)
#!/bin/bash
CORE1="core.12345"
CORE2="core.12346"
EXECUTABLE="./myapp"
# 백트레이스 추출
gdb -batch -ex "bt" -ex "quit" "$EXECUTABLE" "$CORE1" > bt1.txt
gdb -batch -ex "bt" -ex "quit" "$EXECUTABLE" "$CORE2" > bt2.txt
# 차이점 확인
diff -u bt1.txt bt2.txt
# 같은 위치에서 크래시하는지 확인
if diff -q bt1.txt bt2.txt > /dev/null; then
    echo "✅ Same crash location (reproducible)"
else
    echo "⚠️  Different crash locations"
fi

문제 해결

Core Dump가 생성되지 않을 때

# 1. ulimit 확인
ulimit -c
# 0이면 비활성화됨
# 활성화
ulimit -c unlimited
# 2. core_pattern 확인
cat /proc/sys/kernel/core_pattern
# |/usr/share/apport/apport %p %s %c %d %P (Ubuntu)
# core (기본값)
# 3. 디스크 공간 확인
df -h /var/crash
# 4. 권한 확인
ls -ld /var/crash
# drwxrwxrwt 2 root root 4096 ...
# 5. suid 프로그램 (보안상 Core Dump 안 됨)
ls -l myapp
# -rwsr-xr-x (s 플래그 있으면 suid)
# suid 제거
sudo chmod u-s myapp
# 6. 프로세스가 chroot 환경인 경우
# Core Dump가 chroot 내부에 생성됨
# 7. SELinux/AppArmor 확인
sudo getenforce  # SELinux
sudo aa-status   # AppArmor

Core Dump가 너무 클 때

# Core Dump 크기 제한
ulimit -c 1048576  # 1GB
# 또는 /etc/security/limits.conf
* soft core 1048576
* hard core 1048576
# 특정 메모리 영역만 덤프 (Linux)
# /proc/PID/coredump_filter
# 비트 플래그:
# bit 0: anonymous private memory
# bit 1: anonymous shared memory
# bit 2: file-backed private memory
# bit 3: file-backed shared memory
# bit 4: ELF header pages
# bit 5: private huge pages
# bit 6: shared huge pages
# 기본값은 0x33 (익명 private·shared 메모리 + ELF 헤더 + private 대형 페이지)
# 즉 파일에서 매핑한 영역(공유 라이브러리 코드 등)은 기본적으로 덤프하지 않음
cat /proc/self/coredump_filter
# 예: 익명 shared 메모리(대용량 공유 메모리 캐시 등)를 제외해 크기 줄이기
echo 0x31 > /proc/<PID>/coredump_filter

크기를 줄이려고 ulimit -c를 작은 값으로 잡으면 코어가 잘린 채로 저장되어, 힙의 뒷부분 객체를 읽을 수 없는 반쪽짜리 덤프가 됩니다. 크기를 줄여야 한다면 coredump_filter로 필요 없는 매핑을 빼거나, 대용량 버퍼 영역에 madvise(addr, len, MADV_DONTDUMP)를 걸어 제외하는 편이 분석에 쓸 수 있는 코어를 남깁니다.

코어 파일에는 비밀 정보가 남는다

코어 덤프는 프로세스 메모리 전체이므로 비밀번호, API 토큰, TLS 개인 키, 사용자 개인정보가 그대로 들어 있습니다. 코어를 공유 스토리지에 올리거나 외부 업체에 분석을 맡길 때 이 점을 놓치기 쉽습니다. 코어 저장 디렉터리는 root만 읽을 수 있게 두고, 보관 기간을 정해 지우며, 키를 다루는 메모리 영역은 MADV_DONTDUMP로 덤프에서 제외하는 것을 권합니다. 위 설정 템플릿의 fs.suid_dumpable=2는 setuid 프로그램의 코어도 남기게 하는 옵션이라, 파이프 핸들러와 root 전용 저장 위치가 갖춰진 경우에만 켜야 합니다.

심볼 파일 없을 때

# 디버그 심볼 분리 (배포 시)
# 1. 디버그 심볼과 함께 컴파일
g++ -g myapp.cpp -o myapp
# 2. 심볼 분리
objcopy --only-keep-debug myapp myapp.debug
strip --strip-debug --strip-unneeded myapp
# 3. 디버그 링크 추가
objcopy --add-gnu-debuglink=myapp.debug myapp
# 배포: myapp (작은 크기)
# 보관: myapp.debug (디버그 심볼)
# GDB 사용 시 자동으로 myapp.debug 로드
gdb ./myapp core
# 또는 수동 로드
(gdb) symbol-file myapp.debug

--add-gnu-debuglink로 연결하면 GDB는 실행 파일과 같은 폴더, .debug/ 하위 폴더, /usr/lib/debug/ 순서로 .debug 파일을 찾습니다. 여러 버전을 배포한다면 파일 이름보다 빌드 ID로 찾게 하는 편이 안전합니다. 링커가 넣는 빌드 ID(readelf -n myapp | grep "Build ID")가 같은 심볼 파일만 로드되므로, 다른 빌드의 심볼을 잘못 붙여서 엉뚱한 줄 번호를 보는 사고를 막을 수 있습니다. 심볼을 /usr/lib/debug/.build-id/ab/cdef....debug 구조로 보관하면 GDB가 자동으로 찾습니다.

시스템 라이브러리 심볼은 debuginfod로: 백트레이스가 libc.so.6의 ??로 끝나서 원인을 못 보는 경우가 많은데, 최근 Ubuntu(22.10 이후), Fedora, Debian, Arch는 debuginfod 서버를 제공합니다. DEBUGINFOD_URLS 환경 변수가 설정돼 있으면 GDB가 빌드 ID로 필요한 디버그 심볼과 소스를 자동으로 내려받습니다. 처음 GDB를 열 때 “Enable debuginfod for this session?”을 묻는다면 이 기능입니다.

export DEBUGINFOD_URLS="https://debuginfod.ubuntu.com"   # 배포판마다 주소 다름
gdb ./myapp core

사내 바이너리도 debuginfod 서버(elfutils의 debuginfod)에 심볼을 올려 두면, 개발자마다 심볼 파일을 찾아 다니지 않아도 됩니다.


실전 도구

coredumpctl (systemd)

# Core Dump 목록
coredumpctl list
# 최근 Core Dump
coredumpctl list --since today
# 특정 프로그램
coredumpctl list myapp
# 상세 정보
coredumpctl info 12345
# Core Dump 추출
coredumpctl dump 12345 -o core.dump
# GDB로 바로 분석
coredumpctl debug 12345
# 삭제
coredumpctl remove 12345

crash (커널 크래시 분석)

# 커널 크래시 덤프 분석 (kdump)
sudo apt install crash kexec-tools
# vmcore 분석
crash /usr/lib/debug/boot/vmlinux-$(uname -r) /var/crash/vmcore
# crash 명령어
crash> bt              # 백트레이스
crash> ps              # 프로세스 목록
crash> log             # 커널 로그
crash> files          # 열린 파일
crash> vm             # 가상 메모리

gcore (실행 중인 프로세스 덤프)

# 실행 중인 프로세스의 Core Dump 생성
# 프로세스를 종료하지는 않지만, 덤프하는 동안(수 초~수십 초) 프로세스가 멈춘다.
# 트래픽을 받는 서버라면 헬스 체크 실패로 재시작될 수 있으니 주의
# PID 확인
ps aux | grep myapp
# Core Dump 생성
gcore 12345
# Saved corefile core.12345
# 또는 GDB로
gdb -p 12345
(gdb) generate-core-file
(gdb) detach
(gdb) quit
# 프로세스는 계속 실행됨

고급 GDB 기법

GDB 스크립트

# analyze.gdb
set pagination off
set logging file analysis.txt
set logging on
echo \n=== BACKTRACE ===\n
bt full
echo \n=== THREADS ===\n
info threads
thread apply all bt
echo \n=== REGISTERS ===\n
info registers
echo \n=== LOCALS ===\n
info locals
echo \n=== SHARED LIBRARIES ===\n
info sharedlibrary
set logging off
quit
# 스크립트 실행
gdb -batch -x analyze.gdb ./myapp core.12345
# 결과 확인
cat analysis.txt

GDB Python API

# gdb_script.py
import gdb
class AnalyzeCommand(gdb.Command):
    """Analyze core dump"""
    
    def __init__(self):
        super(AnalyzeCommand, self).__init__("analyze", gdb.COMMAND_USER)
    
    def invoke(self, arg, from_tty):
        # 현재 프레임
        frame = gdb.selected_frame()
        
        print(f"Function: {frame.name()}")
        print(f"PC: 0x{frame.pc():x}")
        
        # 지역 변수
        block = frame.block()
        print("\nLocal variables:")
        for symbol in block:
            if symbol.is_variable:
                try:
                    value = symbol.value(frame)
                    print(f"  {symbol.name} = {value}")
                except:
                    pass
        
        # 백트레이스
        print("\nBacktrace:")
        i = 0
        f = gdb.newest_frame()
        while f:
            print(f"  #{i} {f.name()} at {f.find_sal().symtab}:{f.find_sal().line}")
            f = f.older()
            i += 1
AnalyzeCommand()
# GDB에서 Python 스크립트 로드
gdb ./myapp core.12345
(gdb) source gdb_script.py
(gdb) analyze

메모리 손상 디버깅

Heap Corruption 분석

// crash_heap_corruption.cpp
#include <iostream>
#include <cstring>
struct Node {
    int data;
    Node* next;
};
int main() {
    Node* head = new Node{10, nullptr};
    Node* second = new Node{20, nullptr};
    head->next = second;
    
    // 버퍼 오버플로우로 메모리 손상
    char* buffer = new char[10];
    strcpy(buffer, "This is a very long string");  // 오버플로우!
    
    // 나중에 크래시 (손상된 메모리 접근)
    std::cout << head->next->data << std::endl;
    
    delete[] buffer;
    delete second;
    delete head;
    
    return 0;
}
# Valgrind로 메모리 오류 감지
valgrind --leak-check=full --track-origins=yes ./crash_heap_corruption
# ==12345== Invalid write of size 1
# ==12345==    at 0x4C2FB7F: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
# ==12345==    by 0x108A2B: main (crash_heap_corruption.cpp:16)
# AddressSanitizer로 컴파일
g++ -g -O0 -fsanitize=address crash_heap_corruption.cpp -o crash_heap_corruption
./crash_heap_corruption
# =================================================================
# ==12345==ERROR: AddressSanitizer: heap-buffer-overflow
# WRITE of size 28 at 0x602000000010 thread T0
#     #0 0x7ffff7b3a1b0 in strcpy
#     #1 0x555555555289 in main crash_heap_corruption.cpp:16

GDB로 힙 상태 확인

# malloc 디버깅 (glibc)
(gdb) set environment MALLOC_CHECK_ 2
# 힙 청크 확인
(gdb) print *(mchunkptr)0x555555559eb0
# 힙 통계
(gdb) call malloc_stats()
# 메모리 맵 확인
(gdb) info proc mappings

자동화된 크래시 리포팅

Sentry 통합

// sentry_example.cpp
#include <sentry.h>
#include <signal.h>
void signal_handler(int sig) {
    // Sentry에 크래시 리포트
    sentry_capture_event(sentry_value_new_message_event(
        SENTRY_LEVEL_FATAL,
        "crash",
        "Application crashed"
    ));
    
    // Core Dump 생성
    signal(sig, SIG_DFL);
    raise(sig);
}
int main() {
    // Sentry 초기화
    sentry_options_t* options = sentry_options_new();
    sentry_options_set_dsn(options, "https://[email protected]/project");
    sentry_options_set_release(options, "[email protected]");
    sentry_init(options);
    
    // 신호 핸들러 등록
    signal(SIGSEGV, signal_handler);
    signal(SIGABRT, signal_handler);
    
    // 애플리케이션 로직
    // ...
    
    sentry_close();
    return 0;
}

Breakpad (Google)

// breakpad_example.cpp
#include "client/linux/handler/exception_handler.h"
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                         void* context,
                         bool succeeded) {
    printf("✅ Minidump created: %s\n", descriptor.path());
    
    // 크래시 정보를 서버로 전송
    // upload_crash_report(descriptor.path());
    
    return succeeded;
}
int main() {
    google_breakpad::MinidumpDescriptor descriptor("/tmp/crashes");
    google_breakpad::ExceptionHandler eh(
        descriptor,
        nullptr,
        dumpCallback,
        nullptr,
        true,
        -1
    );
    
    // 애플리케이션 로직
    int* ptr = nullptr;
    *ptr = 42;  // 크래시
    
    return 0;
}

프로덕션 베스트 프랙티스

Core Dump 수집 파이프라인

flowchart TB
    Crash[애플리케이션 크래시] --> Generate[Core Dump 생성]
    
    Generate --> Collect["수집 스크립트\n- 압축\n- 메타데이터"]
    
    Collect --> Upload["중앙 저장소 업로드\nS3, NFS 등"]
    
    Upload --> Notify["알림 전송\nSlack, Email"]
    
    Upload --> Analyze["자동 분석\n- 백트레이스\n- 크래시 위치"]
    
    Analyze --> Report["리포트 생성\nJira, GitHub Issue"]
    
    Analyze --> Dedupe["중복 제거\n같은 크래시 그룹화"]

설정 템플릿

# /etc/sysctl.d/50-coredump.conf
kernel.core_pattern=|/usr/local/bin/core-handler %e %p %t %s
kernel.core_pipe_limit=0
fs.suid_dumpable=2
# /etc/security/limits.d/core.conf
* soft core unlimited
* hard core unlimited
# /usr/local/bin/core-handler
#!/bin/bash
EXECUTABLE="$1"
PID="$2"
TIMESTAMP="$3"
SIGNAL="$4"
CORE_DIR="/var/crash"
CORE_FILE="$CORE_DIR/core.$EXECUTABLE.$PID.$TIMESTAMP.gz"
mkdir -p "$CORE_DIR"
gzip > "$CORE_FILE"
# S3 업로드
aws s3 cp "$CORE_FILE" "s3://my-crashes/$EXECUTABLE/"
# Slack 알림
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
  -H 'Content-Type: application/json' \
  -d "{\"text\":\"🔴 Crash: $EXECUTABLE (PID: $PID)\"}"

모니터링 대시보드

#!/usr/bin/env python3
"""
Core Dump 모니터링 대시보드
"""
import os
import time
from pathlib import Path
from collections import defaultdict
import json
class CoreDumpMonitor:
    def __init__(self, crash_dir='/var/crash'):
        self.crash_dir = Path(crash_dir)
        self.stats = defaultdict(int)
    
    def scan(self):
        """Core Dump 파일 스캔"""
        cores = list(self.crash_dir.glob('core.*'))
        
        for core in cores:
            parts = core.name.split('.')
            if len(parts) >= 2:
                executable = parts[1]
                self.stats[executable] += 1
        
        return self.stats
    
    def generate_report(self):
        """리포트 생성"""
        stats = self.scan()
        
        print("=" * 60)
        print("CORE DUMP STATISTICS")
        print("=" * 60)
        
        total = sum(stats.values())
        print(f"\nTotal crashes: {total}")
        
        print("\nBy application:")
        for app, count in sorted(stats.items(), key=lambda x: x[1], reverse=True):
            bar = '█' * min(count, 50)
            print(f"  {app:20s} {count:5d} {bar}")
        
        # JSON 저장
        with open('/var/www/html/crash-stats.json', 'w') as f:
            json.dump({
                'total': total,
                'by_app': dict(stats),
                'timestamp': time.time()
            }, f)
    
    def cleanup_old(self, days=7):
        """오래된 Core Dump 삭제"""
        cutoff = time.time() - (days * 86400)
        
        for core in self.crash_dir.glob('core.*'):
            if core.stat().st_mtime < cutoff:
                print(f"🗑️  Deleting old core: {core.name}")
                core.unlink()
# Cron으로 주기적 실행
# 0 * * * * /usr/local/bin/monitor-cores.py

언어별 Core Dump 생성

C/C++

#include <signal.h>
#include <unistd.h>
// 강제로 Core Dump 생성
void force_core_dump() {
    abort();  // SIGABRT 발생
    // 또는
    raise(SIGSEGV);
    // 또는
    kill(getpid(), SIGQUIT);
}
// 신호 핸들러로 정보 추가
void signal_handler(int sig) {
    fprintf(stderr, "Caught signal %d\n", sig);
    
    // 백트레이스 출력
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char** symbols = backtrace_symbols(callstack, frames);
    
    for (int i = 0; i < frames; i++) {
        fprintf(stderr, "%s\n", symbols[i]);
    }
    
    free(symbols);
    
    // Core Dump 생성
    signal(sig, SIG_DFL);
    raise(sig);
}
int main() {
    signal(SIGSEGV, signal_handler);
    signal(SIGABRT, signal_handler);
    
    // 애플리케이션 로직
    
    return 0;
}

Python

import faulthandler
import sys
# Core Dump 활성화
faulthandler.enable()
# 파일로 출력
with open('crash.log', 'w') as f:
    faulthandler.enable(file=f)
# 강제 크래시 (테스트용)
def crash():
    import ctypes
    ctypes.string_at(0)  # Segmentation Fault
try:
    crash()
except:
    pass
# 또는 신호 등록
import signal
faulthandler.register(signal.SIGUSR1)
# SIGUSR1 전송 시 백트레이스 출력
# kill -USR1 <PID>

Go

package main
import (
    "os"
    "os/signal"
    "runtime/pprof"
    "syscall"
)
func main() {
    // SIGQUIT 시 고루틴 덤프
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGQUIT)
    
    go func() {
        <-sigChan
        
        // 고루틴 스택 덤프
        pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
        
        // 힙 프로파일
        f, _ := os.Create("heap.prof")
        pprof.WriteHeapProfile(f)
        f.Close()
        
        os.Exit(1)
    }()
    
    // 애플리케이션 로직
    select {}
}
// SIGQUIT 전송
// kill -QUIT <PID>

성능 영향 최소화

Core Dump 생성 시 성능

Core Dump 생성 시간:
- 1GB 메모리: 약 1-2초
- 10GB 메모리: 약 10-20초
프로세스는 생성 중 중단됨 (응답 불가)

선택적 Core Dump

#include <sys/prctl.h>
// Core Dump 비활성화
prctl(PR_SET_DUMPABLE, 0);
// 특정 조건에서만 활성화
if (is_debug_mode) {
    prctl(PR_SET_DUMPABLE, 1);
}
// 민감한 정보 처리 전 비활성화
void process_sensitive_data() {
    prctl(PR_SET_DUMPABLE, 0);
    
    // 민감한 작업
    
    prctl(PR_SET_DUMPABLE, 1);
}

압축 및 필터링

# Core Dump 즉시 압축
kernel.core_pattern=|gzip > /var/crash/core.%e.%p.%t.gz
# 특정 프로그램만 수집
kernel.core_pattern=|/usr/local/bin/selective-core-handler %e %p
# selective-core-handler
#!/bin/bash
EXECUTABLE="$1"
PID="$2"
# 특정 프로그램만 저장
if [[ "$EXECUTABLE" == "myapp" || "$EXECUTABLE" == "critical-service" ]]; then
    cat > "/var/crash/core.$EXECUTABLE.$PID"
else
    # 무시
    cat > /dev/null
fi

GDB 명령어 치트시트

# 기본
gdb <executable> <core>     # Core Dump 로드
bt                          # 백트레이스
bt full                     # 상세 백트레이스
frame <N>                   # N번 프레임으로 이동
list                        # 소스 코드
# 변수
print <var>                 # 변수 값
print *<ptr>                # 포인터가 가리키는 값
print <array>[0]@10         # 배열 10개 요소
info locals                 # 모든 지역 변수
info args                   # 함수 인자
# 메모리
x/10x <addr>                # 메모리 (16진수)
x/10s <addr>                # 문자열
x/10i <addr>                # 명령어 (어셈블리)
# 스레드
info threads                # 스레드 목록
thread <N>                  # N번 스레드로 전환
thread apply all bt         # 모든 스레드 백트레이스
# 레지스터
info registers              # 모든 레지스터
print $rip                  # 특정 레지스터
# 기타
info sharedlibrary          # 로드된 라이브러리
info proc mappings          # 메모리 맵

자주 묻는 질문 (FAQ)

Q. -O2로 빌드한 릴리스 바이너리의 코어도 분석할 수 있나요?

A. 할 수 있지만 -g를 함께 줘야 합니다. -O2 -g는 최적화 결과를 바꾸지 않고 디버그 정보만 추가하므로, 릴리스 빌드에도 -g를 넣고 배포 전에 심볼만 분리해 보관하는 방식이 일반적입니다. 최적화 때문에 일부 변수가 <optimized out>으로 보이거나 인라인된 함수가 백트레이스에서 합쳐져 보이는 것은 감안해야 합니다. 이럴 때는 info frame과 disassemble로 레지스터 값을 따라가거나, 호출한 쪽 프레임에서 값을 확인합니다.

참고 자료


같이 보면 좋은 글