Skip to content

Instantly share code, notes, and snippets.

@flga
Last active February 21, 2025 16:16
Show Gist options
  • Save flga/90b9fb63a1f4b670b102f9e1bddb156a to your computer and use it in GitHub Desktop.
Save flga/90b9fb63a1f4b670b102f9e1bddb156a to your computer and use it in GitHub Desktop.
WIP Odin lldb
import lldb
import math
import traceback
def __lldb_init_module(debugger, dict):
debugger.HandleCommand('type summary add -F odin.string_summary -w odin string')
debugger.HandleCommand('type summary add -F odin.typeid_summary -w odin typeid')
debugger.HandleCommand('type synth add -l odin.TypeidSynth -w odin typeid')
debugger.HandleCommand('type summary add -F odin.any_summary -w odin any')
debugger.HandleCommand('type synth add -l odin.AnySynth -w odin any')
debugger.HandleCommand('type summary add -F odin.union_summary --recognizer-function odin.is_union -w odin')
debugger.HandleCommand('type synth add -l odin.UnionSynth --recognizer-function odin.is_union -w odin')
debugger.HandleCommand('type summary add -F odin.slice_dynarray_summary --recognizer-function odin.is_slice_or_dynarray -w odin')
debugger.HandleCommand('type synth add -l odin.SliceDynArraySynth --recognizer-function odin.is_slice_or_dynarray -w odin')
debugger.HandleCommand('type summary add -F odin.soa_slice_dynarray_summary --recognizer-function odin.is_soa_slice_or_dynarray -w odin')
debugger.HandleCommand('type synth add -l odin.SoaSliceDynArraySynth --recognizer-function odin.is_soa_slice_or_dynarray -w odin')
debugger.HandleCommand('type summary add -F odin.map_summary --recognizer-function odin.is_map -w odin')
debugger.HandleCommand('type synth add -l odin.MapSynth --recognizer-function odin.is_map -w odin')
debugger.HandleCommand('type synth add -l odin.EnumArraySynth --recognizer-function odin.is_enum_array -w odin')
debugger.HandleCommand('type category enable odin')
# enum arrays ==================================================================
def is_enum_array(type, internal_dict):
if type.type != lldb.eTypeClassTypedef:
return False
if not type.name.startswith("["):
return False
target = lldb.debugger.GetSelectedTarget()
count_type = target.FindFirstType(type.name[1:type.name.find("]")])
if not count_type.IsValid():
return False
return count_type.type == lldb.eTypeClassEnumeration
class EnumArraySynth:
def __init__(self, value, internal_dict):
self.value = value
count_type_name = value.GetDisplayTypeName()
count_type = value.GetTarget().FindFirstType(count_type_name[1:count_type_name.find("]")])
self.count_members = count_type.GetEnumMembers()
self.elem_type = value.GetType().GetArrayElementType()
self.elem_type_size = self.elem_type.GetByteSize()
self
def num_children(self):
return len(self.count_members)
def has_children(self):
return len(self.count_members) > 0
def get_child_at_index(self, index):
offset = index * self.elem_type_size
return self.value.CreateChildAtOffset(f"[{index}, {self.count_members[index].name}]", offset, self.elem_type)
# slices and dynarrays =========================================================
def is_slice_or_dynarray(type, internal_dict):
if type.type != lldb.eTypeClassStruct:
return False
return type.name.startswith("[]") or type.name.startswith("[dynamic]")
def slice_dynarray_summary(value, internal_dict):
raw = value.GetNonSyntheticValue()
len = raw.GetChildMemberWithName("len")
cap = raw.GetChildMemberWithName("cap")
if cap.IsValid():
return f"len = {len.GetValueAsSigned(0)}, cap = {cap.GetValueAsSigned(0)}"
return f"len = {len.GetValueAsSigned(0)}"
class SliceDynArraySynth:
def __init__(self, value, internal_dict):
self.value = value
self.update()
def update(self):
raw = self.value.GetNonSyntheticValue()
self.len = raw.GetChildMemberWithName("len").GetValueAsSigned()
self.data = raw.GetChildMemberWithName("data")
self.elem_type = self.data.GetType().GetPointeeType()
self.elem_type_size = self.elem_type.GetByteSize()
def num_children(self):
return self.len
def has_children(self):
return self.len > 0
def get_child_at_index(self, index):
offset = index * self.elem_type_size
return self.data.CreateChildAtOffset(f"[{index}]", offset, self.elem_type)
# soa slices and dynarrays =====================================================
def is_soa_slice_or_dynarray(type, internal_dict):
if type.type != lldb.eTypeClassStruct:
return False
return type.name.startswith("#soa[]") or type.name.startswith("#soa[dynamic]")
def soa_slice_dynarray_summary(value, internal_dict):
raw = value.GetNonSyntheticValue()
len = raw.GetChildMemberWithName("__$len")
cap = raw.GetChildMemberWithName("__$cap")
if cap.IsValid():
return f"len = {len.GetValueAsSigned(0)}, cap = {cap.GetValueAsSigned(0)}"
return f"len = {len.GetValueAsSigned(0)}"
class SoaSliceDynArraySynth:
def __init__(self, value, internal_dict):
self.value = value
self.update()
def update(self):
raw = self.value.GetNonSyntheticValue()
total_fields = raw.GetType().GetNumberOfFields()
# double #soa (as in #soa[]#soa...) will have multiple __$len fields, and this will pick the wrong one
# I don't know why you'd do this in the first place, so leaving it as is
# as it allows us to use the exact same code for slices and dynarrays
self.len = raw.GetChildMemberWithName("__$len").GetValueAsSigned()
self.num_members = total_fields - 1
def num_children(self):
if self.len == 0:
return 0
return self.num_members
def has_children(self):
if self.len == 0:
return False
return self.num_members > 0
def get_child_at_index(self, index):
raw = self.value.GetNonSyntheticValue()
member = raw.GetChildAtIndex(index)
addr = member.AddressOf().GetValueAsUnsigned(0)
name = member.GetName()
type = member.GetType().GetPointeeType().GetArrayType(self.len).GetPointerType()
# Dereferencing makes lldb-dap chunk the results
# It should never try to dereference a null because we pretend we have no kids
# if len == 0
return member.CreateValueFromAddress(name, addr, type).Dereference()
# strings ======================================================================
def string_summary(value, internal_dict):
return f"\"{read_string(value)}\""
# any ==========================================================================
def any_summary(value, internal_dict):
try:
data = value.GetChildAtIndex(0)
summary = data.GetSummary()
if summary is None:
summary = data.GetValue()
return f'({data.GetDisplayTypeName()}) {summary}'
except Exception as e:
print(traceback.format_exc())
class AnySynth:
def __init__(self, value, internal_dict):
self.value = value
self.update()
def num_children(self):
return 2
def get_child_index(self, name):
if name == "id":
return 0
if name == "data":
return 1
def get_child_at_index(self, index):
if index == 0:
return self.typeid
if index == 1:
return self.data
def update(self):
raw = self.value.GetNonSyntheticValue()
self.typeid = raw.GetChildMemberWithName("id")
self.data = raw.GetChildMemberWithName("data")
try:
# print("@typeid", self.typeid)
# print("@type_info_of", type_info_of(self.typeid))
# print("@type_of", type_of(type_info_of(self.typeid)))
typ = type_of(type_info_of(self.typeid))
if typ is not None:
self.data = self.data.Cast(typ.GetPointerType())
except Exception as e:
print(traceback.format_exc())
def has_children(self):
return True
# typeid =======================================================================
def typeid_summary(value, internal_dict):
try:
return odin_name(type_info_of(value))
except:
return value.GetValueAsUnsigned(0)
class TypeidSynth:
def __init__(self, value, internal_dict):
self.value = value
self.type_info = None
self.update()
def num_children(self):
if self.type_info is None:
return 0
return 1
def get_child_index(self, name):
if name == "value":
return 0
return None
def get_child_at_index(self, index):
return self.type_info
def update(self):
try:
self.type_info = type_info_of(self.value)
self.type_info = self.type_info.CreateValueFromAddress("value", self.type_info.address_of.GetValueAsUnsigned(0), self.type_info.GetType())
except Exception as e:
print(traceback.format_exc())
def has_children(self):
return self.type_info is not None
# unions =======================================================================
def is_union(type, internal_dict):
if type.type != lldb.eTypeClassUnion:
return False
first_field = type.GetFieldAtIndex(0)
return first_field.name == "v0" or first_field.name == "tag"
def union_summary(value, internal_dict):
variant = value.GetChildMemberWithName("value")
if not variant.IsValid():
return "nil"
# The default lldb format includes the field name (`value`) which is irrelevant,
# so we do the formatting ourselves in order to omit it.
summary = variant.GetSummary()
if summary is None:
summary = variant.GetValue()
return f"({variant.GetDisplayTypeName()}) {summary}"
class UnionSynth:
def __init__(self, value, internal_dict):
self.value = value
self.has_v0 = self.value.GetChildMemberWithName("v0") is not None
def num_children(self):
return 1
def get_child_index(self, name):
if name == "value":
return 0
return None
def get_child_at_index(self, index):
if index != 0:
raise ValueError(f"there is no field at index {index}, unions only have one field so the index can't be anything other than 0")
value = self.value.GetNonSyntheticValue()
tag_value = value.GetChildMemberWithName("tag").GetValueAsUnsigned(0)
variant_name = f"v{tag_value}"
variant = value.GetChildMemberWithName(variant_name)
return variant.CreateValueFromAddress("value", variant.address_of.GetValueAsUnsigned(0), variant.GetType())
def has_children(self):
return True
# maps =========================================================================
def is_map(type, internal_dict):
if type.type != lldb.eTypeClassStruct:
return False
return type.name.startswith("map[")
def map_summary(value, internal_dict):
raw = value.GetNonSyntheticValue()
len = raw.GetChildMemberWithName("len")
cap_log2 = raw.GetChildMemberWithName("data").GetValueAsUnsigned(0) & 63
cap = 0 if cap_log2 <= 0 else 1 << cap_log2
return f"len = {len.GetValueAsSigned(0)}, cap = {cap}"
class MapSynth:
def __init__(self, value, internal_dict):
self.value = value
self.update()
def update(self):
raw = self.value.GetNonSyntheticValue()
self.len = raw.GetChildMemberWithName("len").GetValueAsSigned()
self.data = raw.GetChildMemberWithName("data")
data_addr = self.data.GetValueAsUnsigned(0)
self.base_addr = data_addr & ~63
cap_log2 = data_addr & 63
self.cap = 0 if cap_log2 <= 0 else 1 << cap_log2
self.map_key = self.data.GetChildMemberWithName("key")
self.map_key_type = self.map_key.GetType()
self.map_key_cell = self.data.GetChildMemberWithName("key_cell")
self.map_key_cell_type = self.map_key_cell.GetType()
self.map_key_base_addr = self.base_addr
self.map_value = self.data.GetChildMemberWithName("value")
self.map_value_type = self.map_value.GetType()
self.map_value_cell = self.data.GetChildMemberWithName("value_cell")
self.map_value_cell_type = self.map_value_cell.GetType()
self.map_value_base_addr = self.map_key_base_addr + self.map_cell_offset(self.map_key_type, self.map_key_cell_type, self.cap)
self.map_hash = self.data.GetChildMemberWithName("hash")
self.map_hash_type = self.map_hash.GetType()
self.tombstone_mask = 1 << (self.map_hash_type.GetByteSize() * 8 - 1)
self.map_hash_base_addr = self.map_value_base_addr + self.map_cell_offset(self.map_value_type, self.map_value_cell_type, self.cap)
self.map_hashes = self.value.CreateValueFromAddress("hashes", self.map_hash_base_addr, self.map_hash_type.GetArrayType(self.cap))
def map_cell_offset(self, elem_type, elem_cell_type, index):
if elem_type.GetByteSize() == 0:
return 0
elems_per_cell = elem_cell_type.GetByteSize() // elem_type.GetByteSize()
cell_index = index // elems_per_cell
data_index = index % elems_per_cell
return (cell_index * elem_cell_type.GetByteSize()) + (data_index * elem_type.GetByteSize())
def num_children(self):
return self.len
def has_children(self):
return self.len > 0
def get_child_at_index(self, index):
current_index = 0
for i in range(self.cap):
hash = self.map_hashes.GetChildAtIndex(i).GetValueAsUnsigned()
if hash == 0 or (hash & self.tombstone_mask) != 0:
continue
current_index += 1
if current_index - 1 != index:
continue
key_addr = self.map_key_base_addr + self.map_cell_offset(self.map_key_type, self.map_key_cell_type, i)
val_addr = self.map_value_base_addr + self.map_cell_offset(self.map_value_type, self.map_value_cell_type, i)
key = self.value.CreateValueFromAddress("key", key_addr, self.map_key_type)
if self.map_value_type.GetByteSize() == 0:
return key
name = key.GetSummary()
if name is None:
name = key.GetValue()
return self.value.CreateValueFromAddress(name, val_addr, self.map_value_type)
# type introspection ===========================================================
def find_type_table(target):
for var in target.FindGlobalVariables("type_table", 69):
if var.GetDisplayTypeName() == "[]^runtime.Type_Info":
return var
return None
def type_info_of(typeid):
id = int(typeid.GetValueAsUnsigned(0))
mask = (1 << (8 * typeid.GetByteSize() - 8)) - 1
index = (id & mask)
type_table = find_type_table(typeid.GetTarget()).GetNonSyntheticValue()
type_table_data = type_table.GetChildMemberWithName("data")
element_type = type_table_data.GetType().GetPointeeType()
base = type_table_data.GetValueAsUnsigned(0)
offset = index * element_type.GetByteSize()
return type_table_data.CreateValueFromAddress("", base + offset, element_type).Dereference()
def type_of(type_info):
target = type_info.GetTarget()
size = type_info.GetChildMemberWithName("size").GetValueAsSigned(0)
bit_size = size * 8
variant = type_info.GetChildMemberWithName("variant").GetNonSyntheticValue()
# print("@variant", variant)
variant_tag = variant.GetChildMemberWithName("tag").GetValueAsUnsigned(0)
# print("@variant_tag", variant_tag)
variant_data = variant.GetChildMemberWithName(f"v{variant_tag}")
# print("@variant_data", variant_data)
match variant_tag:
case 1: # Type_Info_Named
prefix = ""
base = variant_data.GetChildMemberWithName("base").Dereference()
# print("@base", base)
match base.GetChildMemberWithName("variant").GetNonSyntheticValue().GetChildMemberWithName("tag").GetValueAsUnsigned():
case 5: # Type_Info_Complex
prefix = "struct"
case 6: # Type_Info_Quaternion
prefix = "struct"
case 7: # Type_Info_String
prefix = "struct"
case 9: # Type_Info_Any
prefix = "struct"
case 16: # Type_Info_Dynamic_Array
prefix = "struct"
case 17: # Type_Info_Slice
prefix = "struct"
case 19: # Type_Info_Struct
prefix = "struct"
case 20: # Type_Info_Union
prefix = "union"
case 21: # Type_Info_Enum
prefix = "enum"
case 22: # Type_Info_Map
prefix = "struct"
case 27: # Type_Info_Bit_Field
prefix = "struct" # TODO: confirm
# print("@prefix", prefix)
name = read_string(variant_data.GetChildMemberWithName("name"))
# print("@name", name)
pkg = read_string(variant_data.GetChildMemberWithName("pkg"))
# print("@pkg", pkg)
# print("@key", f"{prefix} {pkg}.{name}")
# print("@ret", target.FindFirstType(f"{prefix} {pkg}.{name}"))
return target.FindFirstType(f"{prefix} {pkg}.{name}")
case 2: # Type_Info_Integer
signed = variant_data.GetChildMemberWithName("signed").GetValueAsSigned(0) == 1
endianness = variant_data.GetChildMemberWithName("endianness") # TODO: care
return target.FindFirstType(f"{"i" if signed else "u"}{bit_size}")
case 3: # Type_Info_Rune
return target.FindFirstType("rune")
case 4: # Type_Info_Float
endianness = variant_data.GetChildMemberWithName("endianness") # TODO: care
return target.FindFirstType(f"f{bit_size}")
case 5: # Type_Info_Complex
return target.FindFirstType(f"struct complex{bit_size}")
case 6: # Type_Info_Quaternion
return target.FindFirstType(f"struct quaternion{bit_size}")
case 7: # Type_Info_String
return target.FindFirstType(f"struct string")
case 8: # Type_Info_Boolean
if bit_size == 8:
return target.FindFirstType("bool")
return target.FindFirstType(f"b{bit_size}")
case 9: # Type_Info_Any
return target.FindFirstType(f"struct any")
case 10: # Type_Info_Type_Id
return target.FindFirstType(f"typeid")
case 11: # Type_Info_Pointer
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return type_of(elem).GetPointerType()
case 12: # Type_Info_Multi_Pointer
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return type_of(elem).GetPointerType()
case 13: # Type_Info_Procedure
return None
case 14: # Type_Info_Array
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return type_of(elem).GetArrayType(variant_data.GetValueAsSigned("count"))
case 15: # Type_Info_Enumerated_Array
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return type_of(elem).GetArrayType(variant_data.GetValueAsSigned("count"))
case 16: # Type_Info_Dynamic_Array
return target.FindFirstType(f"struct {odin_name(type_info)}")
case 17: # Type_Info_Slice
return target.FindFirstType(f"struct {odin_name(type_info)}")
case 18: # Type_Info_Parameters
return None
case 19: # Type_Info_Struct
return None
case 20: # Type_Info_Union
return None
case 21: # Type_Info_Enum
return None
case 22: # Type_Info_Map
return target.FindFirstType(f"struct {odin_name(type_info)}")
case 23: # Type_Info_Bit_Set
return None
case 24: # Type_Info_Simd_Vector
return None
case 25: # Type_Info_Matrix
return None
case 26: # Type_Info_Soa_Pointer
return None
case 27: # Type_Info_Bit_Field
return None
def odin_name(type_info):
target = type_info.GetTarget()
size = type_info.GetChildMemberWithName("size").GetValueAsSigned(0)
bit_size = size * 8
variant = type_info.GetChildMemberWithName("variant").GetNonSyntheticValue()
variant_tag = variant.GetChildMemberWithName("tag").GetValueAsUnsigned(0)
variant_data = variant.GetChildMemberWithName(f"v{variant_tag}")
match variant_tag:
case 1: # Type_Info_Named
name = read_string(variant_data.GetChildMemberWithName("name"))
pkg = read_string(variant_data.GetChildMemberWithName("pkg"))
return f"{pkg}.{name}"
case 2: # Type_Info_Integer
signed = variant_data.GetChildMemberWithName("signed").GetValueAsSigned(0) == 1
endianness = variant_data.GetChildMemberWithName("endianness") # TODO: care
return f"{"i" if signed else "u"}{bit_size}"
case 3: # Type_Info_Rune
return "rune"
case 4: # Type_Info_Float
endianness = variant_data.GetChildMemberWithName("endianness") # TODO: care
return f"f{bit_size}"
case 5: # Type_Info_Complex
return f"complex{bit_size}"
case 6: # Type_Info_Quaternion
return f"quaternion{bit_size}"
case 7: # Type_Info_String
return "string"
case 8: # Type_Info_Boolean
return f"b{bit_size}"
case 9: # Type_Info_Any
return "any"
case 10: # Type_Info_Type_Id
return "typeid"
case 11: # Type_Info_Pointer
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return f"^{odin_name(elem)}"
case 12: # Type_Info_Multi_Pointer
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return f"^{odin_name(elem)}"
case 13: # Type_Info_Procedure
return None
case 14: # Type_Info_Array
elem = variant_data.GetChildMemberWithName("elem").Dereference()
count = variant_data.GetChildMemberWithName("count").GetValueAsSigned(0)
return f"[{count}]{odin_name(elem)}"
case 15: # Type_Info_Enumerated_Array
elem = variant_data.GetChildMemberWithName("elem").Dereference()
count = variant_data.GetChildMemberWithName("count").GetValueAsSigned(0)
return f"[{count}]{odin_name(elem)}" # TODO: confirm that there is indeed no way to retrieve the enum name
case 16: # Type_Info_Dynamic_Array
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return f"[dynamic]{odin_name(elem)}"
case 17: # Type_Info_Slice
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return f"[]{odin_name(elem)}"
case 18: # Type_Info_Parameters
return None
case 19: # Type_Info_Struct
return None
case 20: # Type_Info_Union
return None
case 21: # Type_Info_Enum
return None
case 22: # Type_Info_Map
key = variant_data.GetChildMemberWithName("key").Dereference()
value = variant_data.GetChildMemberWithName("value").Dereference()
return f"[{odin_name(key)}]{odin_name(value)}"
case 23: # Type_Info_Bit_Set
return None
case 24: # Type_Info_Simd_Vector
elem = variant_data.GetChildMemberWithName("elem").Dereference()
count = variant_data.GetChildMemberWithName("count").GetValueAsSigned(0)
return f"#simd[{count}]{odin_name(elem)}"
case 25: # Type_Info_Matrix
#column_major matrix[2,3]f32
# TODO: row/col major tag
elem = variant_data.GetChildMemberWithName("elem").Dereference()
row_count = variant_data.GetChildMemberWithName("row_count").GetValueAsSigned(0)
col_count = variant_data.GetChildMemberWithName("column_count").GetValueAsSigned(0)
return f"matrix[{row_count}, {col_count}]{odin_name(elem)}"
case 26: # Type_Info_Soa_Pointer
elem = variant_data.GetChildMemberWithName("elem").Dereference()
return f"#soa ^{odin_name(elem)}"
return None
case 27: # Type_Info_Bit_Field
return None
# helpers ======================================================================
def read_string(odin_string):
pointer = odin_string.GetChildMemberWithName("data").GetValueAsUnsigned(0)
length = odin_string.GetChildMemberWithName("len").GetValueAsUnsigned(0)
if pointer == 0 or length == 0:
return ''
error = lldb.SBError()
content = odin_string.process.ReadMemory(pointer, length, error)
assert error.Success(), error
return content.decode("utf-8", "ignore")
@clankill3r
Copy link

Some notes that might help people (mainly aimed at using VSCode).

flga (the author of the OP) recommended me to use LLDB DAP instead of the CodeLLDB extension.

Then for mac users:

If you type lldb in the terminal followed by script import sys; print(sys.version) then it will report the python version:
Mine was:

3.9.6 (default, Nov 11 2024, 03:15:38)

And script import sys; print(sys.executable) told me:

/Applications/Xcode.app/Contents/Developer/usr/bin/lldb

So looks like lldb in xcode is using a kinda old python version in the sense that things like match, which is used in the odin.py script, won't work.

I struggled with PATH and so on, but in the end the only thing that worked for me was using the lldb-dap.executable-path setting:

settings.json

{
    "lldb-dap.executable-path": "/opt/homebrew/Cellar/llvm/19.1.6/bin/lldb-dap"
}

In the debug console you should see something like:

Running initCommands:
(lldb) command script import "/Users/doeke/Desktop/HSL Info Board/server_files/odin.py"

I do get a bunch of errors, but I'm 99% sure that that is because odin.py does not check against None.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment