본문으로 건너뛰기
Abseil Code Review · 76/79

absl::GetStackTrace와 Symbolize — crash 시 readable stack

· Hawk · 4분 읽기

#한 줄 요약

absl::GetStackTrace는 현재 PC 체인을 배열로 채운다. absl::Symbolize는 그 PC를 함수 이름 + 파일 + 라인으로 변환한다. 두 함수는 signal handler 안에서 안전하게 호출할 수 있도록 설계되어 crash 시 readable한 스택을 stderr/log로 남길 수 있다. LOG(FATAL)/CHECK 실패 출력의 근간이 이 둘이다.

#동기

production crash에서 가장 비싼 디버깅 비용은 스택을 사람이 읽을 수 있게 만드는 일이다. core dump가 있다면 gdb로 풀 수 있지만, 다음 상황에서는 core dump가 옵션이 아니다.

  • container 환경에서 core dump 비활성화.
  • core 크기가 수 GB라 저장·전송 불가.
  • intermittent crash라 reproduce 안 됨.

이때는 프로세스가 죽기 직전 자기 자신이 스택을 출력해야 한다. 다음 두 가지가 필요하다.

  1. PC 배열 수집 (unwind).
  2. PC → symbol 변환 (addr2line이 평소 하는 일).

absl::GetStackTrace + absl::Symbolize가 이를 async-signal-safe하게 한다.

#API와 사용법

#include "absl/debugging/stacktrace.h"
#include "absl/debugging/symbolize.h"
void DumpStack() {
constexpr int kMaxDepth = 32;
void* pcs[kMaxDepth];
int n = absl::GetStackTrace(pcs, kMaxDepth, /*skip_count=*/1);
for (int i = 0; i < n; ++i) {
char buf[1024];
if (absl::Symbolize(pcs[i], buf, sizeof(buf))) {
std::fprintf(stderr, " #%d 0x%p %s\n", i, pcs[i], buf);
} else {
std::fprintf(stderr, " #%d 0x%p ??\n", i, pcs[i]);
}
}
}

skip_count=1GetStackTrace 자신의 프레임을 건너뛴다. DumpStack 자신도 빼고 싶으면 2로.

#Symbolize 초기화

심볼라이저는 한 번 초기화가 필요하다. main 진입 직후가 보통.

int main(int argc, char** argv) {
absl::InitializeSymbolizer(argv[0]); // 실행 파일 경로 필요
// ... 이후 LOG(FATAL), Symbolize 등 사용 가능
}

argv[0]로부터 실행 파일을 열어 ELF/Mach-O 심볼 테이블을 읽는다. 한 번 초기화 후 Symbolize 호출은 alloc 없이 동작하도록 사전 buffer를 잡는다. signal handler 안전성의 핵심.

#내부 구현 — unwind

absl/debugging/internal/stacktrace_*.inc에 플랫폼별 구현이 있다. 주요 백엔드는 다음과 같다.

  • stacktrace_libunwind-inl.inc — libunwind 기반 (Linux x86_64, ARM64).
  • stacktrace_x86-inl.inc — frame pointer 기반.
  • stacktrace_powerpc-inl.inc, stacktrace_aarch64-inl.inc — 아키텍처 직접.
  • stacktrace_win32-inl.inc — Windows.

기본 동작은 frame pointer 체인 추적이다.

[rbp 위로 거슬러 올라가며]
rbp → 이전 rbp
rbp + 8 → 호출자의 PC
각 프레임에서 PC를 pcs[]에 기록.

frame pointer가 omitted된(-fomit-frame-pointer) 빌드에서는 libunwind가 DWARF .eh_frame을 사용해 좀 더 정교한 unwind를 한다. 비용은 frame pointer 방식보다 크지만 정확하다.

#내부 구현 — Symbolize

absl/debugging/symbolize_elf.inc (Linux 기준)의 흐름:

  1. PC가 속한 ELF 모듈 찾기 (dl_iterate_phdr 결과를 사전 캐싱).
  2. 모듈 내 .symtab / .dynsym에서 PC ≤ symbol_value + size인 가장 가까운 심볼.
  3. 결과 심볼 이름을 mangled 그대로 반환 (또는 demangle 옵션 적용).
// 핵심 (요약)
bool Symbolize(const void* pc, char* out, int out_size) {
uintptr_t addr = reinterpret_cast<uintptr_t>(pc);
// 1. PC가 속한 ELF 모듈 검색 (캐시)
const SymbolicationModule* mod = FindModuleForPC(addr);
if (!mod) return false;
// 2. symbol table에서 binary search
const ElfSymbol* sym = mod->FindSymbol(addr - mod->base);
if (!sym) return false;
// 3. demangle (옵션)
return Demangle(sym->name, out, out_size);
}

