C++ Segmentation fault 원인 5가지와 디버깅 방법 | GDB로 추적하기

이 글의 핵심

Segmentation fault는 메시지 한 줄만 남기고 프로그램이 죽기 때문에 로그만으로는 원인을 찾기 어렵습니다. 운영체제의 메모리 보호가 어떻게 이 신호를 만드는지 이해하고, core 파일에서 backtrace를 읽어 문제 줄을 찾는 순서, 원인별로 스마트 포인터나 경계 검사로 재발을 막는 방법을 익힐 수 있습니다.

들어가며: “Segmentation fault (core dumped)“가 뜨면

Segmentation fault(세그멘테이션 폴트—허용되지 않은 메모리 접근 시 OS가 프로세스를 강제 종료하는 오류)는 C++ 개발자가 가장 자주 마주치는 런타임 에러 중 하나입니다. 프로그램이 접근 권한이 없는 메모리를 읽거나 쓰려고 할 때, 운영체제가 프로세스를 강제 종료하며 나타나는 메시지입니다. 컴파일은 성공했지만 실행 중에 크래시가 나므로, 에러 메시지만으로는 원인을 찾기 어렵습니다. 이 글에서 다루는 것:

  • Segmentation fault의 5가지 주요 원인 (널 포인터, 댕글링 포인터, 스택 오버플로우, 버퍼 오버런, 잘못된 캐스팅)
  • GDB로 core dump에서 원인을 구분하는 요령 (core 설정과 운영 환경 수집 절차는 Segmentation fault 디버깅 글에서 따로 다룹니다)
  • AddressSanitizer(ASan)로 실행 전에 잡는 법
  • 각 원인별 실전 예제와 해결법 요구 환경: Linux 또는 macOS에서 g++/Clang으로 빌드·실행하는 예제가 많습니다. GDB(Linux/WSL), LLDB(macOS), ASan(-fsanitize=address) 사용. Windows에서는 WSL로 동일 명령 실행하거나, Visual Studio 디버거·ASan(/fsanitize:address)로 대응 가능. Segfault의 5가지 주요 원인과 디버깅 흐름을 요약하면 아래와 같습니다.
flowchart LR
  subgraph cause[원인]
    N[널 포인터]
    D[댕글링]
    S[스택 오버플로우]
    B[버퍼 오버런]
    C[잘못된 캐스팅]
  end
  subgraph debug[대응]
    G[GDB/LLDB]
    A[ASan]
  end
  cause --> debug

Segmentation fault란?

메모리 보호 위반

운영체제는 각 프로세스에 메모리 영역을 할당하며, 그 범위를 벗어난 접근을 막습니다. 프로그램이 다음과 같은 메모리에 접근하려 하면 SIGSEGV 시그널을 보내 프로세스를 강제 종료합니다.

  • 할당되지 않은 주소 (예: NULL, 0x0)
  • 이미 해제된 메모리 (댕글링 포인터)
  • 읽기 전용 영역을 쓰려고 할 때 (예: 문자열 리터럴 수정)
  • 스택 범위를 벗어난 접근 왜 운영체제가 막나요?
    만약 프로그램이 다른 프로세스의 메모리를 마음대로 읽고 쓸 수 있다면, 보안 문제가 발생합니다. 예를 들어, 브라우저가 실수로 은행 앱의 메모리를 덮어쓰면 큰일입니다. 운영체제는 각 프로세스를 가상 메모리 공간으로 격리하며, 허용되지 않은 접근을 하드웨어 레벨(MMU, Memory Management Unit)에서 차단합니다. 이때 CPU가 페이지 폴트(Page Fault) 예외를 발생시키고, 운영체제가 SIGSEGV 시그널을 프로세스에 보냅니다.

Segmentation의 의미:
“Segmentation”은 메모리를 세그먼트(segment)로 나누는 것을 의미합니다. 옛날 컴퓨터(x86 리얼 모드)에서는 메모리를 코드 세그먼트, 데이터 세그먼트, 스택 세그먼트로 나눴습니다. 세그먼트 경계를 넘는 접근을 “Segmentation Violation” 또는 “Segmentation Fault”라고 불렀으며, 이 용어가 지금까지 남아 있습니다.

에러 메시지

Segmentation fault (core dumped)

