LLDB - Python 프로바이더
참고: LLDB의 C++<->Python FFI는 LLDB를 컴파일할 때 지정된 Python 버전을 요구합니다. LLDB는 이 버전을 일반적인 Linux 및 macOS 배포판의 최소 버전에 맞추도록 신경 쓰지만, Windows에서는 쉬운 해결책이 없습니다.
_lldb가 존재하지 않는다는 import 오류가 발생한다면 Python 버전 불일치가 원인일 가능성이 높습니다.LLDB는 이 문제에 대한 해결책을 검토하고 있습니다. 최신 소식은 이 토론과 이 GitHub 이슈를 참고하십시오.
NOTE: 2025년 11월 기준으로 LLDB가 지원하는 최소 Python 버전은 3.8이며, 여러 외부 요인에 따라 3.9 또는 3.10으로 올릴 계획입니다. 스크립트는 가급적 지원되는 최소 Python 버전에서 사용할 수 있는 기능만으로 작성해야 합니다. 자세한 내용은 이 토론을 참고하십시오.
참고: LLDB의 Python 패키지 경로는 CLI 명령
lldb -P로 확인할 수 있습니다.
LLDB는 출력을 사용자 정의하는 3가지 메커니즘을 제공합니다.
- 포맷
- 합성 프로바이더
- 요약 프로바이더
포맷
공식 문서는 여기에 있습니다. 요약하자면, 형식(format)을 사용하면 기본 타입의 기본 출력 형식을 설정할 수 있습니다(예: 25u8을 10진수 25, 16진수 0x19, 또는 바이너리 00011001로 출력).
Rust에서는 거의 항상 unsigned char, signed char, char, u8, i8을 (부호 없는) 10진수 형식으로 재정의해야 합니다.
합성 프로바이더
공식 문서는 여기에 있지만, 일부 정보는 모호하거나 오래되었거나 아예 빠져 있습니다.
사용자가 변수와 상호작용하는 거의 모든 경우는 LLDB의 SBValue 객체를 통해 이루어지며, 이 객체는 Python API에서도, LLDB의 플러그인과 CLI 내부에서도 사용됩니다.
합성 프로바이더는 특정 인터페이스에 맞춰 작성된 Python 클래스로, 하나 이상의 Rust 타입과 연결됩니다. 합성 프로바이더는 SBValue 객체를 감싸며, LLDB는 변수를 검사할 때 우리 클래스의 함수를 호출합니다.
감싸진 값은 여전히 SBValue이지만, 예를 들어 SBValue.GetChildAtIndex를 호출하면 내부적으로 SyntheticProvider.get_child_at_index가 호출됩니다. 값에 합성 프로바이더가 있는지는 SBValue.IsSynthetic()로, 어떤 합성 프로바이더인지는 SBValue.GetTypeSynthetic()으로 확인할 수 있습니다. 내부의 합성되지 않은 값과 상호작용하려면 SBValue.GetNonSyntheticValue()를 호출하면 됩니다.
요구되는 인터페이스는 다음과 같습니다.
class SyntheticProvider:
def __init__(self, valobj: SBValue, _lldb_internal): ...
# optional
def update(self) -> bool: ...
# optional
def has_children(self) -> bool: ...
# optional
def num_children(self, max_children: int) -> int: ...
def get_child_index(self, name: str) -> int: ...
def get_child_at_index(self, index: int) -> SBValue: ...
# optional
def get_type_name(self) -> str: ...
# optional
def get_value(self) -> SBValue: ...
아래에서는 각 메서드와 그 특이점, 그리고 일반적인 사용 방법을 설명합니다. 메서드가 SBValue 메서드를 재정의하는 경우 해당 메서드도 함께 표기합니다.
__init__
이 함수는 객체당 한 번 호출되며, 다른 곳에서 접근할 수 있도록 valobj를 Python 클래스에 저장해야 합니다. 그 밖의 작업은 여기서 최소한으로만 수행해야 합니다.
(선택 사항) update
이 함수는 __init__ 이후, LLDB가 변수와 상호 작용하기 전에 호출됩니다. LLDB는 update가 이미 호출되었는지 추적합니다. 이미 호출되었고 변수가 변경되었을 가능성이 없다면(예: 스텝 실행 없이 같은 변수를 두 번째로 검사하는 경우), LLDB는 update 호출을 생략합니다.
이 함수에는 두 가지 목적이 있습니다:
- 마지막으로
update가 실행된 이후 변경되었을 수 있는 모든 정보를 저장/갱신합니다. - 자식 캐시를 비워야 할 만큼 자식에 변경이 있었다면 이를 LLDB에 알립니다.
대표적인 작업으로는 Vec의 힙 포인터, 길이, 용량, 요소 타입을 저장하는 일, 열거형 변수의 배리언트를 판별하는 일, HashMap의 어느 슬롯이 사용 중인지 확인하는 일 등이 있습니다.
이 함수가 반환하는 bool 값은 다소 복잡하므로, 자세한 내용은 아래의 update 캐싱을 참고하십시오. 확실하지 않다면 False/None을 반환하십시오. 2025년 11월 기준으로 True를 반환하는 시각화는 하나도 없습니다. 하지만 디버그 정보 테스트 스위트가 개선되면 달라질 수 있습니다.
update 캐싱
LLDB는 가능한 경우 자식 값을 포함한 값을 캐시하려고 시도합니다. 이 캐시는 사실상 자식 객체의 개수와, 자식 객체가 나타내는 디버기(debugee)의 기반 메모리 주소로 이루어집니다. True를 반환하면 마지막으로 update가 실행된 이후 자식의 개수와 그 자식들의 주소가 변경되지 않았음을 LLDB에 알리는 것이며, 이는 캐시된 자식을 재사용할 수 있다는 뜻입니다.
잘못된 상황에서 True를 반환하면 디버거가 잘못된 정보를 출력하게 됩니다.
False를 반환하면 변경이 있었음을 나타내며, 캐시가 비워지고 자식을 처음부터 다시 가져옵니다. 확실하지 않다면 이쪽이 더 안전한 선택입니다.
중요한 관계는 부모-자식 관계뿐입니다. 손자는 조부모가 아니라 직계 부모의 update 함수에 의존합니다.
자식 캐시를 메모리를 가리키는 포인터로 보는 것이 중요합니다. 예를 들어 슬라이스의 data_ptr 값과 length가 변경되지 않았다면 True를 반환하는 것이 적절합니다. 슬라이스가 가변이어서 그 요소를 덮어쓰더라도(예: slice[0] = 15), 자식 캐시는 포인터로 구성되어 있으므로 해당 메모리 위치의 새 데이터를 반영합니다.
반대로 data_ptr가 변경되었다면 메모리의 새 위치를 가리키고 있다는 뜻이므로, 자식 포인터는 무효이며 캐시를 비워야 합니다. length가 변경되었다면 새로운 자식 개수를 반영하기 위해 캐시를 비워야 합니다. length는 변경되었지만 data_ptr는 변경되지 않았다면, 이전 자식을 SyntheticProvider 자체에 저장해 두고(예: list[SBValue]) 처음부터 생성하는 대신 그것을 내어 줄 수 있으며, SyntheticProvider의 목록에 아직 없는 경우에만 새 자식을 생성하면 됩니다.
더 자세한 설명은 이 논의를 참고하십시오.
참고: 캐싱 동작을 테스트할 때는 스텝 실행 시 변수를 유지하는 LLDB의 휴리스틱에 의존하지 마십시오. 대신 변수를 Python 객체에 저장하고(예:
v = lldb.frame.var("var_name")), 앞으로 스텝 실행한 다음, 저장된 변수를 검사하십시오.
(선택 사항) has_children
SBValue.MightHaveChildren을 재정의합니다
이 함수는 LLDB가 값에 자식이 하나라도 있는지 확인할 때 사용하는 지름길로, 자식이 몇 개인지 알아내는 데 드는, 비용이 클 수 있는 계산(예: 연결 리스트)을 수행하지 않습니다. 대개 return True/return False 또는 return self.valobj.MightHaveChildren()처럼 한 줄로 작성합니다.
(선택 사항) num_children
SBValue.GetNumChildren을 재정의합니다
타입을 출력할 때 LLDB가 접근을 시도해야 하는 자식의 총개수를 반환합니다. 이 수는 합성 자식의 총개수와 일치할 필요가 없습니다.
자식의 개수를 계산하는 비용이 클 수 있는 경우(예: 연결 리스트) max_children 인자를 반환할 수 있습니다. 이 점을 고려할 필요가 없다면 함수 시그니처에서 max_children을 생략해도 됩니다.
또한 필드를 LLDB에서는 의도적으로 “숨기면서도” 사용자는 계속 접근할 수 있게 할 수 있습니다. 예를 들어 vec![1, 2, 3]은 원소만 표시하되, len 값과 capacity 값은 요청하면 접근할 수 있게 하고 싶을 수 있습니다. num_children에서 3을 반환하면 LLDB가 [1, 2, 3]만 표시하도록 제한할 수 있으며, 사용자는 여전히 v.len과 v.capacity에 직접 접근할 수 있습니다. 이를 구현한 예는 예제 프로바이더: Vec<T>를 참고하십시오.
get_child_index
SBValue.GetIndexOfChildWithName을 재정의합니다
SBValue.GetChildMemberWithName에 영향을 줍니다
이름을 받아 해당 자식에 접근할 때 사용할 인덱스를 반환합니다. 이 함수의 반환값은 get_child_at_index에 그대로 전달된다고 가정합니다. num_children과 마찬가지로, 여기서 반환하는 값은 get_child_at_index와 제대로 맞춰져 있기만 하면 임의의 값이어도 됩니다.
특수한 값으로 $$dereference$$가 있습니다. 이 의사 필드를 처리하면, LLDB가 표현식 파서로 역참조할 때(예: *val, val->field) get_child_at_index에서 반환된 SBValue를 그 결과로 사용할 수 있습니다.
get_child_at_index
SBValue.GetChildAtIndex를 재정의합니다
인덱스를 받아 자식 SBValue를 반환합니다. 이 값은 보통 SBValue.CreateValueFromAddress로 생성하며, 드물게는 SBValue.CreateChildAtOffset, SBValue.CreateValueFromExpression, SBValue.CreateValueFromData로 생성하기도 합니다. 이 함수들은 다소 까다로울 수 있으므로 원하는 출력을 얻으려면 이리저리 조정해 보아야 할 수도 있습니다.
경우에 따라서는 SBValue.Clone이 적합합니다. 이 함수는 기존 자식을 그대로 복사하되 이름만 새로 붙인 자식을 만듭니다. 이는 튜플처럼 필드 이름이 __0, __1, … 형식이지만 0, 1, …로 이름 붙기를 원하는 경우에 유용합니다.
생성된 자식을 반환하기 전에 약간 수정할 수도 있습니다. 이는 &str/String처럼 자식을 단순한 10진수 값이 아니라 lldb.eFormatBytesWithASCII로 표시하고 싶은 경우에 유용합니다.
(선택 사항) get_type_name
SBValue.GetDisplayTypeName을 재정의합니다.
타입의 표시 이름을 재정의합니다. 타입 이름이 재정의된 합성 SBValue라도 원래 타입 이름은 SBValue.GetTypeName()과 SBValue.GetType().GetName()으로 여전히 가져올 수 있습니다.
이 기능은 자주 쓰이는 표준 라이브러리 타입의 이름을 줄이거나(예: std::collections::hash::map::HashMap<K, V, std::hash::random::RandomState> -> HashMap<K, V>), MSVC 타입 이름을 정규화할 때(예: ref$<str$> -> &str) 유용합니다.
문자열 조작은 다소 까다로울 수 있습니다. 타입의 제네릭 매개변수에 편리하게 접근할 수 없는 MSVC에서는 특히 그렇습니다.
(선택 사항) get_value
SBValue.GetValue(),SBValue.GetValueAsUnsigned(),SBValue.GetValueAsSigned(),SBValue.GetValueAsAddress()를 재정의합니다.
반환되는 SBValue는 원시 타입이나 포인터여야 하며, 표현식에서 변수의 값으로 취급됩니다.
중요: 반환되는
SBValue는 반드시SyntheticProvider에 저장해야 합니다. 2025년 11월 기준으로SBValue를get_value안에서 얻은 뒤 어디에도 저장하지 않으면, LLDB가 그 값에 접근할 때 Python이 세그폴트를 일으키는 버그가 있습니다.
요약 프로바이더
요약 프로바이더는 다음과 같은 형태의 Python 함수입니다:
def SummaryProvider(valobj: SBValue, _lldb_internal) -> str: ...
반환된 문자열은 가공 없이 그대로 사용자에게 전달됩니다. 반환값이 문자열이 아니면 단순하게 문자열로 변환됩니다(예: return None은 빈 문자열이 아니라 "None"을 출력합니다).
전달된 SBValue가 합성 프로바이더를 가진 타입이라면 valobj.IsSynthetic()은 True를 반환하고, 합성 프로바이더의 해당 함수들이 사용됩니다. 이렇게 동작하지 않기를 원한다면 valobj.GetNonSyntheticValue()로 원래 값을 가져올 수 있습니다. 이는 String 같은 경우에 유용할 수 있습니다. 이런 경우 루프에서 GetChildAtIndex를 하나씩 호출하는 것은 힙 포인터에 접근하여 디버기의 메모리에서 바이트 배열 전체를 직접 읽고 Python의 bytes.decode()를 사용하는 것보다 훨씬 느립니다.
인스턴스 요약
일반적인 SummaryProvider 함수는 불투명한 SBValue를 인자로 받습니다. 해당 타입에 SyntheticProvider가 있으면 그 SBValue에 반영되지만, SyntheticProvider 인스턴스 자체나 그 내부 구현 세부 사항에는 접근할 수 없습니다. 요약을 완성하는 데 이러한 내부 세부 사항이 일부 필요한 경우에는 이 점이 걸림돌이 됩니다. 2025년 11월 기준으로 합성 쪽에서는 합성이 아닌 값을 합성 프로바이더에 그대로 통과시키기만 하는데(synth = SyntheticProvider(valobj.GetNonSyntheticValue(), _dict)), 이는 분명히 최적이 아니므로 아래에 설명한 방법을 사용할 계획입니다.
그 대신 Python 모듈의 상태를 활용하여 인스턴스 요약을 구현할 수 있습니다. 이 기법은 예전 CodeLLDB Rust 시각화 스크립트에서 이미 사용된 바 있습니다.
간단히 말해, 모든 합성 프로바이더의 __init__ 함수는 고유 ID와 self에 대한 약한 참조를 전역 딕셔너리에 저장합니다. 합성 프로바이더 클래스는 get_summary 함수도 구현합니다. 해당 타입의 SummaryProvider는 이 딕셔너리에서 고유 ID를 찾은 다음, 찾아낸 인스턴스의 get_summary를 호출하는 함수입니다.
import weakref
SYNTH_BY_ID = weakref.WeakValueDictionary()
class SyntheticProvider:
valobj: SBValue
# slots requires opting-in to __weakref__
__slots__ = ("valobj", "__weakref__")
def __init__(valobj: SBValue, _dict):
SYNTH_BY_ID[valobj.GetID()] = self
self.valobj = valobj
def get_summary(self) -> str:
...
def InstanceSummaryProvider(valobj: SBValue, _dict) -> str:
# GetNonSyntheticValue should never fail as InstanceSummaryProvider implies an instance of a
# `SyntheticProvider`. No non-synthetic types should ever have this summary assigned to them
# We use GetNonSyntheticValue because the synthetic vaobj has its own unique ID
return SYNTH_BY_ID[valobj.GetNonSyntheticValue().GetID()].get_summary()
예를 들어, 열거형 합성 프로바이더에 이 방법을 사용할 수 있습니다. 요약은 배리언트 이름에 접근해야 하지만, 합성의 타입 이름이나 자식 값을 통해 이를 반영할 편리한 방법이 없습니다. 인스턴스 요약을 구현하면 self.variant.GetTypeName()과 약간의 문자열 조작을 통해 배리언트 이름을 가져올 수 있습니다.
시각화 스크립트 작성하기
중요: GDB 및 CDB와 달리, LLDB는 DWARF와 PDB 중 어느 디버그 정보를 가진 실행 파일이든 디버깅할 수 있습니다. 시각화 도구는 가능한 한 두 형식을 모두 고려하여 작성해야 합니다. 차이점에 대한 개요는 rust-codegen(코드 생성)을 참조하십시오.
스크립트는 CLI 명령 command script import <path-to-script>.py를 통해 LLDB에 주입됩니다. 주입되고 나면 type synthetic add와 type summary add를 사용하여 클래스와 함수를 각각 합성 풀과 요약 풀에 추가할 수 있습니다. 요약과 합성 프로바이더는 “카테고리“와 연결될 수 있으며, 카테고리는 일반적으로 해당 프로바이더가 대상으로 하는 언어의 이름을 따서 짓습니다. 여기서 사용할 카테고리의 이름은 Rust입니다.
팁: 모든 LLDB 명령 앞에
help를 붙이면(예:help type synthetic add) 간단한 설명, 인자 목록, 예제를 볼 수 있습니다.
과거에는 command source ...를 사용했는데, 이는 lldb_commands 파일에서 일련의 CLI 명령을 실행하여 프로바이더를 추가합니다. 이 파일은 다소 다루기 불편했기 때문에, 아래에 설명하는 동등한 Python API 방식으로 대체되었습니다.
__lldb_init_module
이 함수는 다음과 같은 형태의 선택적 함수입니다:
def __lldb_init_module(debugger: SBDebugger, _lldb_internal) -> None: ...
이 함수는 command script import ...의 끝에서, 제어가 CLI로 돌아가기 전에 호출됩니다. 이를 통해 스크립트는 자체 상태를 초기화할 수 있습니다.
중요한 점은 이 함수에 디버거 자체에 대한 참조가 전달된다는 것입니다. 이를 통해 Rust 카테고리를 생성하고 여기에 프로바이더를 추가할 수 있습니다. 또한 스크립트가 감지한 LLDB 버전에 따라 사용할 프로바이더를 조건부로 변경할 수도 있습니다. 인식기는 lldb 19.0에서 추가되었으므로, 인식기 함수를 사용하기 시작하면 이는 하위 호환성을 위해 매우 중요합니다.
시각화 해석
시각화 도구가 결정되는 순서는 여기에 나와 있습니다. 요약하면 다음과 같습니다:
- 정확히 일치하는 항목(정규식이 아닌 이름, 인식기 함수, 또는 이미 프로바이더와 매칭된 타입)이 있으면 그것을 사용합니다
- 객체가 포인터/참조이면 역참조한 타입의 포매터를 사용해 봅니다.
- 객체가 typedef이면 기반 타입에 포매터가 있는지 확인합니다.
- 위의 방법이 모두 통하지 않으면 정규식 타입 매처를 차례로 순회합니다.
각 단계 안에서는 새 명령이 이전 명령을 “재정의“할 수 있도록 순회가 역순으로 이루어집니다. 이는 Box<str>와 Box<T>처럼 전자에는 특화된 합성을, 후자에는 더 일반화된 합성을 사용하려는 경우에 중요합니다.
세부 사항
LLDB의 API는 매우 강력하지만 몇 가지 “함정“과 직관적이지 않은 동작이 있으며, 그중 일부를 아래에서 설명합니다. Python 구현은 CLI 명령 lldb -P가 반환하는 경로의 lldb\__init__.py에서 확인할 수 있습니다. lldb 저장소의 예제 외에도 참고할 수 있는 C++ 시각화 도구가 있습니다(예: Vec<T>에 해당하는 LibCxxVector). C++의 시각화 도구는 C++로 작성되어 LLDB 내부에 접근할 수 있지만, API와 일반적인 관행은 매우 비슷합니다.
SBValue
- 포인터/참조
SBValue는 경우에 따라 사실상 “자동 역참조“되어, 가리키는 객체의 자식을 마치 자신의 자식인 것처럼 다룹니다. - 함수가 아닌 필드는 대개 결국 함수를 직접 가리키는
property()필드입니다(예:SBValue.type = property(GetType, None)). 이러한 축약형을 통해 접근하면 함수를 직접 호출하는 것보다 약간 느리므로 사용하지 않는 것이 좋습니다. 일부 프로퍼티는 특수한 프로퍼티를 가진 특수 객체를 반환합니다(예:SBValue.member는 자식에 접근하기 위해dict[str, SBValue]처럼 동작하는 객체를 반환합니다). 내부적으로 이러한 특수 객체 중 상당수는 새 클래스 인스턴스를 할당한 뒤 결국SBValue의 함수를 호출할 뿐이므로 추가적인 성능 손실이 발생합니다(예:SBValue.member는 내부적으로__getitem__만 구현하며, 이는 한 줄짜리 코드return self.valobj.GetChildMemberWithName(name)입니다) SBValue.GetID는 디버그 세션 동안 각 값마다 고유한int를 반환합니다. 합성(synthetic)SBValue는 그 기반이 되는SBValue와 다른 ID를 가집니다. 기반 ID는SBValue.GetNonSyntheticValue().GetID()를 통해 얻을 수 있습니다.- 주소를 수동으로 계산할 때는 타깃별 동작 때문에
SBValue.GetValueAsUnsigned보다SBValue.GetValueAsAddress를 사용하는 것이 좋습니다 SBValue의 문자열 표현을 얻는 것은 까다로울 수 있습니다.GetSummary는 요약 프로바이더를 필요로 하고,GetValue는 타입이 원시 타입으로 표현 가능해야 하기 때문입니다. 두 조건 중 어느 것도 충족되지 않는 거의 모든 경우, 해당 타입은StructSummaryProvider에 전달할 수 있는 사용자 정의 구조체입니다.
SBType
- “집합 타입(Aggregate type)“은 원시 타입이 아닌 구조체/클래스/공용체를 의미합니다
- “템플릿(Template)“은 “제네릭(Generic)“과 같은 의미입니다
- 타입은
SBTarget.FindFirstType(type_name)을 통해 이름으로 조회할 수 있습니다.SBTarget은SBValue.GetTarget을 통해 얻을 수 있습니다 SBType.template_args는 타입에 제네릭이 없으면 빈 리스트 대신None을 반환합니다- 때로는
SBType.GetArrayType이나SBType.GetPointerType같은 함수를 통해 타입을 원하는 타입으로 변환해야 합니다. 이 함수들은 실패하지 않습니다. 이 함수들은 디버그 정보를 완전히 우회하여 기반 LLDBTypeSystem플러그인에 해당 타입을 요청합니다. 디버그 정보에 해당 타입이 전혀 존재하지 않더라도 이 함수들은 적절한 타입을 생성할 수 있습니다. SBType.GetCanonicalType은 사실상SBType.GetTypedefedType+SBType.GetUnqualifiedType입니다.SBType.GetTypedefedType과 달리, 이 함수는 원래SBType이 typedef인지 여부와 관계없이 항상 유효한SBType을 반환합니다.SBType.GetStaticFieldWithName은 LLDB 18에서 추가되었습니다. 안타깝게도 정적 필드는 이 함수 없이는 전혀 접근할 수 없으므로, 하위 호환성을 항상 보장할 수는 없습니다.
예제 프로바이더: Vec<T>
SyntheticProvider
필드가 정해져 있으므로 __slots__를 사용하는 일반적인 서두부터 시작합니다. Vec의 힙 포인터는 *mut T가 아니라 *mut u8이므로, 객체 자체 외에 요소의 타입도 저장해야 합니다. Rust는 정적 타입 언어이므로 T의 타입은 절대 바뀌지 않습니다. 따라서 초기화 시점에 이를 저장할 수 있습니다. 하지만 힙 포인터, 길이, 용량은 바뀔 수 있으므로 여기서는 기본값으로 초기화합니다.
import lldb
class VecSyntheticProvider:
valobj: SBValue
data_ptr: SBValue
len: int
cap: int
element_type: SBType
__slots__ = (
"valobj",
"data_ptr",
"len",
"cap",
"element_type",
"__weakref__",
)
def __init__(valobj: SBValue, _dict) -> None:
self.valobj = valobj
# invalid type is a better default than `None`
self.element_type = SBType()
# special handling to account for DWARF/PDB differences
if (arg := valobj.GetType().GetTemplateArgumentType(0)):
self.element_type = arg
else:
arg_name = next(get_template_args(valobj.GetTypeName()))
self.element_type = resolve_msvc_template_arg(arg_name, valobj.GetTarget())
get_template_args와 resolve_msvc_template_arg의 구현은 lldb_providers.py를 참고하십시오.
다음은 update 함수입니다. 포인터나 길이가 바뀌었는지 확인합니다. 자식의 수는 len이 바뀌지 않는 한 그대로 유지되므로 용량 확인은 생략할 수 있습니다. 용량 변경으로 재할당이 일어났다면 data_ptr의 주소가 달라졌을 것입니다.
data_ptr, length 모두 바뀌지 않았다면 LLDB의 캐싱을 활용하여 일찍 반환할 수 있습니다. 바뀌었다면 새 값을 저장하고 LLDB에 캐시를 비우도록 지시합니다.
def update(self):
ptr = self.valobj.GetChildMemberWithName("data_ptr")
len = self.valobj.GetChildMemberWithName("length").GetValueAsUnsigned()
if (
self.data_ptr.GetValueAsAddress() == ptr.GetValueAsAddress()
and self.len == len
):
# Our child address offsets and child count are still valid
# so we can reuse cached children
return True
self.data_ptr = ptr
self.len = len
return False
has_children과 num_children은 모두 간단합니다:
def has_children(self) -> bool:
return True
def num_children(self) -> int:
return self.len
요소에 접근할 때는 인덱싱을 흉내 내기 위해 [0], [1] 등의 형식으로 된 값을 기대합니다. 또한 길이와 용량은 디버깅할 때 매우 유용할 수 있으므로, 사용자가 이 값들에도 빠르게 접근할 수 있게 하려 합니다. 요소 값과 겹치지 않는다고 거의 확실히 보장할 수 있으므로, 이 값들에 각각 u32::MAX - 1과 u32::MAX - 2를 할당합니다. capacity의 전체 이름과 축약 이름을 모두 처리할 수 있다는 점에 유의하십시오.
def get_child_index(self, name: str) -> int:
index = name.lstrip("[").rstrip("]")
if index.isdigit():
return int(index)
if name == "len":
return lldb.UINT32_MAX - 1
if name == "cap" or name == "capacity":
return lldb.UINT32_MAX - 2
return -1
이제 요소, 길이, 용량에 모두 접근할 수 있도록 get_child_at_index를 적절히 조정해야 합니다.
def get_child_at_index(self, index: int) -> SBValue:
if index == UINT32_MAX - 1:
return self.valobj.GetChildMemberWithName("len")
if index == UINT32_MAX - 2:
return (
self.valobj.GetChildMemberWithName("buf")
.GetChildMemberWithName("inner")
.GetChildMemberWithName("cap")
.GetChildAtIndex(0)
.Clone("capacity")
)
addr = self.data_ptr.GetValueAsAddress()
addr += index * self.element_type.GetByteSize()
return self.valobj.CreateValueFromAddress(f"[{index}]", addr, self.element_type)
타입의 표시 이름에서는 경로 한정자를 제거할 수 있습니다. Vec이라는 이름의 사용자 정의 타입은 완전한 경로로 표시되므로 모호성이 없을 것입니다. 할당자 제네릭도 유용한 경우가 매우 드물기 때문에 제거할 수 있습니다. 다음 3가지 이유로 self.element_type.GetName() 대신 get_template_args를 사용합니다:
- 어떤 이유로든 요소 타입을 해석하지 못하더라도,
self.valobj의 타입 이름으로 요소의 실제 타입이 무엇인지 사용자에게 알려 줄 수 있습니다 - 타입 이름은 DWARF 및 PDB 노드의 제약을 받지 않으므로, 이름 안의 템플릿 타입에는
*const/*mut와&/&mut같은 것이 반영됩니다. - 2025년 11월 기준으로 MSVC 타입 이름을 정규화하지 않지만, 정규화하게 되면 어차피 타입의 문자열 이름을 다뤄야 합니다. 또한 문자열에서 문자열로의 변환을 캐시하는 것이 `SBType`에서 문자열로의 변환을 캐시하는 것보다 훨씬 쉽습니다.
def get_type_name(self) -> str:
return f"Vec<{next(get_template_args(self.valobj))}>"
Vec을 표현할 적절한 원시 값이 없으므로 get_value 함수는 그냥 생략합니다.
SummaryProvider
합성 프로바이더 덕분에 요약 프로바이더는 매우 간단합니다. 유일한 문제는 객체의 타입에 SummaryProvider가 있을 때만 GetSummary가 값을 반환한다는 점입니다. 그렇지 않으면 빈 문자열을 반환하는데, 이는 바람직하지 않습니다. 완전한 시각화 스크립트 세트에서는 GetSummary()나 GetValue()가 없는 모든 타입이 구조체임을 보장한 다음, 제네릭 StructSummaryProvider에 위임할 수 있습니다. 이 시연에서는 그 세부 사항을 생략하겠습니다.
def VecSummaryProvider(valobj: SBValue, _lldb_internal) -> str:
children = []
for i in range(valobj.GetNumChildren()):
child = valobj.GetChildAtIndex(i)
summary = child.GetSummary()
if summary is None:
summary = child.GetValue()
if summary is None:
summary = "{...}"
children.append(summary)
return f"vec![{", ".join(children)}]"
프로바이더 활성화하기
이 합성 제공자를 lldb_lookup.py에 임포트했다고 가정합니다
CLI 명령을 사용하는 경우:
type synthetic add -l lldb_lookup.synthetic_lookup -x "^(alloc::([a-z_]+::)+)Vec<.+>$" --category Rust
type summary add -F lldb_lookup.summary_lookup -x "^(alloc::([a-z_]+::)+)Vec<.+>$" --category Rust
__lldb_init_module을 사용하는 경우:
def __lldb_init_module(debugger: SBDebugger, _dict: LLDBOpaque):
# Ensure the category exists and is enabled
rust_cat = debugger.GetCategory("Rust")
if not rust_cat.IsValid():
rust_cat = debugger.CreateCategory("Rust")
rust_cat.SetEnabled(True)
# Register Vec providers
vec_regex = r"^(alloc::([a-z_]+::)+)Vec<.+>$"
sb_name = lldb.SBTypeNameSpecifier(vec_regex, is_regex=True)
sb_synth = lldb.SBTypeSynthetic.CreateWithClassName("lldb_lookup.VecSyntheticProvider")
sb_synth.SetOptions(lldb.eTypeOptionCascade)
sb_summary = lldb.SBTypeSummary.CreateWithFunctionName("lldb_lookup.VecSummaryProvider")
sb_summary.SetOptions(lldb.eTypeOptionCascade)
rust_cat.AddTypeSynthetic(sb_name, sb_synth)
rust_cat.AddSummary(sb_name, sb_summary)
출력
프로바이더가 없는 경우:
(lldb) v vec_v
(alloc::vec::Vec<int, alloc::alloc::Global>) vec_v = {
buf = {
inner = {
ptr = {
pointer = (pointer = "\n")
_marker = {}
}
cap = (__0 = 5)
alloc = {}
}
_marker = {}
}
len = 5
}
(lldb) v vec_v[0]
error: <user expression 0>:1:6: subscripted value is not an array or pointer
1 | vec_v[0]
| ^
프로바이더가 있는 경우(v <var_name>은 요약을 출력한 다음 모든 자식의 목록을 출력합니다):
(lldb) v vec_v
(Vec<int>) vec_v = vec![10, 20, 30, 40, 50] {
[0] = 10
[1] = 20
[2] = 30
[3] = 40
[4] = 50
}
(lldb) v vec_v[0]
(int) vec_v[0] = 10
“숨겨진” 길이와 용량에 여전히 접근할 수 있다는 것도 확인할 수 있습니다:
(lldb) v vec_v.len
(unsigned long long) vec_v.len = 5
(lldb) v vec_v.capacity
(unsigned long long) vec_v.capacity = 5
(lldb) v vec_v.cap
(unsigned long long) vec_v.cap = 5