핵심 설계 결정은 심볼 테이블을 사전에 mmap해 두고 signal handler 안에서는 alloc/lock 없이 lookup만 한다는 점.

#crash 핸들러에서 사용

absl/debugging/failure_signal_handler.h가 통합 도구를 제공한다.

#include "absl/debugging/failure_signal_handler.h"
int main(int argc, char** argv) {
absl::InitializeSymbolizer(argv[0]);
absl::FailureSignalHandlerOptions opts;
opts.use_alternate_stack = true;
opts.alarm_on_failure_secs = 3;
absl::InstallFailureSignalHandler(opts);
// ...
}

이제 SIGSEGV/SIGABRT/SIGFPE 발생 시 자동으로 심볼화된 스택이 stderr로 출력된다. LOG(FATAL) 출력 형식과 일치.

*** SIGSEGV (@0x0) received by PID 12345 (TID 0x7f...) ...
PC: @ 0x55... main
@ 0x7f... __libc_start_main
@ 0x55... _start

#std와 boost와의 비교

항목abseilstd (C++23)boost
stack traceGetStackTrace (async-safe)std::stacktrace (not async-safe)boost::stacktrace
symbolizeSymbolize (async-safe)std::stacktrace_entry::descriptiondemangle 호출
signal handler안전미정의일부 안전
표준 포함×C++23×
backtrace lib 의존optional libunwindlibstdc++ libbacktracelibbacktrace/dbghelp

C++23 std::stacktraceasync-signal-safe를 요구하지 않는다. crash handler에서는 여전히 abseil이 적격.

#코드 리뷰 포인트

1. signal handler 안에서 alloc·lock 금지

// 회피 — std::string은 alloc, std::cout은 stream lock
void Handler(int) {
std::string s = MakeMessage();
std::cout << s;
}
// Good — async-safe만
void Handler(int) {
static char buf[256];
int n = snprintf(buf, sizeof(buf), "crash\n");
::write(STDERR_FILENO, buf, n);
}

absl::Symbolize내부 buffer를 사전 잡아둔다. handler 안에서 호출해도 lock 없음.

2. InitializeSymbolizer를 잊지 않는다

// 회피
absl::Symbolize(pc, buf, sizeof(buf)); // 호출은 되지만 결과 빈약
// Good
absl::InitializeSymbolizer(argv[0]);
absl::Symbolize(pc, buf, sizeof(buf));

초기화 없이도 함수가 동작하지만 심볼 lookup이 실패하거나 mangled 이름만 나온다.

3. 빌드 옵션 — frame pointer 유지

Terminal window
# Release 빌드에 frame pointer 유지 (스택 추적 정확도)
g++ -O2 -fno-omit-frame-pointer ...

-fomit-frame-pointer로 빌드된 release는 unwind가 부정확하다. 약간의 성능을 양보해도 디버깅 가능성을 선택하는 게 보통의 균형.

4. skip_count 정확히

int n = absl::GetStackTrace(pcs, 32, /*skip*/ 0); // GetStackTrace 자신 포함
int n = absl::GetStackTrace(pcs, 32, /*skip*/ 1); // 호출자부터

skip_count가 너무 작으면 출력 첫 줄에 absl::GetStackTrace만 보인다.

#자주 보는 안티패턴

signal handler에서 LOG(FATAL)

void Handler(int) { LOG(FATAL) << "crash"; } // 비추천

LOG는 mutex를 잡는다. signal handler 안 lock은 데드락 위험. 대신 RAW_LOG(FATAL, "crash") (Part 2-07).

Symbolize 실패 시 빈 출력

if (absl::Symbolize(pc, buf, sizeof(buf))) {
printf("%s\n", buf);
}
// else 무시 — PC라도 찍어야 분석 가능

실패해도 PC를 hex로 남기면 addr2line으로 후처리할 수 있다.

stack depth 너무 작게

void* pcs[8]; // 깊은 스택에서 부족
int n = absl::GetStackTrace(pcs, 8, 0);

Google 내부 보통 32~64. 깊이 비용은 무시할 수준.

