## Description Contribution to this topic [Rich Disassembler for LLDB](https://discourse.llvm.org/t/rich-disassembler-for-lldb/76952), this part. ``` The rich disassembler output should be exposed as structured data and made available through LLDB’s scripting API so more tooling could be built on top of this ``` ---- This pr replaces #174847 As was suggested in [this comment](https://github.com/llvm/llvm-project/pull/174847#issuecomment-3757015552), implement access to variable annotations from `SBInstruction` class itself. Notes: - ✅ did run black formatter on the python file; ## Testing Run test with ```sh ./build/bin/lldb-dotest -v -p TestVariableAnnotationsDisassembler.py lldb/test/API/functionalities/disassembler-variables ``` all tests (9 existing + 1 newly added) are passing <details> <summary>screenshot 2026-01-23</summary> build from the latest commit 08f00730b5768a8e3f7039d810084fabaaa24470 <img width="1506" height="562" alt="image" src="https://github.com/user-attachments/assets/69516353-3432-47df-ae45-c40b51ec14c4" /> </details> <details> <summary>screenshot 2026-01-29</summary> build from the latest commit f48a1a2c10f96a457ca6844be2ccc9406d3d57a0 <img width="1232" height="740" alt="image" src="https://github.com/user-attachments/assets/9d104ce6-36c3-430b-98fe-f028f83a6b6d" /> </details> --------- Signed-off-by: Nikita B <n2h9z4@gmail.com>
216 lines
8.9 KiB
Python
216 lines
8.9 KiB
Python
from lldbsuite.test.lldbtest import *
|
||
from lldbsuite.test.decorators import *
|
||
import lldb
|
||
import os
|
||
import re
|
||
|
||
|
||
# Requires ELF assembler directives (.section … @progbits, .ident, etc.);
|
||
# not compatible with COFF/Mach-O toolchains.
|
||
@skipUnlessPlatform(["linux", "android", "freebsd", "netbsd"])
|
||
class TestVariableAnnotationsDisassembler(TestBase):
|
||
def _build_obj(self, obj_name: str) -> str:
|
||
# Let the Makefile build all .o’s (pattern rule). Then grab the one we need.
|
||
self.build()
|
||
obj = self.getBuildArtifact(obj_name)
|
||
self.assertTrue(os.path.exists(obj), f"missing object: {obj}")
|
||
return obj
|
||
|
||
def _create_target(self, path):
|
||
target = self.dbg.CreateTarget(path)
|
||
self.assertTrue(target, f"failed to create target for {path}")
|
||
return target
|
||
|
||
def _disassemble_verbose_symbol(self, symname):
|
||
self.runCmd(f"disassemble -n {symname} -v", check=True)
|
||
return self.res.GetOutput()
|
||
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_d_original_example_O1(self):
|
||
obj = self._build_obj("d_original_example.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("main")
|
||
print(out)
|
||
self.assertIn("argc = ", out)
|
||
self.assertIn("argv = ", out)
|
||
self.assertIn("i = ", out)
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_regs_int_params(self):
|
||
obj = self._build_obj("regs_int_params.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("regs_int_params")
|
||
print(out)
|
||
self.assertRegex(out, r"\ba\s*=\s*(DW_OP_reg5\b|RDI\b)")
|
||
self.assertRegex(out, r"\bb\s*=\s*(DW_OP_reg4\b|RSI\b)")
|
||
self.assertRegex(out, r"\bc\s*=\s*(DW_OP_reg1\b|RDX\b)")
|
||
self.assertRegex(out, r"\bd\s*=\s*(DW_OP_reg2\b|RCX\b)")
|
||
self.assertRegex(out, r"\be\s*=\s*(DW_OP_reg8\b|R8\b)")
|
||
self.assertRegex(out, r"\bf\s*=\s*(DW_OP_reg9\b|R9\b)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_regs_fp_params(self):
|
||
obj = self._build_obj("regs_fp_params.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("regs_fp_params")
|
||
print(out)
|
||
self.assertRegex(out, r"\ba\s*=\s*(DW_OP_reg17\b|XMM0\b)")
|
||
self.assertRegex(out, r"\bb\s*=\s*(DW_OP_reg18\b|XMM1\b)")
|
||
self.assertRegex(out, r"\bc\s*=\s*(DW_OP_reg19\b|XMM2\b)")
|
||
self.assertRegex(out, r"\bd\s*=\s*(DW_OP_reg20\b|XMM3\b)")
|
||
self.assertRegex(out, r"\be\s*=\s*(DW_OP_reg21\b|XMM4\b)")
|
||
self.assertRegex(out, r"\bf\s*=\s*(DW_OP_reg22\b|XMM5\b)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_regs_mixed_params(self):
|
||
obj = self._build_obj("regs_mixed_params.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("regs_mixed_params")
|
||
print(out)
|
||
self.assertRegex(out, r"\ba\s*=\s*(DW_OP_reg5\b|RDI\b)")
|
||
self.assertRegex(out, r"\bb\s*=\s*(DW_OP_reg4\b|RSI\b)")
|
||
self.assertRegex(out, r"\bx\s*=\s*(DW_OP_reg17\b|XMM0\b|DW_OP_reg\d+\b)")
|
||
self.assertRegex(out, r"\by\s*=\s*(DW_OP_reg18\b|XMM1\b|DW_OP_reg\d+\b)")
|
||
self.assertRegex(out, r"\bc\s*=\s*(DW_OP_reg1\b|RDX\b)")
|
||
self.assertRegex(out, r"\bz\s*=\s*(DW_OP_reg19\b|XMM2\b|DW_OP_reg\d+\b)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_live_across_call(self):
|
||
obj = self._build_obj("live_across_call.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("live_across_call")
|
||
print(out)
|
||
self.assertRegex(out, r"\bx\s*=\s*(DW_OP_reg5\b|RDI\b)")
|
||
self.assertIn("call", out)
|
||
self.assertRegex(out, r"\br\s*=\s*(DW_OP_reg0\b|RAX\b|DW_OP_reg\d+\b)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_loop_reg_rotate(self):
|
||
obj = self._build_obj("loop_reg_rotate.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("loop_reg_rotate")
|
||
print(out)
|
||
self.assertRegex(out, r"\bn\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertRegex(out, r"\bseed\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertRegex(out, r"\bk\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertRegex(out, r"\bj\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertRegex(out, r"\bi\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_seed_reg_const_undef(self):
|
||
obj = self._build_obj("seed_reg_const_undef.o")
|
||
target = self._create_target(obj)
|
||
out = self._disassemble_verbose_symbol("main")
|
||
print(out)
|
||
self.assertRegex(out, r"\b(i|argc)\s*=\s*(DW_OP_reg\d+\b|R[A-Z0-9]+)")
|
||
self.assertNotIn("<decoding error>", out)
|
||
|
||
@no_debug_info_test
|
||
@skipIf(archs=no_match(["x86_64"]))
|
||
def test_structured_annotations_api(self):
|
||
"""Test SBInstruction.variable_annotations() Python API."""
|
||
obj = self._build_obj("d_original_example.o")
|
||
target = self._create_target(obj)
|
||
|
||
main_symbols = target.FindSymbols("main")
|
||
self.assertTrue(
|
||
main_symbols.IsValid() and main_symbols.GetSize() > 0,
|
||
"Could not find 'main' symbol",
|
||
)
|
||
|
||
main_symbol = main_symbols.GetContextAtIndex(0).GetSymbol()
|
||
start_addr = main_symbol.GetStartAddress()
|
||
self.assertTrue(start_addr.IsValid(), "Invalid start address for main")
|
||
|
||
instructions = target.ReadInstructions(start_addr, 16)
|
||
self.assertGreater(instructions.GetSize(), 0, "No instructions read")
|
||
|
||
if self.TraceOn():
|
||
print(
|
||
f"\nTesting SBInstruction.variable_annotations on {instructions.GetSize()} instructions"
|
||
)
|
||
|
||
expected_vars = ["argc", "argv", "i"]
|
||
|
||
# Track current state of variables across instructions.
|
||
found_variables = set()
|
||
|
||
# Test each instruction.
|
||
for i in range(instructions.GetSize()):
|
||
inst = instructions.GetInstructionAtIndex(i)
|
||
self.assertTrue(inst.IsValid(), f"Invalid instruction at index {i}")
|
||
|
||
# Get annotations as Python list of dicts.
|
||
annotations = inst.variable_annotations()
|
||
|
||
for ann in annotations:
|
||
# Validate required fields are present.
|
||
self.assertIn("variable_name", ann, "Missing 'variable_name' field")
|
||
self.assertIn(
|
||
"location_description", ann, "Missing 'location_description' field"
|
||
)
|
||
self.assertIn("start_address", ann, "Missing 'start_address' field")
|
||
self.assertIn("end_address", ann, "Missing 'end_address' field")
|
||
self.assertIn("register_kind", ann, "Missing 'register_kind' field")
|
||
|
||
var_name = ann["variable_name"]
|
||
|
||
# Validate types and values.
|
||
self.assertIsInstance(var_name, str, "variable_name should be string")
|
||
self.assertIsInstance(
|
||
ann["location_description"],
|
||
str,
|
||
"location_description should be string",
|
||
)
|
||
self.assertIsInstance(
|
||
ann["start_address"], int, "start_address should be integer"
|
||
)
|
||
self.assertIsInstance(
|
||
ann["end_address"], int, "end_address should be integer"
|
||
)
|
||
self.assertIsInstance(
|
||
ann["register_kind"], int, "register_kind should be integer"
|
||
)
|
||
|
||
self.assertGreater(
|
||
len(var_name), 0, "variable_name should not be empty"
|
||
)
|
||
self.assertGreater(
|
||
len(ann["location_description"]),
|
||
0,
|
||
"location_description should not be empty",
|
||
)
|
||
self.assertGreater(
|
||
ann["end_address"],
|
||
ann["start_address"],
|
||
"end_address should be > start_address",
|
||
)
|
||
|
||
self.assertIn(
|
||
var_name, expected_vars, f"Unexpected variable name: {var_name}"
|
||
)
|
||
|
||
found_variables.add(var_name)
|
||
|
||
# Validate we find all expected variables.
|
||
self.assertEqual(
|
||
found_variables,
|
||
set(expected_vars),
|
||
f"Did not find all expected variables. Expected: {expected_vars}, find: {found_variables}",
|
||
)
|
||
|
||
if self.TraceOn():
|
||
print(f"\nTest complete. All expected variables found: {found_variables}")
|