core dumped: 크래시 시점의 메모리 상태가 core 파일로 저장되었다는 뜻입니다. 이 파일을 GDB/LLDB로 열면 어느 줄에서 크래시났는지, 변수 값이 무엇인지 확인할 수 있습니다.


원인 1: 널(NULL) 포인터 역참조

문제 코드

int* ptr = nullptr;
*ptr = 42;  // ❌ Segmentation fault

원인: nullptr(주소 0)을 역참조하려고 했습니다. 주소 0은 보호된 영역이므로 접근 시 segfault가 발생합니다. 왜 주소 0은 특별한가요?
운영체제는 주소 0 근처(보통 첫 4KB~64KB)를 의도적으로 비워 둡니다. 이렇게 하면 널 포인터 역참조를 즉시 탐지할 수 있습니다. 만약 주소 0이 유효한 메모리였다면, 널 포인터 버그가 조용히 다른 데이터를 덮어쓰는 더 심각한 문제가 될 수 있습니다. Segfault는 오히려 빨리 실패(Fail Fast)해서 버그를 찾기 쉽게 만들어 줍니다.

실전 예시

std::string* getName() {
    return nullptr;  // 조건에 따라 nullptr 반환
}
int main() {
    std::string* name = getName();
    std::cout << *name << '\n';  // ❌ name이 nullptr이면 크래시
    return 0;
}

해결법

1. 사용 전에 nullptr 체크:

std::string* name = getName();
if (name != nullptr) {  // ✅ 체크
    std::cout << *name << '\n';
}

2. 포인터 대신 참조 또는 optional 사용:

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o optional_safe optional_safe.cpp && ./optional_safe
#include <optional>
#include <string>
#include <iostream>
std::optional<std::string> getName() {
    return std::nullopt;  // 없을 때
}
int main() {
    auto name = getName();
    if (name.has_value()) {
        std::cout << *name << '\n';
    } else {
        std::cout << "(no name)\n";
    }
    return 0;
}

실행 결과: (no name) 이 한 줄 출력됩니다.


원인 2: 댕글링 포인터 (Use-After-Free)

문제 코드

int* createArray() {
    int arr[5] = {1, 2, 3, 4, 5};
    return arr;  // ❌ 지역 배열의 주소 반환
}
int main() {
    int* ptr = createArray();
    std::cout << ptr[0] << '\n';  // ❌ 이미 해제된 메모리 접근
    return 0;
}

원인: arr는 스택에 할당된 지역 변수입니다. 함수가 리턴하면 스택 프레임이 해제되어, ptr은 이미 무효화된 주소를 가리킵니다. 이를 댕글링 포인터(dangling pointer)라고 합니다. 스택 메모리의 생명주기:
함수가 호출되면 스택 프레임(Stack Frame)이 생성되고, 지역 변수가 여기에 할당됩니다. 함수가 리턴하면 스택 프레임이 팝(pop)되어 사라집니다. 이때 메모리 자체는 물리적으로 남아 있을 수 있지만, 운영체제가 이 영역을 다른 용도로 재사용할 수 있습니다. 따라서 댕글링 포인터로 접근하면:

  • 운이 좋으면: 아직 덮어쓰이지 않아서 원래 값이 보임 (버그가 숨어 있음)
  • 운이 나쁘면: 다른 함수가 스택을 덮어써서 쓰레기 값이 보임
  • 최악의 경우: 보호된 영역을 건드려서 segfault 왜 위험한가요?
    댕글링 포인터 버그는 재현이 어렵습니다. 실행할 때마다 스택 상태가 다르므로, 어떨 때는 정상 작동하고 어떨 때는 크래시가 납니다. 이런 비결정적(Non-deterministic) 버그는 디버깅이 어렵습니다.

실전 예시: delete 후 사용

int* ptr = new int(42);
delete ptr;
std::cout << *ptr << '\n';  // ❌ Use-after-free

해결법

1. 동적 할당 사용:

int* createArray() {
    int* arr = new int[5]{1, 2, 3, 4, 5};
    return arr;  // ✅ 힙 메모리는 명시적으로 delete 전까지 유효
}
int main() {
    int* ptr = createArray();
    std::cout << ptr[0] << '\n';
    delete[] ptr;  // ✅ 사용 후 해제
    return 0;
}

2. 스마트 포인터 사용 (권장):