#정리

  • GetStackTrace로 PC 배열, Symbolize로 함수 이름.
  • 둘 다 async-signal-safe로 설계 — crash handler에서 직접 호출 가능.
  • InstallFailureSignalHandler가 SIGSEGV/SIGABRT/SIGFPE 통합 처리.
  • 프로덕션 빌드는 frame pointer 유지 + 심볼 테이블 포함.
  • LOG(FATAL)/CHECK 실패 출력의 기반이다.

#다음 편

Part 16-02 — CRC32C에서 하드웨어 가속 체크섬을 본다.

#관련 항목

Abseil Code Review · 77 of 79

  1. 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
  2. 2 Abseil 개요 — Google이 std를 보완한 이유
  3. 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
  4. 4 Abseil 빌드와 의존성 — Bazel vs CMake
  5. 5 Abseil LTS vs HEAD 릴리스 모델 분석
  6. 6 Abseil Versioning과 ABI 호환성 정책
  7. 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
  8. 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
  9. 9 absl::LogSeverity — 로그 레벨 타입
  10. 10 Abseil type_traits — negation·conjunction·void_t
  11. 11 Abseil Conformance·Policy 분석
  12. 12 Abseil Memory utilities 분석
  13. 13 Abseil raw_logging — heap-free 로깅
  14. 14 Abseil thread_annotations — clang TSA 통합
  15. 15 absl::Status — exception-free error handling
  16. 16 absl::StatusOr<T> — 값 또는 에러
  17. 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
  18. 18 absl::Status payload — 구조화된 에러 컨텍스트
  19. 19 absl::Status ↔ exception 변환 패턴
  20. 20 absl::string_view — non-owning 문자열 참조
  21. 21 absl::string_view 함정 — dangling·c_str·임시 객체
  22. 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
  23. 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
  24. 24 absl::StrJoin — 컨테이너 결합과 Formatter
  25. 25 absl::StrFormat — type-safe printf·FormatSpec
  26. 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
  27. 27 Abseil Escape — CEscape·HexEscape·Base64
  28. 28 absl::flat_hash_map — Swiss Table 기반 hash map
  29. 29 absl::flat_hash_set — set 버전 Swiss Table
  30. 30 absl::node_hash_map — stable pointer가 필요할 때
  31. 31 absl::btree_map — sorted·cache-friendly B-tree
  32. 32 absl::FixedArray — 런타임 크기 stack 배열
  33. 33 absl::InlinedVector — small buffer optimization
  34. 34 Abseil Swiss Table internals — control byte·SIMD probing
  35. 35 absl::Mutex — reader-writer·fairness·deadlock 검출
  36. 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
  37. 37 absl::Notification — once-only signal
  38. 38 absl::BlockingCounter·Barrier — 다중 thread 조율
  39. 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
  40. 40 absl::Time·Duration 분석 — 단단한 type
  41. 41 absl::Time Format·Parse
  42. 42 absl::CivilTime 분석
  43. 43 absl::time_zone 분석
  44. 44 absl::Time mocking — 테스트 친화 시간
  45. 45 absl::BitGen — 모던 난수 생성기
  46. 46 Abseil Random Distributions — Uniform·Exponential
  47. 47 Abseil Mocking Random — 테스트 결정성
  48. 48 Abseil Random Seeding·Entropy
  49. 49 absl::int128·uint128 분석
  50. 50 absl::bits — popcount·countl_zero
  51. 51 absl::optional vs std::optional
  52. 52 absl::variant 분석
  53. 53 absl::span 분석
  54. 54 absl::any 분석
  55. 55 absl::compare — three-way 비교
  56. 56 Abseil utility — apply·in_place
  57. 57 Abseil AbslHashValue 분석
  58. 58 Abseil HashState chaining
  59. 59 Abseil Custom hashable 구현
  60. 60 Abseil LOG·VLOG·CHECK 분석
  61. 61 Abseil LogSink 분석
  62. 62 Abseil LogEntry·structured logging
  63. 63 Abseil Stack trace·failure_signal_handler
  64. 64 ABSL_FLAG 정의 분석
  65. 65 Abseil ParseCommandLine 동작
  66. 66 Abseil Flag introspection·validation
  67. 67 Google 스타일의 Abseil 사용 패턴
  68. 68 Abseil 자주 보는 anti-pattern
  69. 69 std → absl 마이그레이션 전략
  70. 70 absl::Cleanup — 함수 종료 시 실행 보장
  71. 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
  72. 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
  73. 73 absl::bind_front와 Overload — 함수 객체 보조 도구
  74. 74 absl::Cord — 분산 시스템용 대용량 문자열
  75. 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
  76. 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
  77. 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
  78. 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
  79. 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피