mirror of
https://github.com/odin-lang/Odin.git
synced 2026-10-12 19:19:53 -04:00
497 lines
17 KiB
Python
497 lines
17 KiB
Python
# Pretty printers for Odin types in gdb and lldb
|
|
#
|
|
# gdb: source <odin>/base/runtime/odin_debugger.py
|
|
# lldb: command script import <odin>/base/runtime/odin_debugger.py
|
|
#
|
|
# gdb also loads it by itself from the `.debug_gdb_scripts` section of an ELF binary built with `-debug`,
|
|
# once that binary's directory is trusted, with `add-auto-load-safe-path <directory>`
|
|
#
|
|
# Shown: `string`, `string16`, slices, dynamic arrays (and fixed capacity ones), maps as their entries,
|
|
# and unions as the variant they hold, or `nil`.
|
|
# Types are recognised by their fields rather than their names, so named types, such as
|
|
# `Table :: map[string]int`, are shown the same way.
|
|
#
|
|
# A map's debug type does not describe its memory: `data` points to a struct whose fields give the
|
|
# `key`, `value` and `hash` types and how keys and values are packed into cells, `key_cell` and `value_cell`
|
|
# (see `init_map_internal_debug_types` in the compiler and `Raw_Map` in `base:runtime`).
|
|
# A union's members are its variants, `v<tag>`, and the `tag` itself, except for a `Maybe` of a pointer,
|
|
# which is `v0` alone and nil when the pointer is (see `lb_debug_union`).
|
|
# Every target Odin supports is little-endian.
|
|
|
|
import re
|
|
|
|
MAX_STRING_BYTES = 64*1024
|
|
|
|
MAP_METADATA_FIELDS = ["key", "value", "hash", "key_cell", "value_cell"]
|
|
|
|
|
|
def odin_kind(type_name, field_names, data_is_pointer, data_is_array, data_target_fields):
|
|
if field_names == ["data", "len"] and type_name in ("string", "string16"):
|
|
return type_name
|
|
if field_names == ["data", "len", "allocator"] and data_target_fields == MAP_METADATA_FIELDS:
|
|
return "map"
|
|
if field_names == ["data", "len", "cap", "allocator"] and data_is_pointer:
|
|
return "dynamic"
|
|
if field_names == ["data", "len"] and data_is_pointer:
|
|
return "slice"
|
|
if field_names == ["data", "len"] and data_is_array:
|
|
return "fixed"
|
|
return None
|
|
|
|
|
|
def is_odin_union(field_names):
|
|
variants = [n for n in field_names if n != "tag"]
|
|
return len(variants) != 0 and all(re.fullmatch(r"v[0-9]+", n) for n in variants)
|
|
|
|
|
|
def union_variant(field_names, tag):
|
|
# the member holding the union's value, None when it is nil; `tag` is None for a `Maybe` of a pointer
|
|
if tag is None:
|
|
return "v0"
|
|
name = "v%d" % tag
|
|
if name in field_names:
|
|
return name
|
|
if tag == 0:
|
|
return None
|
|
raise ValueError("invalid tag %d" % tag)
|
|
|
|
|
|
def cell_layout(elem_size, cell_size, cell_data_size):
|
|
# A cell is the element type itself when packing needs no padding,
|
|
# else `struct{v: [n]T, _: [padding]u8}`, where `cell_data_size` is the size of `v`
|
|
if cell_size == elem_size:
|
|
return elem_size, 1, 0
|
|
per_cell = cell_data_size // elem_size
|
|
if per_cell == 1:
|
|
return cell_size, 1, 0
|
|
return elem_size, per_cell, cell_size - per_cell*elem_size
|
|
|
|
|
|
def cell_address(base, layout, index):
|
|
stride, per_cell, padding = layout
|
|
return base + index*stride + (index // per_cell)*padding
|
|
|
|
|
|
def map_capacity(data, length):
|
|
if data == 0:
|
|
return 0
|
|
capacity = 1 << (data & 63)
|
|
if length <= 0 or length > capacity:
|
|
return 0
|
|
if capacity > 1 << 40:
|
|
return 0
|
|
return capacity
|
|
|
|
|
|
def map_slots(data, length, key_layout, value_layout, hash_size, read_memory):
|
|
capacity = map_capacity(data, length)
|
|
if capacity == 0:
|
|
return
|
|
keys = data & ~63
|
|
values = cell_address(keys, key_layout, capacity)
|
|
hashes = cell_address(values, value_layout, capacity)
|
|
tombstone = 1 << (8*hash_size - 1)
|
|
found = 0
|
|
chunk = 1024
|
|
for start in range(0, capacity, chunk):
|
|
count = min(chunk, capacity - start)
|
|
raw = read_memory(hashes + start*hash_size, count*hash_size)
|
|
for j in range(count):
|
|
h = int.from_bytes(raw[j*hash_size:(j+1)*hash_size], "little")
|
|
if h != 0 and h & tombstone == 0:
|
|
yield cell_address(keys, key_layout, start+j), cell_address(values, value_layout, start+j)
|
|
found += 1
|
|
if found == length:
|
|
return
|
|
|
|
|
|
def array_count(data, length, capacity=None):
|
|
if data == 0:
|
|
return 0
|
|
if length <= 0:
|
|
return 0
|
|
if capacity is not None and length > capacity:
|
|
return 0
|
|
return length
|
|
|
|
|
|
def read_text(read_memory, address, count, unit, encoding):
|
|
if address == 0 or count <= 0:
|
|
return ""
|
|
size = min(count*unit, MAX_STRING_BYTES)
|
|
text = read_memory(address, size).decode(encoding, "replace")
|
|
if size < count*unit:
|
|
text += "..."
|
|
return text
|
|
|
|
|
|
################################################################################
|
|
# gdb
|
|
|
|
try:
|
|
import gdb
|
|
except ImportError:
|
|
gdb = None
|
|
|
|
if gdb is not None:
|
|
def _gdb_read(address, size):
|
|
return bytes(gdb.selected_inferior().read_memory(address, size))
|
|
|
|
def _gdb_fields(t):
|
|
return {f.name: f.type for f in t.fields()}
|
|
|
|
def _gdb_kind(t):
|
|
if t.code != gdb.TYPE_CODE_STRUCT:
|
|
return None
|
|
names = [f.name for f in t.fields()]
|
|
if not names or names[0] != "data":
|
|
return None
|
|
data = t.fields()[0].type.strip_typedefs()
|
|
target_fields = []
|
|
if data.code == gdb.TYPE_CODE_PTR:
|
|
target = data.target().strip_typedefs()
|
|
if target.code == gdb.TYPE_CODE_STRUCT:
|
|
target_fields = [f.name for f in target.fields()]
|
|
return odin_kind(t.name, names, data.code == gdb.TYPE_CODE_PTR, data.code == gdb.TYPE_CODE_ARRAY, target_fields)
|
|
|
|
def _gdb_cell_layout(elem, cell):
|
|
cell_data_size = 0
|
|
if cell.sizeof != elem.sizeof:
|
|
cell_data_size = _gdb_fields(cell.strip_typedefs())["v"].sizeof
|
|
return cell_layout(elem.sizeof, cell.sizeof, cell_data_size)
|
|
|
|
class _GdbString:
|
|
def __init__(self, val, kind):
|
|
self.val = val
|
|
self.unit, self.encoding = (1, "utf-8") if kind == "string" else (2, "utf-16-le")
|
|
|
|
def to_string(self):
|
|
return read_text(_gdb_read, int(self.val["data"]), int(self.val["len"]), self.unit, self.encoding)
|
|
|
|
def display_hint(self):
|
|
return "string"
|
|
|
|
class _GdbArray:
|
|
def __init__(self, val, kind):
|
|
self.val = val
|
|
self.kind = kind
|
|
|
|
def to_string(self):
|
|
if self.kind == "dynamic":
|
|
return "len=%d cap=%d" % (int(self.val["len"]), int(self.val["cap"]))
|
|
return "len=%d" % int(self.val["len"])
|
|
|
|
def display_hint(self):
|
|
return "array"
|
|
|
|
def children(self):
|
|
data = self.val["data"]
|
|
length = int(self.val["len"])
|
|
if self.kind == "fixed":
|
|
capacity = data.type.strip_typedefs().range()[1] + 1
|
|
for i in range(array_count(1, length, capacity)):
|
|
yield "[%d]" % i, data[i]
|
|
return
|
|
capacity = int(self.val["cap"]) if self.kind == "dynamic" else None
|
|
for i in range(array_count(int(data), length, capacity)):
|
|
yield "[%d]" % i, (data + i).dereference()
|
|
|
|
class _GdbMap:
|
|
def __init__(self, val):
|
|
self.val = val
|
|
f = _gdb_fields(val["data"].type.strip_typedefs().target().strip_typedefs())
|
|
self.key_type = f["key"]
|
|
self.value_type = f["value"]
|
|
self.key_layout = _gdb_cell_layout(f["key"], f["key_cell"])
|
|
self.value_layout = _gdb_cell_layout(f["value"], f["value_cell"])
|
|
self.hash_size = f["hash"].sizeof
|
|
|
|
def to_string(self):
|
|
return "len=%d" % int(self.val["len"])
|
|
|
|
def display_hint(self):
|
|
return "map"
|
|
|
|
def children(self):
|
|
slots = map_slots(int(self.val["data"]), int(self.val["len"]), self.key_layout, self.value_layout, self.hash_size, _gdb_read)
|
|
for key, value in slots:
|
|
yield "key", gdb.Value(key).cast(self.key_type.pointer()).dereference()
|
|
yield "value", gdb.Value(value).cast(self.value_type.pointer()).dereference()
|
|
|
|
class _GdbUnion:
|
|
def __init__(self, val):
|
|
self.val = val
|
|
|
|
def to_string(self):
|
|
names = [f.name for f in self.val.type.strip_typedefs().fields()]
|
|
tag = int(self.val["tag"]) if "tag" in names else None
|
|
try:
|
|
name = union_variant(names, tag)
|
|
except ValueError as e:
|
|
return "<%s>" % e
|
|
if name is None or (tag is None and int(self.val[name]) == 0):
|
|
return "nil"
|
|
return self.val[name]
|
|
|
|
def _gdb_lookup(val):
|
|
t = val.type.strip_typedefs()
|
|
if t.code == gdb.TYPE_CODE_UNION and is_odin_union([f.name for f in t.fields()]):
|
|
return _GdbUnion(val)
|
|
kind = _gdb_kind(t)
|
|
if kind in ("string", "string16"):
|
|
return _GdbString(val, kind)
|
|
if kind == "map":
|
|
return _GdbMap(val)
|
|
if kind is not None:
|
|
return _GdbArray(val, kind)
|
|
return None
|
|
|
|
# NOTE: loaded from a binary's `.debug_gdb_scripts` section, the printers belong to that binary
|
|
if gdb.current_objfile() is not None:
|
|
gdb.current_objfile().pretty_printers.append(_gdb_lookup)
|
|
else:
|
|
gdb.pretty_printers.append(_gdb_lookup)
|
|
|
|
|
|
################################################################################
|
|
# lldb
|
|
|
|
try:
|
|
import lldb
|
|
except ImportError:
|
|
lldb = None
|
|
|
|
if lldb is not None:
|
|
def _lldb_kind(sbtype):
|
|
t = sbtype.GetCanonicalType()
|
|
if t.GetTypeClass() not in (lldb.eTypeClassStruct, lldb.eTypeClassClass):
|
|
return None
|
|
names = [t.GetFieldAtIndex(i).GetName() for i in range(t.GetNumberOfFields())]
|
|
if not names or names[0] != "data":
|
|
return None
|
|
data = t.GetFieldAtIndex(0).GetType().GetCanonicalType()
|
|
target_fields = []
|
|
if data.IsPointerType():
|
|
target = data.GetPointeeType().GetCanonicalType()
|
|
target_fields = [target.GetFieldAtIndex(i).GetName() for i in range(target.GetNumberOfFields())]
|
|
return odin_kind(t.GetName(), names, data.IsPointerType(), data.IsArrayType(), target_fields)
|
|
|
|
def _lldb_reader(valobj):
|
|
process = valobj.GetProcess()
|
|
def read(address, size):
|
|
error = lldb.SBError()
|
|
data = process.ReadMemory(address, size, error)
|
|
if error.Fail():
|
|
raise MemoryError(error.GetCString())
|
|
return data
|
|
return read
|
|
|
|
def _lldb_fields(t):
|
|
t = t.GetCanonicalType()
|
|
return {t.GetFieldAtIndex(i).GetName(): t.GetFieldAtIndex(i).GetType() for i in range(t.GetNumberOfFields())}
|
|
|
|
def _lldb_cell_layout(elem, cell):
|
|
cell_data_size = 0
|
|
if cell.GetByteSize() != elem.GetByteSize():
|
|
cell_data_size = _lldb_fields(cell)["v"].GetByteSize()
|
|
return cell_layout(elem.GetByteSize(), cell.GetByteSize(), cell_data_size)
|
|
|
|
def lldb_is_string(sbtype, internal_dict):
|
|
return _lldb_kind(sbtype) in ("string", "string16")
|
|
|
|
def lldb_is_array(sbtype, internal_dict):
|
|
return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed")
|
|
|
|
def lldb_is_map(sbtype, internal_dict):
|
|
return _lldb_kind(sbtype) == "map"
|
|
|
|
def lldb_is_container(sbtype, internal_dict):
|
|
return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed", "map")
|
|
|
|
def lldb_is_union(sbtype, internal_dict):
|
|
t = sbtype.GetCanonicalType()
|
|
return t.GetTypeClass() == lldb.eTypeClassUnion and is_odin_union(list(_lldb_fields(t)))
|
|
|
|
def _lldb_union_value(valobj):
|
|
# the variant the union holds, None when it is nil; raises ValueError for an invalid tag
|
|
v = valobj.GetNonSyntheticValue()
|
|
names = list(_lldb_fields(v.GetType()))
|
|
tag = v.GetChildMemberWithName("tag").GetValueAsUnsigned() if "tag" in names else None
|
|
name = union_variant(names, tag)
|
|
if name is None:
|
|
return None
|
|
value = v.GetChildMemberWithName(name)
|
|
if tag is None and value.GetValueAsUnsigned() == 0:
|
|
return None
|
|
value.SetPreferSyntheticValue(True)
|
|
return value
|
|
|
|
def lldb_union_summary(valobj, internal_dict):
|
|
try:
|
|
value = _lldb_union_value(valobj)
|
|
except ValueError as e:
|
|
return "<%s>" % e
|
|
if value is None:
|
|
return "nil"
|
|
# NOTE: a struct has neither, so it is shown by its fields, the union's children
|
|
return value.GetSummary() or value.GetValue() or ""
|
|
|
|
def lldb_string_summary(valobj, internal_dict):
|
|
v = valobj.GetNonSyntheticValue()
|
|
unit, encoding = (1, "utf-8") if _lldb_kind(v.GetType()) == "string" else (2, "utf-16-le")
|
|
data = v.GetChildMemberWithName("data").GetValueAsUnsigned()
|
|
count = v.GetChildMemberWithName("len").GetValueAsSigned()
|
|
try:
|
|
text = read_text(_lldb_reader(v), data, count, unit, encoding)
|
|
except MemoryError:
|
|
return "<invalid>"
|
|
return '"%s"' % text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
|
|
def lldb_len_summary(valobj, internal_dict):
|
|
v = valobj.GetNonSyntheticValue()
|
|
summary = "len=%d" % v.GetChildMemberWithName("len").GetValueAsSigned()
|
|
if _lldb_kind(v.GetType()) == "dynamic":
|
|
summary += " cap=%d" % v.GetChildMemberWithName("cap").GetValueAsSigned()
|
|
return summary
|
|
|
|
class LldbArraySynth:
|
|
def __init__(self, valobj, internal_dict):
|
|
self.valobj = valobj
|
|
|
|
def update(self):
|
|
data = self.valobj.GetChildMemberWithName("data")
|
|
t = data.GetType().GetCanonicalType()
|
|
length = self.valobj.GetChildMemberWithName("len").GetValueAsSigned()
|
|
if t.IsArrayType():
|
|
self.elem = t.GetArrayElementType()
|
|
self.address = data.GetLoadAddress()
|
|
capacity = t.GetByteSize() // max(1, self.elem.GetByteSize())
|
|
self.count = array_count(1, length, capacity)
|
|
else:
|
|
self.elem = t.GetPointeeType()
|
|
self.address = data.GetValueAsUnsigned()
|
|
capacity = None
|
|
if self.valobj.GetChildMemberWithName("cap").IsValid():
|
|
capacity = self.valobj.GetChildMemberWithName("cap").GetValueAsSigned()
|
|
self.count = array_count(self.address, length, capacity)
|
|
return False
|
|
|
|
def num_children(self):
|
|
return self.count
|
|
|
|
def get_child_index(self, name):
|
|
try:
|
|
return int(name.strip("[]"))
|
|
except ValueError:
|
|
return -1
|
|
|
|
def get_child_at_index(self, index):
|
|
return self.valobj.CreateValueFromAddress("[%d]" % index, self.address + index*self.elem.GetByteSize(), self.elem)
|
|
|
|
def has_children(self):
|
|
return True
|
|
|
|
class LldbMapSynth:
|
|
# Each entry is a child named by its key; a key without a short form, such as a struct,
|
|
# gives two children per entry instead, `[i].key` and `[i].value`
|
|
def __init__(self, valobj, internal_dict):
|
|
self.valobj = valobj
|
|
|
|
def update(self):
|
|
data = self.valobj.GetChildMemberWithName("data")
|
|
f = _lldb_fields(data.GetType().GetCanonicalType().GetPointeeType())
|
|
self.key_type = f["key"]
|
|
self.value_type = f["value"]
|
|
key_layout = _lldb_cell_layout(f["key"], f["key_cell"])
|
|
value_layout = _lldb_cell_layout(f["value"], f["value_cell"])
|
|
raw = data.GetValueAsUnsigned()
|
|
self.count = self.valobj.GetChildMemberWithName("len").GetValueAsSigned()
|
|
if map_capacity(raw, self.count) == 0:
|
|
self.count = 0
|
|
self.slots = []
|
|
self.scan = map_slots(raw, self.count, key_layout, value_layout, f["hash"].GetByteSize(), _lldb_reader(self.valobj))
|
|
key = self.key_type.GetCanonicalType()
|
|
self.pairs = not (key.GetTypeClass() in (lldb.eTypeClassBuiltin, lldb.eTypeClassEnumeration, lldb.eTypeClassPointer) or lldb_is_string(key, None))
|
|
return False
|
|
|
|
def _slot(self, entry):
|
|
while len(self.slots) <= entry:
|
|
try:
|
|
self.slots.append(next(self.scan))
|
|
except (StopIteration, MemoryError):
|
|
return None
|
|
return self.slots[entry]
|
|
|
|
def num_children(self):
|
|
return 2*self.count if self.pairs else self.count
|
|
|
|
def get_child_index(self, name):
|
|
return -1
|
|
|
|
def get_child_at_index(self, index):
|
|
entry = index//2 if self.pairs else index
|
|
slot = self._slot(entry)
|
|
if slot is None:
|
|
return None
|
|
key, value = slot
|
|
if self.pairs:
|
|
if index % 2 == 0:
|
|
return self.valobj.CreateValueFromAddress("[%d].key" % entry, key, self.key_type)
|
|
return self.valobj.CreateValueFromAddress("[%d].value" % entry, value, self.value_type)
|
|
k = self.valobj.CreateValueFromAddress("key", key, self.key_type)
|
|
text = k.GetSummary() or k.GetValue() or str(entry)
|
|
return self.valobj.CreateValueFromAddress("[%s]" % text, value, self.value_type)
|
|
|
|
def has_children(self):
|
|
return True
|
|
|
|
class LldbUnionSynth:
|
|
# The children of the variant the union holds
|
|
def __init__(self, valobj, internal_dict):
|
|
self.valobj = valobj
|
|
|
|
def update(self):
|
|
try:
|
|
self.value = _lldb_union_value(self.valobj)
|
|
except ValueError:
|
|
self.value = None
|
|
if self.value is not None and self.value.GetSummary() and not self.value.IsSynthetic():
|
|
# a string, which its summary shows whole
|
|
self.value = None
|
|
return False
|
|
|
|
def num_children(self):
|
|
return self.value.GetNumChildren() if self.value is not None else 0
|
|
|
|
def get_child_index(self, name):
|
|
return self.value.GetIndexOfChildWithName(name) if self.value is not None else -1
|
|
|
|
def get_child_at_index(self, index):
|
|
return self.value.GetChildAtIndex(index)
|
|
|
|
def has_children(self):
|
|
return self.value is not None and self.value.MightHaveChildren()
|
|
|
|
def __lldb_init_module(debugger, internal_dict):
|
|
# NOTE: commands go through the interpreter with a result object, as `SBDebugger.HandleCommand` writes to
|
|
# the debugger's output, which lldb-dap uses for its protocol, so editors using it lose the session.
|
|
# `--recognizer-function` needs LLDB 15; before it, types are matched by their names, so named types are not.
|
|
m = __name__
|
|
rules = [
|
|
("type summary add -w odin -F %s.lldb_string_summary" % m, "lldb_is_string", "^string(16)?$"),
|
|
("type summary add -w odin -e -F %s.lldb_len_summary" % m, "lldb_is_container", "^([[][]]|[[]dynamic|map[[])"),
|
|
("type synthetic add -w odin -l %s.LldbArraySynth" % m, "lldb_is_array", "^([[][]]|[[]dynamic)"),
|
|
("type synthetic add -w odin -l %s.LldbMapSynth" % m, "lldb_is_map", "^map[[]"),
|
|
("type summary add -w odin -e -F %s.lldb_union_summary" % m, "lldb_is_union", "^union[{]"),
|
|
("type synthetic add -w odin -l %s.LldbUnionSynth" % m, "lldb_is_union", "^union[{]"),
|
|
]
|
|
interpreter = debugger.GetCommandInterpreter()
|
|
def run(command):
|
|
result = lldb.SBCommandReturnObject()
|
|
interpreter.HandleCommand(command, result)
|
|
return result.Succeeded()
|
|
for command, recognizer, regex in rules:
|
|
if not run("%s --recognizer-function %s.%s" % (command, m, recognizer)):
|
|
run('%s -x "%s"' % (command, regex))
|
|
run("type category enable odin")
|