std::unique_ptr<int[]> createArray() {
    return std::make_unique<int[]>(5);
}
int main() {
    auto ptr = createArray();
    ptr[0] = 1;
    std::cout << ptr[0] << '\n';
    // ✅ 자동 해제
    return 0;
}

3. 컨테이너 사용 (가장 권장):

std::vector<int> createArray() {
    return {1, 2, 3, 4, 5};
}
int main() {
    auto arr = createArray();
    std::cout << arr[0] << '\n';
    return 0;
}

원인 3: 스택 오버플로우

문제 코드

void recursive() {
    recursive();  // ❌ 무한 재귀
}
int main() {
    recursive();  // Segmentation fault
    return 0;
}

원인: 재귀 호출이 끝나지 않아 스택 메모리가 고갈됩니다. 스택 크기를 넘어서면 segfault가 발생합니다. 재귀 호출과 스택:
함수를 호출할 때마다 스택 프레임이 쌓입니다. 각 프레임은 지역 변수, 매개변수, 반환 주소를 저장합니다. 재귀가 깊어질수록 스택이 계속 쌓이며, 결국 스택 한계(Stack Limit)를 넘으면 segfault가 발생합니다.

예시 계산:

  • 함수 하나당 스택 프레임 크기: 약 100바이트
  • 기본 스택 크기: 8MB = 8,388,608바이트
  • 최대 재귀 깊이: 약 80,000번 실제로는 지역 변수 크기에 따라 달라집니다. 지역 변수가 크면 재귀 깊이가 훨씬 줄어듭니다.

실전 예시: 큰 배열을 스택에 할당

int main() {
    int arr[10000000];  // ❌ 스택에 40MB 할당 시도
    return 0;
}

대부분 시스템의 기본 스택 크기는 8MB 정도입니다. 이를 초과하면 segfault가 발생합니다.

해결법

1. 재귀 종료 조건 추가:

void recursive(int depth) {
    if (depth > 1000) return;  // ✅ 종료 조건
    recursive(depth + 1);
}

2. 큰 배열은 힙에 할당:

// 변수 선언 및 초기화
int main() {
    std::vector<int> arr(10000000);  // ✅ 힙 할당
    return 0;
}

또는:

// 변수 선언 및 초기화
int main() {
    int* arr = new int[10000000];
    // ...
    delete[] arr;
    return 0;
}

3. 스택 크기 늘리기 (임시 방편):

ulimit -s unlimited  # Linux/macOS

하지만 근본 원인을 고치는 것이 좋습니다.


원인 4: 버퍼 오버런 (배열 범위 초과)

문제 코드

int arr[5] = {1, 2, 3, 4, 5};
arr[10] = 100;  // ❌ 범위 초과

원인: 배열 크기는 5인데 인덱스 10에 접근하려 했습니다. C++는 범위 체크를 하지 않으므로, 다른 메모리 영역을 덮어쓰거나 보호된 영역에 접근해 segfault가 발생할 수 있습니다. 왜 C++는 범위 체크를 안 하나요?
성능 때문입니다. 배열 접근은 매우 빈번한 연산이므로, 매번 범위를 체크하면 5~10% 성능 저하가 발생합니다. C++는 “프로그래머가 알아서 조심하겠지”라는 철학으로, 범위 체크를 하지 않습니다. 대신 vector::at()처럼 명시적으로 체크하는 함수를 제공합니다. 버퍼 오버런의 위험성:
범위를 벗어난 쓰기는 인접한 변수를 덮어쓸 수 있습니다. 이게 보안 취약점으로 이어질 수 있습니다. 예를 들어, 스택에 있는 반환 주소(Return Address)를 덮어쓰면, 공격자가 임의의 코드를 실행할 수 있습니다(Buffer Overflow Attack).

실전 예시: 반복문 실수

std::vector<int> v(5);
for (int i = 0; i <= v.size(); ++i) {  // ❌ <= 때문에 i=5도 접근
    v[i] = i;  // v[5]는 범위 초과
}

해결법

1. 범위 체크하는 at() 사용:

std::vector<int> v(5);
try {
    v.at(10) = 100;  // ✅ 범위 초과 시 예외 발생
} catch (const std::out_of_range& e) {
    std::cerr << "Out of range: " << e.what() << '\n';
}

2. 반복문 조건 확인:

for (int i = 0; i < v.size(); ++i) {  // ✅ < 사용
    v[i] = i;
}

또는 range-based for:

for (auto& x : v) {
    x = 0;
}

