Rust 코드 생성
디버그 정보 생성의 첫 단계에서 Rust는 프로그램의 MIR을 검사하여 이를 LLVM에 전달해야 합니다. 이 작업은 주로 rustc_codegen_llvm/debuginfo에서 이루어지며, 일부 타입 이름 처리는 rustc_codegen_ssa/debuginfo에 있습니다. Rust는 rustc_llvm에 있는, LLVM 내부를 감싼 얇은 래퍼인 DIBuilder API를 통해 LLVM과 통신합니다.
타입 정보
타입 정보는 일반적으로 타입 이름, 크기, 정렬로 구성되며, 관련이 있는 경우 필드, 제네릭 매개변수, 저장 한정자 등도 포함합니다. 이 작업의 상당 부분은 rustc_codegen_llvm/src/debuginfo/metadata에서 이루어집니다.
목표가 반드시 “타입을 Rust에 나타나는 그대로 정확하게 표현하는 것“은 아니며, 오히려 디버거가 디버깅 중에 데이터를 가장 정확하게 재구성할 수 있는 방식으로 타입을 표현하는 것이라는 점을 명심해야 합니다. 이 구분은 이 계층에서 이루어지는 핵심 작업을 이해하는 데 매우 중요합니다. 여기에서 이루어지는 많은 변경은 다른 방법이 통하지 않을 때 디버거의 한계를 우회하기 위한 것입니다.
특이 사항
Rust가 생성하는 DI 노드는 CDB와 LLDB 모두를 위해 C/C++인 “척“합니다. 이로 인해 다소 직관적이지 않고 관용적이지 않은 디버그 정보가 만들어질 수 있습니다.
포인터와 참조
와이드 포인터/참조/Box는 data_ptr와 length라는 2개의 필드를 가진 구조체로 취급됩니다.
와이드가 아닌 모든 포인터, 참조, Box 포인터는 포인터 노드로 출력되며, mut와 비mut는 구분되지 않습니다. 이를 바로잡으려는 시도가 여러 번 있었지만, 안타깝게도 간단한 해결책은 없습니다. 각 형식의 reference DI 노드를 사용하는 데에는 함정이 있습니다. C++ 참조와 Rust 참조 사이에는 조화시킬 수 없는 의미상의 차이가 있습니다.
cppreference에서 인용합니다:
참조는 객체가 아닙니다. 참조는 반드시 저장 공간을 차지하지는 않습니다. 다만 원하는 의미를 구현하는 데 필요하다면 컴파일러가 저장 공간을 할당할 수 있습니다(예: 참조 타입의 비정적 데이터 멤버는 일반적으로 메모리 주소를 저장하는 데 필요한 만큼 클래스의 크기를 늘립니다).
참조는 객체가 아니므로 참조의 배열, 참조를 가리키는 포인터, 참조에 대한 참조는 존재하지 않습니다
현재 제안된 해결책은 단순히 포인터 노드를 typedef하는 것입니다.
비mut를 나타내기 위해 const 한정자를 사용하면 LLDB의 내부 최적화 때문에 문제가 생길 수 있습니다. 요약하면, LLDB는 코드를 단계별로 실행할 때 변수의 자식 값(예: 구조체 필드, 배열 요소)을 캐시하려고 합니다. 어떤 값을 안전하게 캐시할 수 있는지 판단하는 데 휴리스틱이 사용되며, const는 그 휴리스틱의 일부입니다. 이것이 Rust의 내부 가변성 구조 등과 어떻게 상호 작용할지에 대해서는 아직 연구가 이루어지지 않았습니다.
DWARF 대 PDB
타입 정보의 대부분은 상당히 간단하지만, 주목할 만한 문제 하나는 대상의 디버그 정보 형식입니다. 각 형식은 의미와 한계가 서로 다르므로, 경우에 따라 약간씩 다른 디버그 정보가 필요합니다. 이는 cpp_like_debuginfo 호출로 분기됩니다.
이름 지정
Rust는 타입 이름을 가능한 한 정확하게 전달하려고 하지만, 디버거와 디버그 정보 형식이 항상 이를 따르는 것은 아닙니다.
MSVC 표현식 파서의 제약 때문에 PDB 디버그 정보에서는 이름이 다음과 같이 변환됩니다.
| Rust 이름 | MSVC 이름 |
|---|---|
&str/&mut str | ref$<str$>/ref_mut$<str$> |
&[T]/&mut [T] | ref$<slice$<T> >/ref_mut$<slice$<T> >1 |
[T; N] | array$<T, N> |
RustEnum | enum2$<RustEnum> |
(T1, T2) | tuple$<T1, T2> |
*const T | ptr_const$<T> |
*mut T | ptr_mut$<T> |
usize | size_t2 |
isize | ptrdiff_t2 |
uN | unsigned __intN2 |
iN | __intN2 |
f32 | float2 |
f64 | double2 |
f128 | fp1282 |
제네릭
Rust는 제네릭 타입 정보(ArrayVec<T, N: usize>의 T)는 출력하지만, 제네릭 값 정보(ArrayVec<T, N: usize>의 N)는 출력하지 않습니다.
CodeView에는 제네릭/C++ 템플릿을 위한 리프 노드가 없으므로, PDB 디버그 정보를 생성할 때 모든 제네릭 정보가 사라집니다. 디버거가 타입 이름을 통해 제네릭 인자를 가져올 수 있게 하는 우회 방법이 있지만, 좋게 보아도 취약한 해결책에 불과합니다. 이 결함을 바로잡기 위해 Microsoft와 연락하거나, 사용되지 않는 CodeView 노드 타입 중 하나를 적절한 대체물로 사용하려는 노력이 진행 중입니다.
타입 별칭
Rust는 디버거의 한계를 보완하기 위해 여러 경우에 typedef 노드를 출력하지만, 현재 소스 코드의 타입 별칭에 대해서는 노드를 출력하지 않습니다.
열거형
열거형 DI 노드는 rustc_codegen_llvm/src/debuginfo/metadata/enums에서 생성됩니다.
DWARF
DWARF에는 판별 공용체를 위한 전용 노드인 DW_TAG_variant가 있습니다. 이는 판별값을 포함할 수도 있고 포함하지 않을 수도 있는 DW_TAG_variant_part 노드를 참조하는 컨테이너입니다. 계층 구조는 다음과 같습니다.
DW_TAG_structure_type (top-level type for the coroutine)
DW_TAG_variant_part (variant part)
DW_AT_discr (reference to discriminant DW_TAG_member)
DW_TAG_member (discriminant member)
DW_TAG_variant (variant 1)
DW_TAG_variant (variant 2)
DW_TAG_variant (variant 3)
DW_TAG_structure_type (type of variant 1)
DW_TAG_structure_type (type of variant 2)
DW_TAG_structure_type (type of variant 3)
PDB
PDB에는 전용 노드가 없으므로, 판별 공용체에 해당하는 C 구조를 생성합니다.
union enum2$<RUST_ENUM_NAME> {
enum VariantNames {
First,
Second
};
struct Variant0 {
struct First {
// fields
};
static const enum2$<RUST_ENUM_NAME>::VariantNames NAME;
static const unsigned long DISCR_EXACT;
enum2$<RUST_ENUM_NAME>::Variant0::First value;
};
struct Variant1 {
struct Second {
// fields
};
static enum2$<RUST_ENUM_NAME>::VariantNames NAME;
static unsigned long DISCR_EXACT;
enum2$<RUST_ENUM_NAME>::Variant1::Second value;
};
enum2$<RUST_ENUM_NAME>::Variant0 variant0;
enum2$<RUST_ENUM_NAME>::Variant1 variant1;
unsigned long tag;
}
중요한 점은 LLDB의 한계 때문에 값이 #[repr(u64)]가 아니더라도 생성되는 DISCR_* 값은 항상 u64라는 것입니다. DISCR_* 값과 tag는 타입과 관계없이 uint64_t 값으로 읽히므로, LLDB에서는 이것이 대체로 문제가 되지 않습니다.
소스 정보
TODO