DWARF 도구 생태계 — split-DWARF·dwz·debuginfod·pyelftools
DWARF는 큽니다. Chrome, LLVM, Linux 커널 등 큰 코드베이스는 디버그 정보가 본 코드의 5~10배. 빌드 시간·메모리·디스크가 모두 영향을 받습니다.
이 시리즈의 마지막 장은 큰 DWARF를 다루는 도구·기법을 모았습니다. split-DWARF로 *.dwo 파일 분리, dwz로 중복 제거, 별도 debuginfo + build-id 매칭, 네트워크 자동 다운로드(debuginfod), 그리고 pyelftools로 자체 분석 도구 작성까지.
#크기 측정
$ ls -la /usr/bin/clang-17-rwxr-xr-x ... 156M /usr/bin/clang-17$ size -A /usr/bin/clang-17 | grep debug | awk '{sum+=$2} END {print sum/1024/1024" MB"}'1234.5 MBclang 바이너리 156MB 중 1.2 GB가 디버그 정보. (debuginfo 패키지에 분리 저장.)
.debug_info가 단연 가장 크고, .debug_loc/.debug_str이 그 다음, .debug_line은 비교적 작음. 큰 프로젝트의 디버그 정보가 왜 크냐는 거의 항상 .debug_info의 답이 옵니다 — 같은 STL 인스턴스화·인라인 함수가 수십 CU에 중복.
#Split-DWARF — .dwo 파일 분리
해법: DWARF를 .o 파일 안이 아닌 별도 .dwo 파일에 두기. 링크 시 본 ELF엔 skeleton만 남고, 디버거가 필요할 때 .dwo를 읽기.
#빌드
$ clang++ -g -gsplit-dwarf -c foo.cpp # foo.o + foo.dwo 생성$ clang++ -g -gsplit-dwarf foo.o -o prog # prog 안엔 skeleton # foo.dwo가 같은 경로에 남아 있음GCC도 같은 옵션 -gsplit-dwarf. -c로 컴파일할 때 .dwo가 분리되고, 링크 단계에선 건드리지 않음.
#Skeleton CU
ELF 안에 남는 건 DW_TAG_skeleton_unit (DWARF 5).
ELF .debug_info:
- DW_TAG_skeleton_unit
- ├─ DW_AT_dwo_name = “foo.dwo”
- ├─ DW_AT_addr_base = ref(.debug_addr)
- ├─ DW_AT_str_offsets_base = ref(.debug_str_offsets)
- ├─ DW_AT_loclists_base = ref(.debug_loclists)
- ├─ DW_AT_rnglists_base = ref(.debug_rnglists)
- └─ (no children — 본체는 dwo에)
본 DIE 트리는 .dwo의 .debug_info.dwo 섹션에 있습니다. ELF엔 어디서 찾는지의 포인터만.
#효과
- 링크 시간: DWARF 처리가 사라져 매우 빨라짐 (큰 프로젝트에서 50% 이상).
- 링크 메모리: 마찬가지.
- 분산 빌드:
.dwo는 분산 가능 (distcc/icecc가.dwo만 따로 전송). - 디버깅: 변화 없음 (디버거가
.dwo를 자동 찾음).
#.dwp 패키지
.dwo가 수천 개면 관리가 어려움. dwp 도구가 한 .dwp 패키지로 묶습니다.
$ llvm-dwp -o myapp.dwp *.dwo$ ls myapp myapp.dwp배포 시 .dwp 하나만 같이 두면 GDB가 자동 사용. ELF의 DW_AT_dwo_name이 foo.dwo든 myapp.dwp든 GDB가 적절히 검색.
#디버거 검색 경로
(gdb) show debug-file-directory(gdb) set debug-file-directory /opt/dwo:/opt/symbols또는 환경변수 DEBUGINFOD_URLS (다음 절).
#dwz — 중복 제거
dwz가 DIE 트리에서 중복 부분 트리를 찾아 한 번만 저장하고 나머지는 참조로.
대표 예: std::vector<int>의 instantiation이 수십 CU에서 완전히 동일. dwz가 partial CU (DW_TAG_partial_unit)로 공유 정의를 만들고 각 CU가 import (DW_TAG_imported_unit).
# 단일 파일$ dwz my_prog$ size -A my_prog | grep debug | awk '{sum+=$2} END {print sum}'# 기존의 60~80%
# 여러 바이너리 공유 (libfoo.so + libbar.so가 공통 DIE를 가질 때)$ dwz -m common.debug a.so b.so c.so# common.debug에 공유 DIE, a.so/b.so/c.so는 그걸 참조Fedora·Debian·Ubuntu의 모든 debuginfo 패키지가 dwz 처리됩니다. 따라서 공식 패키지의 DWARF는 이미 압축된 상태.
#-gline-tables-only — 줄 정보만
bt의 줄 번호만 필요하고 print var은 안 쓸 거라면 line table만 보존.
$ clang++ -gline-tables-only -O2 ....debug_info, .debug_loc, .debug_ranges가 거의 비고 .debug_line만 남음. 크기는 1/10 이하. addr2line 정상 동작.
운영 빌드의 표준 옵션. crash log에서 함수명 + 줄만 알면 충분.
#별도 debuginfo + build-id
배포 시 stripped 바이너리 + 별도 debuginfo 파일.
$ objcopy --only-keep-debug a.out a.debug$ strip --strip-debug a.out$ objcopy --add-gnu-debuglink=a.debug a.outGDB가 a.out을 열 때 a.debug를 같이 찾습니다. 또는 build-id로 자동 매칭.
$ readelf -n a.out | grep "Build ID" Build ID: 8d3a91f0e5a3...
$ ls /usr/lib/debug/.build-id/8d/3a91f0e5a3....debug배포판은 /usr/lib/debug/.build-id/<XX>/<YY...>.debug 디렉터리에 별도 패키지(libfoo-debug)로 debuginfo.
GDB 설정.
(gdb) set debug-file-directory /usr/lib/debug(gdb) set debug-file-directory /opt/my-symbols:/usr/lib/debug여러 경로를 콜론으로 구분.
#—build-id
$ ld --build-id=sha1 ... # SHA-1 (기본)$ ld --build-id=md5 ...$ ld --build-id=uuid ...$ ld --build-id=0xdeadbeef # 명시 값대부분의 빌드 시스템(CMake, Bazel, autotools)이 자동으로 --build-id를 켭니다.
#debuginfod — 네트워크 자동 다운로드
build-id로 네트워크에서 debuginfo를 가져오는 표준 서비스.
$ export DEBUGINFOD_URLS="https://debuginfod.fedoraproject.org/ https://debuginfod.elfutils.org/"
# core dump 분석 — 자동 다운로드$ gdb /usr/bin/foo /var/crash/core.foo.123[자동으로 https://debuginfod.fedoraproject.org/buildid/<id>/debuginfo 다운로드]
# 명시적으로$ debuginfod-find debuginfo /usr/bin/foo$ debuginfod-find source /usr/bin/foo /usr/src/...$ debuginfod-find executable BUILDID내부적으로 HTTP GET + Cookie/Range. 부분 다운로드도 가능 — 첫 콜스택에 일부만, print var로 깊이 들어가면 그때 더.
#캐시
$ ls ~/.cache/debuginfod_client/$ du -sh ~/.cache/debuginfod_client/기본 1 GB. 한 번 받은 debuginfo는 캐싱.
#자체 서버
# 사내 debuginfod 서버$ debuginfod -F /opt/builds -d /var/cache/debuginfod /opt/builds[/opt/builds 안의 모든 ELF 인덱스, build-id로 검색 가능]
# 클라이언트$ export DEBUGINFOD_URLS="http://debuginfod.internal.example.com:8002/"CI 빌드 산출물을 모두 debuginfod 서버에 넣으면 모든 개발자가 자동 debuginfo 매칭. 대규모 운영의 game changer.
#Compressed debug sections
DWARF 자체를 압축해 더 작게.
$ clang++ -g -gz=zlib ... # .debug_info → .zdebug_info (zlib)$ clang++ -g -gz=zstd ... # .debug_info (zstd compressed) (GCC 12+)ELF 안에 SHF_COMPRESSED 플래그 + 압축된 데이터. GDB가 자동 압축 해제. 크기 30-50% 절감, 디버깅 속도엔 영향 거의 없음.
DWARF 5에 공식 표준화. 옛 도구는 .zdebug_* 형태 (zlib 한정, GCC 확장).
#pyelftools — 자체 도구 작성
pip install pyelftools. ELF·DWARF의 Python 파서.
#기본 — DIE 트리 순회
from elftools.elf.elffile import ELFFile
def dump_dies(filename): with open(filename, 'rb') as f: elf = ELFFile(f) if not elf.has_dwarf_info(): print("no DWARF"); return
dwarf = elf.get_dwarf_info() for cu in dwarf.iter_CUs(): top = cu.get_top_DIE() print(f"CU: {top.attributes['DW_AT_name'].value.decode()}") walk(top, indent=2)
def walk(die, indent): for child in die.iter_children(): name = child.attributes.get('DW_AT_name') name_str = name.value.decode() if name else '?' print(f"{' ' * indent}{child.tag}: {name_str}") walk(child, indent + 2)#활용 1 — 코드 크기 분석
함수별 크기 상위 20개.
import sysfrom elftools.elf.elffile import ELFFile
with open(sys.argv[1], 'rb') as f: elf = ELFFile(f) dwarf = elf.get_dwarf_info() sizes = [] for cu in dwarf.iter_CUs(): for die in cu.iter_DIEs(): if die.tag != 'DW_TAG_subprogram': continue low = die.attributes.get('DW_AT_low_pc') high = die.attributes.get('DW_AT_high_pc') name = die.attributes.get('DW_AT_name') if not (low and high and name): continue
low_val = low.value high_val = high.value if high.form.startswith('DW_FORM_data'): size = high_val # offset form (DWARF 4+) else: size = high_val - low_val
sizes.append((size, name.value.decode(), low_val))
sizes.sort(reverse=True)for size, name, pc in sizes[:20]: print(f"{size:>8} bytes {name} @ {pc:#x}")#활용 2 — 미사용 함수 검출
링커가 --gc-sections로 안 제거한 호출되지 않는 함수.
# 모든 함수 이름 수집all_funcs = set()called = set()
for cu in dwarf.iter_CUs(): for die in cu.iter_DIEs(): if die.tag == 'DW_TAG_subprogram': name = die.attributes.get('DW_AT_name') if name and 'DW_AT_low_pc' in die.attributes: all_funcs.add(name.value.decode()) # 호출 사이트 — DWARF 5의 DW_TAG_call_site elif die.tag == 'DW_TAG_call_site': target = die.attributes.get('DW_AT_call_origin') if target: # 참조된 함수의 DIE 찾기 → 이름 ...
# 진입점 (main, ISR, exported)을 제외하고 호출 안 되는 함수unused = all_funcs - called - {'main', 'Reset_Handler'}DWARF 5의 DW_TAG_call_site가 모든 호출을 메타로 표시. 임베디드에서 flash 공간 감축에 활용.
#활용 3 — 매핑 분석
-fdata-sections로 변수가 자기 섹션을 가질 때, 각 변수의 크기·주소를 추출.
for cu in dwarf.iter_CUs(): for die in cu.iter_DIEs(): if die.tag != 'DW_TAG_variable': continue loc = die.attributes.get('DW_AT_location') if not loc: continue # DW_FORM_exprloc — 단일 표현식 if loc.form == 'DW_FORM_exprloc': expr = loc.value # DW_OP_addr (0x03) 이후 4 또는 8바이트가 주소 if expr[0] == 0x03: addr = int.from_bytes(expr[1:], 'little') ...#활용 4 — 콜그래프 추출
함수 → 호출하는 다른 함수 매핑.
edges = []for cu in dwarf.iter_CUs(): for die in cu.iter_DIEs(): if die.tag != 'DW_TAG_subprogram': continue caller_name = die.attributes.get('DW_AT_name') if not caller_name: continue for inner in die.iter_children(): if inner.tag in ('DW_TAG_inlined_subroutine', 'DW_TAG_call_site'): origin = inner.attributes.get('DW_AT_abstract_origin') or \ inner.attributes.get('DW_AT_call_origin') if origin: callee_die = cu.get_DIE_from_refaddr(origin.value) callee_name = callee_die.attributes.get('DW_AT_name') if callee_name: edges.append((caller_name.value.decode(), callee_name.value.decode()))
import networkx as nxG = nx.DiGraph(edges)# ...대규모 프로젝트의 모듈 의존성 분석. clang-tooling보다 가볍게.
#활용 5 — CFI 추출
for cfi in dwarf.CFI_entries(): if cfi.is_CIE(): print("CIE") else: addr = cfi.header.initial_location print(f"FDE for function @ {addr:#x}") # 디코딩된 표 decoded = cfi.get_decoded() for row in decoded.table: cfa = row['cfa'] print(f" pc={row['pc']:#x} CFA={cfa}")자체 unwinder, 콜스택 시뮬레이터 등의 출발점.
#llvm-dwarfdump의 깊은 옵션
# 모든 정보$ llvm-dwarfdump my_prog
# 특정 섹션$ llvm-dwarfdump --debug-info my_prog$ llvm-dwarfdump --debug-line my_prog$ llvm-dwarfdump --debug-loc my_prog$ llvm-dwarfdump --debug-frame my_prog$ llvm-dwarfdump --debug-aranges my_prog$ llvm-dwarfdump --debug-str my_prog
# 특정 주소$ llvm-dwarfdump --find=0x401234 my_prog$ llvm-dwarfdump --lookup=0x401234 my_prog
# 특정 함수$ llvm-dwarfdump --name='main' my_prog
# verbose (raw 바이트 + 디코딩)$ llvm-dwarfdump -v my_prog
# 통계$ llvm-dwarfdump --statistics my_prog{ "version": 6, "#functions": 12345, "#inlined functions": 8765, "#variables": 23456, "#variables - in scope": 22000, "#variables - source location": 21500, "sum_inscope_bytes": ..., ...}--statistics가 디버그 정보의 품질을 정량화 — 변수의 몇 %가 “optimized out”인지, 인라인이 얼마나 깊이 표현되는지.
#DWARF 검증 — dwarfdump —verify
$ llvm-dwarfdump --verify my_progVerifying .debug_abbrev...Verifying .debug_info Unit Header Chain...Verifying .debug_info references...Verifying .debug_line...
No errors.도구 체인 버그·옛 컴파일러의 DWARF 오류 검출. CI에서 DWARF 출력의 정상성 자동 확인에 유용.
#DWARF in different formats
같은 DWARF 데이터를 다른 컨테이너로.
- Mach-O (.dSYM) — macOS. 별도
.dSYM번들에 분리. - PDB — Windows. 별 포맷이지만 LLVM이 DWARF → PDB 변환 지원.
- WASM — WebAssembly. 표준 DWARF 사용.
# macOS — Mach-O 분석$ dwarfdump MyApp.dSYM/Contents/Resources/DWARF/MyAppDWARF의 컨테이너 중립성이 큰 장점. 같은 도구 (pyelftools 포함)가 거의 모든 환경에 동작.
#DWARF 7 — 미래
DWARF 6는 2024년 작성 중 (작은 변경). DWARF 7은 형식 인덱싱과 언어별 확장에 더 신경 쓸 것으로 예상. 다만 DWARF 5가 너무 잘 됨이라 큰 도약은 없을 듯.
#운영 권장 셋업
대규모 운영 환경의 표준.
- 빌드 옵션
-g3 -gdwarf-5 -gsplit-dwarf-fno-omit-frame-pointer--build-id=sha1
- 링크 시
- debuginfo
.dwo를.dwp하나로 묶음. - 본 ELF는 stripped.
- debuginfo
- 저장
.dwp+<build-id>.debug를 사내 debuginfod 서버에 인덱싱.
- 운영 머신
DEBUGINFOD_URLS로 사내 서버 가리킴.- core dump에 build-id 기록.
- 사고 분석
coredumpctl debug또는gdb exe core.- debuginfo 자동 다운로드.
- 인라인 trace + 변수 검사.
이 흐름이 대규모 시스템에서 디버깅 시간을 수십 시간 → 분 단위로 줄입니다.
#정리
- DWARF 크기 문제: split-DWARF (분리), dwz (중복 제거),
-gline-tables-only(line만). - Split-DWARF: 빌드 가속 + 분산 빌드 친화.
- dwz: 중복 DIE를 partial CU로 공유. 배포판 표준.
- Build-id가 바이너리 정체성 — debuginfo 매칭의 핵심.
- debuginfod로 네트워크 자동 다운로드.
- Compressed debug section (
.zdebug_*,-gz=zstd). - pyelftools가 자체 분석 도구의 표준.
llvm-dwarfdump --statistics로 DWARF 품질 정량화.--verify로 도구 체인 검증.- 대규모 운영의 셋업 = split + dwp + build-id + debuginfod.
#시리즈 정리
- Ch 1 ELF — 한 파일 두 시각 (섹션/세그먼트), 헤더, dynamic linking.
- Ch 2 DWARF 개관 — DIE/abbrev, TAG/AT/FORM 카탈로그.
- Ch 3
.debug_line— PC↔소스의 바이트코드 VM. - Ch 4
.debug_loc— 변수 위치의 스택 머신 표현. - Ch 5 CFI — 콜스택 unwinding의 표.
- Ch 6 (이 장) 도구·기법 — split/dwz/debuginfod/pyelftools.
이 시리즈로 디버거가 보는 모든 데이터를 다뤘습니다. GDB/LLDB가 왜 그렇게 동작하는지는 결국 ELF + DWARF의 답으로 귀결됩니다.
#관련 항목 (시리즈 전체)
#외부 자료
DWARF and ELF Internals · 6 of 6
- 1ELF 포맷 분해 — Section·Segment·Symbol Table 구조 추적
- 2DWARF 디버그 정보 분해 — DIE 트리와 .debug_abbrev
- 3DWARF .debug_line 분석 — Source-to-PC 매핑 바이트코드 VM
- 4DWARF .debug_loc 분석 — Variable Location Expression VM
- 5DWARF Call Frame Information — .debug_frame과 .eh_frame 분해
- 6DWARF 도구 생태계 — split-DWARF·dwz·debuginfod·pyelftools
관련 글
DWARF Call Frame Information — .debug_frame과 .eh_frame 분해
콜스택 풀기의 정체. CIE/FDE, CFA, register rule, .eh_frame_hdr 이진 탐색.
DWARF .debug_loc 분석 — Variable Location Expression VM
변수가 어디 있는지의 표현. 스택 머신 opcode, location list, DW_OP_entry_value.
DWARF .debug_line 분석 — Source-to-PC 매핑 바이트코드 VM
DWARF 줄 정보의 정체. 상태 머신, 표준/확장 opcode, file table, addr2line의 내부.