3. AddressSanitizer로 검출 (아래 섹션 참고).


원인 5: 잘못된 타입 캐스팅

문제 코드

class Base { virtual void foo() {} };
class Derived : public Base { void bar() {} };
int main() {
    Base* b = new Base();
    Derived* d = static_cast<Derived*>(b);  // ❌ 실제로는 Base 객체
    d->bar();  // Segmentation fault 가능
    delete b;
    return 0;
}

원인: b는 실제로 Base 객체인데, Derived*로 캐스팅했습니다. bar()는 Derived에만 있으므로, 잘못된 메모리를 접근하게 됩니다. 왜 segfault가 나나요?
Derived 클래스는 Base보다 크기가 큽니다 (추가 멤버 변수·함수가 있음). Base 객체를 Derived*로 캐스팅하면, 컴파일러는 “Derived의 멤버는 이 오프셋에 있겠지”라고 가정하고 메모리를 읽습니다. 하지만 실제로는 Base 객체이므로, 존재하지 않는 메모리 영역을 읽게 됩니다.

메모리 레이아웃 예시:

Base 객체 (8바이트):
[vptr: 8바이트]
Derived 객체 (16바이트):
[vptr: 8바이트][추가 멤버: 8바이트]

Base*를 Derived*로 캐스팅하면, 컴파일러는 “오프셋 8에 추가 멤버가 있다”고 생각하지만, 실제로는 다른 변수 또는 보호된 영역입니다.

해결법

dynamic_cast 사용 (런타임 타입 체크):

Base* b = new Base();
Derived* d = dynamic_cast<Derived*>(b);
if (d != nullptr) {  // ✅ 캐스팅 실패 시 nullptr
    d->bar();
} else {
    std::cerr << "Not a Derived object\n";
}
delete b;

주의: dynamic_cast는 가상 함수가 있는 다형성 클래스에서만 작동합니다. 가상 함수가 없으면 컴파일 에러가 납니다.


GDB/LLDB로 core dump 분석하기

위 원인 중 무엇인지 코드만 보고 알기 어렵다면 크래시 순간의 상태를 봐야 합니다. g++ -g로 디버그 심볼을 넣어 빌드하고, ulimit -c unlimited로 core 생성을 허용한 뒤 다시 실행해 core 파일을 얻습니다. gdb ./your_program core로 열어 bt로 호출 스택을 보고, frame 0에서 print ptr로 크래시한 줄의 포인터 값을 확인하면 원인 후보가 좁혀집니다. 0x0이면 원인 1(널 포인터), 그럴듯한 힙 주소인데 이미 해제된 영역이면 원인 2(댕글링), 같은 함수가 수백 번 반복되는 스택이면 원인 3(스택 오버플로우)입니다.

(gdb) bt
#0  0x... in crash() at test.cpp:5
#1  0x... in main() at test.cpp:9
(gdb) print ptr
$1 = (int *) 0x0

core가 생성되지 않는 경우(systemd-coredump가 가로채는 경우, core_pattern 설정), macOS의 LLDB 명령, 운영 서버에서 core를 자동으로 모으고 디버그 심볼을 따로 보관하는 방법은 Segmentation fault 디버깅: core dump·GDB·LLDB에서 단계별로 다룹니다.


AddressSanitizer로 사전에 잡기

ASan이란?

AddressSanitizer(ASan)는 컴파일 타임에 계측 코드를 삽입해서, 실행 중에 메모리 에러를 즉시 탐지하는 도구입니다. Segfault가 나기 전에 정확한 줄 번호와 원인을 알려 줍니다.

사용법

g++ -g -fsanitize=address test.cpp -o test
./test

또는 Clang:

clang++ -g -fsanitize=address test.cpp -o test
./test

예시: 버퍼 오버런 탐지

코드:

int main() {
    int arr[5];
    arr[10] = 100;  // ❌ 범위 초과
    return 0;
}

ASan 출력:

=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
WRITE of size 4 at 0x7ffc... thread T0
    #0 0x... in main test.cpp:3
    
Address 0x7ffc... is located in stack of thread T0 at offset 60 in frame
    #0 0x... in main test.cpp:1
  This frame has 1 object(s):
    [32, 52) 'arr' (line 2) <== Memory access at offset 60 overflows this variable

