GDB JIT 디버깅 인터페이스: 과거부터 현재까지의 이해

The GDB JIT Interface

작성자
HackerNews
발행일
2025년 12월 30일

핵심 요약

  • 1 GDB는 JIT 컴파일된 코드의 디버깅 정보를 기본적으로 알지 못하며, 이를 위해 특별한 JIT 디버깅 인터페이스를 제공합니다.
  • 2 JIT 디버깅 인터페이스는 초기에는 인메모리 ELF/Mach-O 객체 생성을 요구했으나, 현재는 커스텀 바이너리 포맷을 읽는 새로운 인터페이스를 지원합니다.
  • 3 JIT 디버깅 구현 시 O(n^2) 성능 문제, 가비지 컬렉션에 따른 코드 포인터 안정성 유지 등 여러 기술적 난관이 존재합니다.

도입

GDB는 머신 코드를 단계별로 추적하고 디버그 정보를 활용하여 깔끔한 백트레이스를 제공하는 강력한 도구입니다. 이 디버그 정보는 Clang, GCC, rustc와 같은 컴파일러가 DWARF 형식으로 생성하여 바이너리(ELF, Mach-O 등)에 내장합니다. 그러나 JIT(Just-In-Time) 컴파일된 함수에서는 이러한 디버그 정보가 없어 GDB가 코드의 맥락을 이해하지 못하고 '??'로 표시되는 문제가 발생합니다. 본문은 이러한 GDB의 JIT 디버깅 한계와 이를 극복하기 위한 GDB JIT 인터페이스의 발전 과정을 상세히 설명합니다.

GDB는 JIT 컴파일된 코드에 대한 디버깅 지원을 위해 두 가지 주요 인터페이스를 제공합니다. 과거에는 번거로운 방식을 요구했지만, 현재는 좀 더 유연한 접근법이 가능합니다.

기존 JIT 인터페이스: 인메모리 객체 파일 생성

GDB는 런타임이 __jit_debug_register_code 함수와 전역 변수 __jit_debug_descriptor를 노출할 것으로 기대합니다. GDB는 이 함수에 내부 브레이크포인트를 설정하고, JIT 컴파일러가 코드를 컴파일할 때 이 함수를 호출하여 새로운 코드의 메타데이터를 GDB에 전달합니다. 구체적인 과정은 다음과 같습니다.

  • JIT 컴파일러가 함수를 컴파일하여 함수 이름, 실행 가능한 코드 주소, 코드 크기 등을 확보합니다.

  • 해당 함수에 대한 전체 ELF/Mach-O 객체(이름, 코드 영역, DWARF 메타데이터 등 포함)를 메모리 내에서 생성합니다.

  • 이 객체를 가리키는 jit_code_entry 연결 리스트 노드를 작성합니다.

  • 이를 __jit_debug_descriptor 연결 리스트에 연결합니다.

  • __jit_debug_register_code를 호출하여 GDB가 새로운 함수의 메타데이터를 가져갈 수 있도록 제어권을 넘깁니다.

  • 함수가 GC될 때, 연결 리스트를 수정하고 __jit_debug_register_code를 다시 호출하여 등록을 해제합니다.

이 방식은 V8과 같은 프로젝트에서 객체 파일을 만들기 위해 많은 코드를 포함해야 하는 번거로움이 있었습니다.

새로운 JIT 인터페이스: 커스텀 디버그 정보 리더

이러한 복잡성 때문에 GDB는 ELF/Mach-O+DWARF 객체 생성을 요구하지 않는 새로운 인터페이스를 도입했습니다. 이 방식은 개발자가 선택한 바이너리 포맷으로 디버그 정보를 작성하고, 해당 포맷을 읽는 리더를 구현해야 합니다. 이 리더는 GDB에서 공유 객체로 로드되며, 다음 인터페이스를 구현해야 합니다.

c GDB_DECLARE_GPL_COMPATIBLE_READER; extern struct gdb_reader_funcs *gdb_init_reader (void); struct gdb_reader_funcs { int reader_version; void *priv_data; gdb_read_debug_info *read; gdb_unwind_frame *unwind; gdb_get_frame_id *get_frame_id; gdb_destroy_reader *destroy; };

이 인터페이스를 구현하는 런타임은 아직 소수에 불과하며, 리더는 GPL 호환성을 선언해야 합니다.

Linux perf 인터페이스 적용 가능성

Linux perf map 인터페이스를 JIT 디버깅에 활용하는 방안도 고려될 수 있습니다. /tmp/perf-… 파일에서 자동으로 심볼을 가져올 수 있다면, 기본적인 디버그 정보를 ‘무료’로 얻을 수 있습니다. 이는 커스텀 디버그 리더를 재사용 가능한 형태로 만들어 perf map 파일을 파싱하도록 구현하는 방식으로 가능할 수 있습니다. 비록 파일명과 코드 영역만 처리 가능하여 DWARF나 커스텀 리더만큼 유연하지는 않지만, 부분적인 해결책으로는 유용할 수 있습니다.

기술적 과제: N-제곱 문제와 가비지 컬렉션

V8 문서에 따르면, JIT 인터페이스가 연결 리스트 기반이고 헤드 포인터만 유지하므로 O(n^2) 성능 문제가 발생할 수 있습니다. 특히 함수뿐만 아니라 트램폴린, 캐시 스텁 등 다양한 코드 객체를 등록할 때 더욱 두드러집니다. 또한, GDB는 심볼 객체 파일 내의 코드 포인터가 이동하지 않을 것을 기대하므로, 안정적인 심볼 파일 포인터와 실행 가능한 코드 포인터를 유지해야 합니다. V8은 이를 위해 moving GC를 비활성화합니다. 컴파일된 함수가 가비지 컬렉션될 경우, 해당 함수를 등록 해제해야 하며, ART(Android Runtime)는 GDB JIT 연결 리스트를 약한 참조(weakref)로 처리하여 주기적으로 죽은 코드 엔트리를 제거하는 방식을 사용합니다.

결론

GDB의 JIT 디버깅은 컴파일러가 생성하는 정적 디버그 정보에 의존하는 한계로 인해 '??' 프레임 문제를 겪었습니다. 이를 해결하기 위해 GDB는 `__jit_debug_register_code`와 `__jit_debug_descriptor`를 활용하는 기존 인터페이스를 제공했으나, 이는 인메모리 ELF/Mach-O 객체 생성이라는 복잡성을 수반했습니다. 이후 커스텀 디버그 정보 리더를 통해 더 유연한 새로운 인터페이스가 도입되었으며, Linux `perf map`을 활용하는 간소화된 접근 방식도 모색되고 있습니다. 그러나 JIT 코드의 동적인 특성상 O(n^2) 성능 문제와 가비지 컬렉션에 따른 코드 포인터 안정성 확보 등 여전히 해결해야 할 기술적 과제들이 남아있습니다. 이러한 노력들은 JIT 컴파일러 기반 시스템의 디버깅 경험을 지속적으로 개선하는 데 기여하고 있습니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!