C++ Exception Basics
TL;DR
-
C++ 예외 처리는 Itanium C++ ABI의 규칙을 따른다.
-
정상 경로는 비용이 없고(=zero-cost), 예외가 던져질 때만 스택 언와인딩이 수행된다.
-
언와인딩은 DWARF 언와인드 정보(.eh_frame) 를 이용하여 스택 프레임을 한 단계씩 되돌리며,
프레임마다 personality 함수가 “catch 대상인지/정리만 할지”를 판단한다. -
예외 객체는 heap 에 만들어지며 reference count 로 생명주기를 관리한다.
-
언와인딩 과정에서 callee-saved 레지스터만 복원되고, caller-saved 레지스터는 필요 시 landing pad 코드가 재구성한다.
-
성능 병목은 동적 라이브러리 목록을 보호하는 전역 로더 락과 .eh_frame 탐색 비용이며,
언와인더 구현체(libgcc_s, LLVM libunwind 등)는 이를 캐싱 전략으로 최적화한다.
Overview
1) 예외 발생 시 전체 흐름
-
throw→ 컴파일러가 생성한 코드가 예외 객체를 heap에 할당하고__cxa_throw호출 -
__cxa_throw→ Itanium ABI의_Unwind_RaiseException호출 -
언와인더가 현재 스택 프레임 위치에서 DWARF CFI(unwind info) 를 읽는다
-
1단계(Search phase)
- 각 프레임마다 personality 함수를 호출해
“이 프레임이 이 예외를 잡을 수 있는가?” 검사
- 각 프레임마다 personality 함수를 호출해
-
2단계(Cleanup phase)
-
다시 처음부터 프레임을 순회하며
-
destructor 호출
-
catch 대상 프레임까지 스택 롤백
-
-
도착하면 landing pad로 점프
-
-
landing pad에서
__cxa_begin_catch실행 → 예외 객체 참조 획득 -
catch 블록 종료 시
__cxa_end_catch호출 → refcount 감소 및 필요 시 삭제
2) Itanium C++ ABI가 보장하는 것
-
예외 객체 메모리 레이아웃
-
__cxa_throw,__cxa_begin_catch,__cxa_end_catch,__gxx_personality_v0등의 인터페이스 -
personality 함수가 수행해야 하는 동작과 판단 규칙
-
DWARF 기반 스택 언와인딩 절차
→ 컴파일러(gcc/clang)가 달라도 예외 모델은 호환됨
3) DWARF 언와인드 정보(.eh_frame)
-
각 함수의 스택 레이아웃 변화, 프롤로그/에필로그에서 저장된 레지스터 위치 등을 기록한 metadata
-
언와인더는 이를 해석하여 “이전 프레임의 SP/IP/FP/저장 레지스터”를 복원할 수 있다
-
예외 처리 전용이며, 디버그 정보(.debug_info)와는 별개
4) personality 함수
프레임마다 호출되는 “언어별 처리 엔진” 역할.
-
C++:
__gxx_personality_v0 -
책임
-
이 프레임이 해당 예외 타입을 catch할 수 있는지 판단
-
cleanup-only 영역인지 판단
-
landing pad의 위치(IP)를 언와인더에 알려줌
-
5) 레지스터 복원 규칙
-
callee-saved 레지스터만 DWARF를 통해 반드시 복원
- (예: x86-64: RBX, RBP, R12~15 등)
-
caller-saved는 landing pad 코드가 직접 필요 시 다시 로딩
-
SP, IP(PC), FP 같은 프레임 관련 레지스터는 항상 언와인더가 복원
6) 성능 병목과 최적화
-
동적 라이브러리 목록은 로더의 전역 락(dl loader lock) 으로 보호됨
→.eh_frame위치 탐색 시 락 획득 필요 -
대규모 프로그램에서 예외 비용이 급증하는 이유
-
프레임마다 DWARF를 파싱
-
잦은 전역 락 획득
-
-
LLVM libunwind, libgcc_s 등은
-
IP → CFI 캐싱
-
함수 단위 또는 호출 지점 단위 캐싱
-
backtrace fast path 최적화
등을 사용해 성능 개선
-
Example (개념 흐름 예시)
예외 발생 → __cxa_throw
→ _Unwind_RaiseException
→ Frame N personality 검사(부적합)
→ Frame N–1 personality 검사(부적합)
→ Frame N–2 personality 검사(catch 가능 → search phase 종료)
→ cleanup phase 시작(위 프레임들 destructor 실행)
→ landing pad IP로 점프
→ __cxa_begin_catch
→ catch 블록 실행
→ __cxa_end_catch → refcount 0 → 예외 객체 delete
Takeaways
-
C++ 예외 처리는 zero-cost 모델이며, 실행 중 예외가 없을 때 비용이 사실상 없다.
-
언와인딩은 DWARF + Itanium ABI + personality 함수의 조합으로 동작한다.
-
예외 객체는 heap에 있으며 refcount로 생명주기 관리된다.
-
caller-saved / callee-saved 구분은 언와인딩 비용 최적화를 위해 중요하다.
-
동적 라이브러리 환경에서는 로더 락 경쟁이 예외 비용을 악화시킬 수 있으며,
현대 언와인더는 이를 캐싱 전략으로 완화한다. -
GCC/Clang 등 서로 다른 C++ 런타임이라도 Itanium ABI를 준수하는 한 상호 운용 가능하다.
References
CppCon 2017: Dave Watson “C++ Exceptions and Stack Unwinding” C++ Exceptions for Smaller Firmware - Khalil Estell - CppCon 2024