해석: arr[10] 접근이 배열 범위를 벗어났음을 정확한 줄 번호(line 3)와 함께 알려 줍니다. ASan의 장점:

  • 정확한 줄 번호: GDB보다 훨씬 정확합니다.
  • 스택 트레이스: 어떤 함수 호출 경로로 에러가 발생했는지 보여 줍니다.
  • 메모리 상태: 해당 메모리가 언제 할당·해제되었는지 추적합니다. 성능 오버헤드:
    ASan은 2~3배 느려지고, 메모리 사용량도 2~3배 증가합니다. 따라서 디버그 빌드에서만 사용하며, 릴리스 빌드에서는 끄는 것이 일반적입니다. 하지만 개발 단계에서는 항상 켜 두는 것을 강력히 권장합니다. 버그를 조기에 발견할수록 수정 비용이 줄어듭니다.

CMake에서 ASan 활성화

add_executable(myapp main.cpp)
target_compile_options(myapp PRIVATE -fsanitize=address -g)
target_link_options(myapp PRIVATE -fsanitize=address)

8-1. 그 밖에 자주 만나는 원인과 헷갈리는 증상

위 다섯 가지만큼 흔하지는 않지만, 코드 리뷰에서 자주 보이는 원인이 몇 가지 더 있습니다. 공통점은 “항상 바로 죽지는 않는다” 는 것이라, 증상만 보고 원인을 추측하기 어렵습니다.

초기화하지 않은 포인터. int* ptr; *ptr = 42;에서 ptr에는 스택에 남아 있던 아무 값이나 들어 있습니다. 그 값이 우연히 매핑되지 않은 주소면 segfault가 나고, 매핑된 주소면 다른 변수를 조용히 덮어쓰고 한참 뒤 엉뚱한 곳에서 죽습니다. 디버그 빌드에서는 멀쩡하다가 릴리스 빌드에서만 죽는 전형적인 원인입니다. 선언과 동시에 초기화하는 습관(int* ptr = nullptr;)이 가장 확실하고, -Wall의 -Wuninitialized/-Wmaybe-uninitialized가 상당수를 잡아 줍니다. 참고로 ASan은 초기화되지 않은 값 읽기를 탐지하지 못하므로, 이 경우는 Valgrind memcheck나 Clang의 MemorySanitizer(-fsanitize=memory)가 필요합니다.

이중 해제. 같은 포인터를 두 번 delete하면 segfault보다 free(): double free detected 메시지와 함께 SIGABRT(Aborted) 로 끝나는 경우가 많습니다. glibc 할당자가 해제 목록을 검사하다가 스스로 프로그램을 멈추기 때문입니다. 검사를 통과하면 힙 메타데이터가 망가져 나중의 new/malloc에서 죽기도 합니다. “Aborted (core dumped)“도 메모리 버그일 수 있다는 점을 기억해 두세요. 소유권을 unique_ptr 하나에 두면 구조적으로 사라지는 버그입니다.

문자열 리터럴 수정. 문자열 리터럴은 읽기 전용 영역(.rodata)에 놓이므로 쓰려고 하면 segfault가 납니다. C++11부터는 char* s = "Hello"; 자체가 컴파일 에러라(C에서 온 코드나 -fpermissive에서만 통과) 새 코드에서는 드물지만, C 라이브러리 경계에서 const_cast<char*>로 const를 벗겨 넘긴 버퍼를 라이브러리가 수정하면 똑같이 터집니다. 수정할 문자열은 char s[] = "Hello";나 std::string으로 복사해서 넘깁니다.

플랫폼마다 이름이 다릅니다. 같은 종류의 오류를 Linux는 Segmentation fault(SIGSEGV), Windows는 Access violation(예외 코드 0xC0000005), macOS는 EXC_BAD_ACCESS로 보고합니다. 크래시 리포트나 이슈 검색을 할 때 세 이름을 같이 넣어 찾으면 결과가 훨씬 많이 나옵니다.


원인별 빠른 체크리스트

원인증상확인 방법해결법
널 포인터ptr = nullptr 후 *ptrGDB에서 print ptr → 0x0사용 전 nullptr 체크
댕글링 포인터delete 후 사용, 지역 변수 주소 반환ASan: use-after-free스마트 포인터, 컨테이너 사용
스택 오버플로우무한 재귀, 큰 배열 스택 할당bt에서 같은 함수 반복재귀 종료 조건, 힙 할당
버퍼 오버런배열 범위 초과 (arr[10])ASan: stack/heap-buffer-overflowat() 사용, 반복문 조건 확인
잘못된 캐스팅static_cast로 다운캐스트dynamic_cast 실패dynamic_cast + nullptr 체크
미초기화 포인터빌드 설정에 따라 증상이 바뀜-Wall 경고, Valgrind, MSan선언 시 nullptr로 초기화
이중 해제double free detected + AbortedASan: attempting double-freeunique_ptr로 소유권 단일화

실전 디버깅 프로세스

1단계: Core dump 확보

  • ulimit -c unlimited 설정
  • 프로그램 재실행해서 core 파일 생성 2단계: GDB/LLDB로 분석
  • gdb ./program core 실행
  • bt 명령으로 호출 스택 확인
  • 크래시 프레임에서 print 명령으로 변수 값 확인 3단계: 원인 파악
  • 포인터가 0x0이면 → 널 포인터
  • 이상한 주소(0xdeadbeef 등)면 → 댕글링 포인터
  • 같은 함수가 스택에 반복되면 → 무한 재귀 4단계: ASan으로 재현
  • -fsanitize=address로 재빌드
  • 실행하면 정확한 줄 번호와 원인 출력 5단계: 수정 후 재테스트
  • 원인을 고친 뒤, ASan으로 다시 실행해서 에러가 사라졌는지 확인 Segfault는 널·댕글링·범위 초과 등 잘못된 메모리 접근에서 나오므로, core dump·gdb·ASan으로 원인을 좁히면 됩니다. 다음으로 LNK2019 해결이나 C++ 메모리 기초를 읽어보면 좋습니다.

자주 묻는 질문 (FAQ)

Q. Segmentation fault와 Bus error의 차이는?

A: 둘 다 메모리 접근 에러지만:

  • Segmentation fault: 잘못된 주소 접근 (널 포인터, 범위 초과)
  • Bus error: 정렬되지 않은(Unaligned) 메모리 접근 (예: 4바이트 정렬이 필요한데 홀수 주소 접근) 정렬 문제로 인한 Bus error는 x86/x64에서는 거의 없고 일부 ARM·SPARC 등에서 납니다. x86에서 SIGBUS를 본다면 mmap으로 매핑한 파일이 도중에 잘려서(truncate) 파일 끝을 넘는 페이지에 접근한 경우가 더 흔합니다.

Q. Core dump 파일이 너무 큽니다. 줄일 수 있나요?

A: ulimit -c 10000(bash 기준 KB 단위, 약 10MB)으로 상한을 둘 수 있지만, 상한을 넘으면 core가 잘려 분석이 안 될 수 있습니다. 요즘 배포판은 core_pattern이 systemd-coredump로 파이프되어 있어 core를 압축해 저장하고(coredumpctl gdb로 바로 열 수 있음), 보관 용량은 /etc/systemd/coredump.conf에서 정합니다.

Q. ASan을 켰는데 에러가 안 나옵니다.

A: 에러가 발생하는 코드 경로가 실행되지 않았을 수 있습니다. 모든 분기를 테스트하세요. 또한 ASan은 컴파일 타임 에러는 못 잡습니다. 런타임에 실제로 실행되어야 탐지합니다.

Q. Valgrind vs AddressSanitizer, 뭘 쓰나요?

A:

  • ASan: 약 2배 느림, 재컴파일 필요. 힙뿐 아니라 스택·전역 배열 범위 초과도 잡습니다.
  • Valgrind(memcheck): 10~50배 느림, 재컴파일 불필요. ASan이 못 잡는 초기화되지 않은 값 사용을 잡지만, 스택 배열 범위 초과는 거의 못 잡습니다. 둘은 서로 보완 관계입니다. 평소 개발과 CI에는 ASan을 켜 두고, 재빌드가 어려운 바이너리나 미초기화 값이 의심될 때 Valgrind를 씁니다.

Segmentation fault는 무서워 보이지만, core dump + GDB + ASan 조합으로 체계적으로 접근하면 원인을 찾을 수 있습니다. 특히 ASan은 실행만 하면 정확한 줄 번호를 알려 주므로, 개발 단계에서 항상 켜 두는 것을 권장합니다. 프로덕션 빌드에서는 성능 오버헤드 때문에 끄지만, 디버그 빌드에서는 필수입니다.


같이 보면 좋은 글