| import lldb | |
| import os | |
| import binascii | |
| from lldbsuite.test.lldbtest import * | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.gdbclientutils import * | |
| from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase | |
| # Ids of the instances the fake stub loads. An id is written in base 10, both | |
| # in a packet and in the library list it is reported through, so these read | |
| # differently in base 16, which a single digit would not. | |
| MODULE_ID = 16 | |
| SECOND_MODULE_ID = 26 | |
| # Ids of the instances the fake stub loads in place of the others when it | |
| # reloads them. A reloaded module is a new instance, with an id of its own. | |
| RELOADED_MODULE_ID = 36 | |
| RELOADED_SECOND_MODULE_ID = 46 | |
| # The address spaces an address can point into, in the bits above the id of the | |
| # instance the address belongs to. The object space holds the module image, so | |
| # that is the space a module is loaded in. | |
| WASM_OBJECT_ADDRESS = 1 << 62 | |
| WASM_GLOBAL_ADDRESS = 2 << 62 | |
| WASM_ID_MASK = 0x3FFFFFFF | |
| LOAD_ADDRESS = WASM_OBJECT_ADDRESS | (MODULE_ID << 32) | |
| WASM_LOCAL_ADDR = 0x103E0 | |
| # The key under which a packet names the module instance whose state it reads. | |
| # The key form is also what tells the two shapes of qWasmGlobal apart, since the | |
| # first field is a global index in one and a frame index in the other. | |
| INSTANCE_KEY = "instance:" | |
| # Globals the fake stub holds, as index -> (size in bytes, value). Every | |
| # instance has an index space of its own, so these are the globals of one | |
| # instance and the same index names a different global in another. An index is | |
| # written in base 10 like an instance id, so one of them reads differently in | |
| # base 16. | |
| WASM_GLOBALS = {0: (4, 0x2A), 1: (8, 0xDEADBEEF), 26: (4, 0x33)} | |
| SECOND_WASM_GLOBALS = {0: (4, 0x1234), 1: (8, 0xFEEDFACE)} | |
| # Bytes of the fake stack frame a frame base points at, holding the values of the | |
| # parameters of "add" at the offsets its DWARF gives them, and above them those | |
| # of the variables of "main". A Wasm stack grows down, so the frame of a caller | |
| # sits above the frame of what it called. | |
| WASM_FRAME_BYTES = bytes.fromhex( | |
| "0000000000000000020000000100000000000000020000000100000000000000" | |
| ) | |
| WASM_CALLER_FRAME_ADDR = WASM_LOCAL_ADDR + 16 | |
| # The globals simple_global_frame_base.yaml gives the two functions of the module | |
| # as their frame base. Each function is based on a global of its own, so a read | |
| # of the wrong one does not land in the frame it belongs to. | |
| INNER_FRAME_BASE_GLOBAL_INDEX = 0 | |
| OUTER_FRAME_BASE_GLOBAL_INDEX = 1 | |
| FRAME_BASE_GLOBALS = { | |
| INNER_FRAME_BASE_GLOBAL_INDEX: (4, WASM_LOCAL_ADDR), | |
| OUTER_FRAME_BASE_GLOBAL_INDEX: (4, WASM_CALLER_FRAME_ADDR), | |
| } | |
| class WasmModule: | |
| """ | |
| A module the fake stub has loaded, together with the globals held by the | |
| instance it was loaded as. | |
| """ | |
| def __init__(self, obj_path, name, module_id=MODULE_ID, global_values=None): | |
| self.obj_path = obj_path | |
| self.name = name | |
| self.module_id = module_id | |
| self.load_address = WASM_OBJECT_ADDRESS | (module_id << 32) | |
| self.global_values = WASM_GLOBALS if global_values is None else global_values | |
| self._image = None | |
| def get_image(self): | |
| """ | |
| The bytes of the module itself, which is what the stub serves in the | |
| range the module is loaded at. | |
| """ | |
| if self._image is None: | |
| with open(self.obj_path, mode="rb") as file: | |
| self._image = file.read() | |
| return self._image | |
| def encode_global(self, global_index): | |
| """ | |
| Encode the global at the given index, or an error when this instance | |
| holds no such global. A global is transferred as a whole value, in | |
| little-endian order. | |
| """ | |
| value = self.global_values.get(global_index) | |
| if value is None: | |
| return "E03" | |
| size, val = value | |
| return val.to_bytes(size, "little").hex() | |
| def global_read_packet(global_index, module_id=MODULE_ID): | |
| """ | |
| The packet that reads a global from the module instance holding it. | |
| """ | |
| return f"qWasmGlobal:{global_index};{INSTANCE_KEY}{module_id};" | |
| def format_register_value(val): | |
| """ | |
| Encode each byte by two hex digits in little-endian order. | |
| """ | |
| result = "" | |
| mask = 0xFF | |
| shift = 0 | |
| for i in range(0, 8): | |
| x = (val & mask) >> shift | |
| result += format(x, "02x") | |
| mask <<= 8 | |
| shift += 8 | |
| return result | |
| class WasmStackFrame: | |
| def __init__(self, address, load_address=LOAD_ADDRESS): | |
| self._address = address | |
| self._load_address = load_address | |
| def __str__(self): | |
| return format_register_value(self._load_address | self._address) | |
| class WasmCallStack: | |
| def __init__(self, wasm_stack_frames): | |
| self._wasm_stack_frames = wasm_stack_frames | |
| def __len__(self): | |
| return len(self._wasm_stack_frames) | |
| def __str__(self): | |
| result = "" | |
| for frame in self._wasm_stack_frames: | |
| result += str(frame) | |
| return result | |
| class FakeMemory: | |
| def __init__(self, start_addr, end_addr): | |
| self._base_addr = start_addr | |
| self._memory = bytearray(end_addr - start_addr) | |
| self._memoryview = memoryview(self._memory) | |
| def store_bytes(self, addr, bytes_obj): | |
| assert addr > self._base_addr | |
| assert addr < self._base_addr + len(self._memoryview) | |
| offset = addr - self._base_addr | |
| chunk = self._memoryview[offset : offset + len(bytes_obj)] | |
| for i in range(len(bytes_obj)): | |
| chunk[i] = bytes_obj[i] | |
| def get_bytes(self, addr, length): | |
| assert addr > self._base_addr | |
| assert addr < self._base_addr + len(self._memoryview) | |
| offset = addr - self._base_addr | |
| return self._memoryview[offset : offset + length] | |
| def contains(self, addr): | |
| return addr - self._base_addr < len(self._memoryview) | |
| class MyResponder(MockGDBServerResponder): | |
| current_pc = LOAD_ADDRESS | 0x01AD | |
| def __init__( | |
| self, | |
| modules, | |
| wasm_call_stacks=[], | |
| memory=None, | |
| supports_instance=True, | |
| ): | |
| self._modules = modules | |
| self._wasm_call_stacks = wasm_call_stacks | |
| self._call_stack_request_count = 0 | |
| self._reported_frames = 0 | |
| self._memory = memory | |
| self._supports_instance = supports_instance | |
| MockGDBServerResponder.__init__(self) | |
| def other(self, packet): | |
| if packet.startswith("qWasmCallStack"): | |
| return self.qWasmCallStack() | |
| if packet.startswith("qWasmLocal"): | |
| return self.qWasmLocal(packet) | |
| if packet.startswith("qWasmGlobal"): | |
| return self.qWasmGlobal(packet) | |
| return MockGDBServerResponder.other(self, packet) | |
| def module_with_id(self, module_id): | |
| """ | |
| The module the stub loaded with the given id, if it loaded one. Nothing | |
| about a module is shared with another, so an id that names none has no | |
| code to read and no globals to hand out. | |
| """ | |
| for module in self._modules: | |
| if module.module_id == module_id: | |
| return module | |
| return None | |
| def qWasmGlobal(self, packet): | |
| """ | |
| Read a global. A client that can name the instance holding it does so | |
| with the instance suffix, and one that cannot names a frame. | |
| Format: qWasmGlobal:index;instance:id; or | |
| qWasmGlobal:frame_index;index | |
| """ | |
| first, _, rest = packet.split(":", 1)[1].partition(";") | |
| if rest.startswith(INSTANCE_KEY): | |
| if not self._supports_instance: | |
| # A stub is only asked for what it advertised. | |
| return "E05" | |
| # The global index space belongs to the instance, so an index only | |
| # names a global together with the instance it indexes. Another | |
| # instance's globals answer a different question. | |
| module = self.module_with_id(int(rest[len(INSTANCE_KEY) :].rstrip(";"))) | |
| if module is None: | |
| return "E04" | |
| return module.encode_global(int(first)) | |
| if self._supports_instance: | |
| # A frame cannot name the instance whose globals are indexed, so a | |
| # stub that can be given an instance is never given a frame. | |
| return "E05" | |
| # A frame index that names none of the frames the stub reported is no | |
| # scope to read a global through. | |
| if int(first) >= self._reported_frames: | |
| return "E06" | |
| # A frame stands in for the instance it is executing, which with a single | |
| # loaded module is that module. | |
| return self._modules[0].encode_global(int(rest)) | |
| def qSupported(self, client_supported): | |
| response = "qXfer:libraries:read+;PacketSize=1000;vContSupported-" | |
| if self._supports_instance: | |
| response += ";qWasmInstance+" | |
| return response | |
| def qHostInfo(self): | |
| return "" | |
| def QEnableErrorStrings(self): | |
| return "" | |
| def qRegisterInfo(self, index): | |
| if index == 0: | |
| return "name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:General Purpose Registers;gcc:16;dwarf:16;generic:pc;" | |
| return "E45" | |
| def qProcessInfo(self): | |
| return "pid:1;ppid:1;uid:1;gid:1;euid:1;egid:1;name:%s;triple:%s;ptrsize:4" % ( | |
| hex_encode_bytes("lldb"), | |
| hex_encode_bytes("wasm32-unknown-unknown-wasm"), | |
| ) | |
| def haltReason(self): | |
| return "T02thread:1;" | |
| def readRegister(self, register): | |
| return format_register_value(self.current_pc) | |
| def qXferRead(self, obj, annex, offset, length): | |
| if obj == "libraries": | |
| libraries = "".join( | |
| '<library name="%s"><section address="%d"/></library>' | |
| % (module.name, module.load_address) | |
| for module in self._modules | |
| ) | |
| return "<library-list>" + libraries + "</library-list>", False | |
| else: | |
| return None, False | |
| def readMemory(self, addr, length): | |
| if self._memory and self._memory.contains(addr): | |
| chunk = self._memory.get_bytes(addr, length) | |
| return chunk.hex() | |
| # A module is loaded in the object space of its instance, so an address | |
| # in that space is what asks for the module image, and the id the address | |
| # carries picks the instance whose module answers. | |
| if addr >> 62 != WASM_OBJECT_ADDRESS >> 62: | |
| return "E02" | |
| module = self.module_with_id((addr >> 32) & WASM_ID_MASK) | |
| if module is None: | |
| return "E02" | |
| image = module.get_image() | |
| offset = addr - module.load_address | |
| if offset >= len(image): | |
| return "E03" | |
| end = offset + min(length, len(image) - offset) | |
| return image[offset:end].hex() | |
| def setBreakpoint(self, packet): | |
| bp_data = packet[1:].split(",") | |
| self._bp_address = bp_data[1] | |
| return "OK" | |
| def qfThreadInfo(self): | |
| return "m1" | |
| def cont(self): | |
| # Continue execution. Simulates running the Wasm stub until a breakpoint is hit. | |
| # A program counter names the space and the instance it belongs to, as | |
| # the address the breakpoint was set at does. | |
| return "T05thread-pcs:" + self._bp_address + ";thread:1" | |
| def qWasmCallStack(self): | |
| if len(self._wasm_call_stacks) == 0: | |
| return "" | |
| call_stack = self._wasm_call_stacks[self._call_stack_request_count] | |
| self._call_stack_request_count += 1 | |
| # A frame is only a scope the stub can be asked about once it has | |
| # reported it. | |
| self._reported_frames = len(call_stack) | |
| return str(call_stack) | |
| def qWasmLocal(self, packet): | |
| # Format: qWasmLocal:frame_index;index | |
| data = packet.split(":") | |
| data = data[1].split(";") | |
| frame_index, local_index = data | |
| if frame_index == "0" and local_index == "2": | |
| return format_register_value(WASM_LOCAL_ADDR) | |
| return "E03" | |
| class ReloadingResponder(MyResponder): | |
| """ | |
| A fake stub that replaces the modules it has loaded with others when it | |
| continues, as an engine does when the page running them reloads, and stops | |
| to report the change. | |
| """ | |
| def __init__(self, modules, reloaded_modules): | |
| MyResponder.__init__(self, modules) | |
| self._reloaded_modules = reloaded_modules | |
| def cont(self): | |
| self._modules = self._reloaded_modules | |
| return "T05thread:1;library:;" | |
| class TestWasm(GDBRemoteTestBase): | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_load_module_with_embedded_symbols_from_remote(self): | |
| """Test connecting to a WebAssembly stub via GDB-remote and loading a Wasm module with embedded DWARF symbols""" | |
| yaml_path = "test_wasm_embedded_debug_sections.yaml" | |
| yaml_base, ext = os.path.splitext(yaml_path) | |
| obj_path = self.getBuildArtifact(yaml_base) | |
| self.yaml2obj(yaml_path, obj_path) | |
| self.server.responder = MyResponder([WasmModule(obj_path, "test_wasm")]) | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| num_modules = target.GetNumModules() | |
| self.assertEqual(1, num_modules) | |
| module = target.GetModuleAtIndex(0) | |
| num_sections = module.GetNumSections() | |
| self.assertEqual(5, num_sections) | |
| code_section = module.GetSectionAtIndex(0) | |
| self.assertEqual("code", code_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | code_section.GetFileOffset(), | |
| code_section.GetLoadAddress(target), | |
| ) | |
| debug_info_section = module.GetSectionAtIndex(1) | |
| self.assertEqual(".debug_info", debug_info_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | debug_info_section.GetFileOffset(), | |
| debug_info_section.GetLoadAddress(target), | |
| ) | |
| debug_abbrev_section = module.GetSectionAtIndex(2) | |
| self.assertEqual(".debug_abbrev", debug_abbrev_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | debug_abbrev_section.GetFileOffset(), | |
| debug_abbrev_section.GetLoadAddress(target), | |
| ) | |
| debug_line_section = module.GetSectionAtIndex(3) | |
| self.assertEqual(".debug_line", debug_line_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | debug_line_section.GetFileOffset(), | |
| debug_line_section.GetLoadAddress(target), | |
| ) | |
| debug_str_section = module.GetSectionAtIndex(4) | |
| self.assertEqual(".debug_str", debug_str_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | debug_line_section.GetFileOffset(), | |
| debug_line_section.GetLoadAddress(target), | |
| ) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_load_module_with_stripped_symbols_from_remote(self): | |
| """Test connecting to a WebAssembly stub via GDB-remote and loading a Wasm module with symbols stripped into a separate Wasm file""" | |
| sym_yaml_path = "test_sym.yaml" | |
| sym_yaml_base, ext = os.path.splitext(sym_yaml_path) | |
| sym_obj_path = self.getBuildArtifact(sym_yaml_base) + ".wasm" | |
| self.yaml2obj(sym_yaml_path, sym_obj_path) | |
| yaml_path = "test_wasm_external_debug_sections.yaml" | |
| yaml_base, ext = os.path.splitext(yaml_path) | |
| obj_path = self.getBuildArtifact(yaml_base) + ".wasm" | |
| self.yaml2obj(yaml_path, obj_path) | |
| self.server.responder = MyResponder([WasmModule(obj_path, "test_wasm")]) | |
| folder, _ = os.path.split(obj_path) | |
| self.runCmd( | |
| "settings set target.debug-file-search-paths " + os.path.abspath(folder) | |
| ) | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| num_modules = target.GetNumModules() | |
| self.assertEqual(1, num_modules) | |
| module = target.GetModuleAtIndex(0) | |
| num_sections = module.GetNumSections() | |
| self.assertEqual(5, num_sections) | |
| code_section = module.GetSectionAtIndex(0) | |
| self.assertEqual("code", code_section.GetName()) | |
| self.assertEqual( | |
| LOAD_ADDRESS | code_section.GetFileOffset(), | |
| code_section.GetLoadAddress(target), | |
| ) | |
| debug_info_section = module.GetSectionAtIndex(1) | |
| self.assertEqual(".debug_info", debug_info_section.GetName()) | |
| self.assertEqual( | |
| lldb.LLDB_INVALID_ADDRESS, debug_info_section.GetLoadAddress(target) | |
| ) | |
| debug_abbrev_section = module.GetSectionAtIndex(2) | |
| self.assertEqual(".debug_abbrev", debug_abbrev_section.GetName()) | |
| self.assertEqual( | |
| lldb.LLDB_INVALID_ADDRESS, debug_abbrev_section.GetLoadAddress(target) | |
| ) | |
| debug_line_section = module.GetSectionAtIndex(3) | |
| self.assertEqual(".debug_line", debug_line_section.GetName()) | |
| self.assertEqual( | |
| lldb.LLDB_INVALID_ADDRESS, debug_line_section.GetLoadAddress(target) | |
| ) | |
| debug_str_section = module.GetSectionAtIndex(4) | |
| self.assertEqual(".debug_str", debug_str_section.GetName()) | |
| self.assertEqual( | |
| lldb.LLDB_INVALID_ADDRESS, debug_line_section.GetLoadAddress(target) | |
| ) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_simple_wasm_debugging_session(self): | |
| """Test connecting to a WebAssembly stub via GDB-remote, loading a | |
| Wasm module with embedded DWARF symbols, setting a breakpoint and | |
| checking the debuggee state""" | |
| # simple.yaml was created by compiling simple.c to wasm and using | |
| # obj2yaml on the output. | |
| # | |
| # $ clang -target wasm32 -nostdlib -Wl,--no-entry -Wl,--export-all -O0 -g -o simple.wasm simple.c | |
| # $ obj2yaml simple.wasm -o simple.yaml | |
| yaml_path = "simple.yaml" | |
| yaml_base, _ = os.path.splitext(yaml_path) | |
| obj_path = self.getBuildArtifact(yaml_base) | |
| self.yaml2obj(yaml_path, obj_path) | |
| # Create a fake call stack. | |
| call_stacks = [ | |
| WasmCallStack( | |
| [WasmStackFrame(0x019C), WasmStackFrame(0x01E5), WasmStackFrame(0x01FE)] | |
| ), | |
| ] | |
| # Create fake memory for our wasm locals. | |
| self.memory = FakeMemory(0x10000, 0x20000) | |
| self.memory.store_bytes(WASM_LOCAL_ADDR, WASM_FRAME_BYTES) | |
| self.server.responder = MyResponder( | |
| [WasmModule(obj_path, "test_wasm")], call_stacks, self.memory | |
| ) | |
| target = self.dbg.CreateTarget("") | |
| breakpoint = target.BreakpointCreateByName("add") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| location = breakpoint.GetLocationAtIndex(0) | |
| self.assertTrue(location and location.IsEnabled(), VALID_BREAKPOINT_LOCATION) | |
| num_modules = target.GetNumModules() | |
| self.assertEqual(1, num_modules) | |
| thread = process.GetThreadAtIndex(0) | |
| self.assertTrue(thread.IsValid()) | |
| # Check that our frames match our fake call stack. | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertTrue(frame0.IsValid()) | |
| self.assertEqual(frame0.GetPC(), LOAD_ADDRESS | 0x019C) | |
| self.assertIn("add", frame0.GetFunctionName()) | |
| frame1 = thread.GetFrameAtIndex(1) | |
| self.assertTrue(frame1.IsValid()) | |
| self.assertEqual(frame1.GetPC(), LOAD_ADDRESS | 0x01E5) | |
| self.assertIn("main", frame1.GetFunctionName()) | |
| frame2 = thread.GetFrameAtIndex(2) | |
| self.assertTrue(frame2.IsValid()) | |
| # Wasm frames need distinct, ordered call frame addresses for StackID to | |
| # tell an inner frame from an outer one. Without them every frame shares | |
| # one address and stepping mistakes a step in for a step out. | |
| self.assertLess(frame0.GetCFA(), frame1.GetCFA()) | |
| self.assertLess(frame1.GetCFA(), frame2.GetCFA()) | |
| # Check that we can resolve local variables. | |
| a = frame0.FindVariable("a") | |
| self.assertTrue(a.IsValid()) | |
| self.assertEqual(a.GetValueAsUnsigned(), 1) | |
| b = frame0.FindVariable("b") | |
| self.assertTrue(b.IsValid()) | |
| self.assertEqual(b.GetValueAsUnsigned(), 2) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_max_backtrace_depth(self): | |
| """Test that a Wasm backtrace stops at the depth the target sets, so | |
| that a stack recursing without end is not walked to its end.""" | |
| yaml_path = "simple.yaml" | |
| yaml_base, _ = os.path.splitext(yaml_path) | |
| obj_path = self.getBuildArtifact(yaml_base) | |
| self.yaml2obj(yaml_path, obj_path) | |
| call_stacks = [ | |
| WasmCallStack( | |
| [WasmStackFrame(0x019C), WasmStackFrame(0x01E5), WasmStackFrame(0x01FE)] | |
| ), | |
| ] | |
| self.server.responder = MyResponder( | |
| [WasmModule(obj_path, "test_wasm")], call_stacks | |
| ) | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| thread = process.GetThreadAtIndex(0) | |
| self.assertTrue(thread.IsValid()) | |
| self.assertTrue(thread.GetFrameAtIndex(0).IsValid()) | |
| # A depth set after a stop still applies to the frames a backtrace has | |
| # not reported yet, which is when a user lowers it. | |
| self.runCmd("settings set target.process.thread.max-backtrace-depth 2") | |
| # The stub reported three frames, and the innermost two are the ones | |
| # kept. | |
| self.assertEqual(2, thread.GetNumFrames()) | |
| self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), LOAD_ADDRESS | 0x019C) | |
| self.assertEqual(thread.GetFrameAtIndex(1).GetPC(), LOAD_ADDRESS | 0x01E5) | |
| self.assertFalse(thread.GetFrameAtIndex(2).IsValid()) | |
| def build_wasm_module(self, name, yaml_path="simple.yaml", **module_args): | |
| """ | |
| Build a Wasm module the fake stub reports under the given name. A | |
| loaded module is told apart from another by the name it is reported | |
| under, so every instance has to be reported under a name of its own. | |
| """ | |
| obj_path = self.getBuildArtifact(name) | |
| self.yaml2obj(yaml_path, obj_path) | |
| return WasmModule(obj_path, name, **module_args) | |
| def connect_to_modules( | |
| self, modules, call_stacks, supports_instance=True, memory=None | |
| ): | |
| """ | |
| Connect to a fake stub holding the given modules and return its target | |
| and process. | |
| """ | |
| self.server.responder = MyResponder( | |
| modules, | |
| call_stacks, | |
| memory, | |
| supports_instance=supports_instance, | |
| ) | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| return target, process | |
| def get_globals_address(self, target, module): | |
| """ | |
| The address the loaded module gives its globals. Reading through it | |
| rather than a constructed address checks the encoding the object file | |
| produces and the one the process decodes against each other. | |
| """ | |
| image = target.FindModule(lldb.SBFileSpec(module.name)) | |
| self.assertTrue(image.IsValid()) | |
| global_section = image.FindSection("global") | |
| self.assertTrue(global_section.IsValid()) | |
| globals_addr = global_section.GetLoadAddress(target) | |
| # A global lives in the global index space of its instance rather than | |
| # in the space the module itself is loaded in. | |
| self.assertEqual(globals_addr, WASM_GLOBAL_ADDRESS | (module.module_id << 32)) | |
| return globals_addr | |
| def connect_to_globals(self, call_stacks, supports_instance=True): | |
| """ | |
| Connect to a fake stub holding WASM_GLOBALS and return its process | |
| together with the address its module gives its globals. | |
| """ | |
| module = self.build_wasm_module("test_wasm") | |
| target, process = self.connect_to_modules( | |
| [module], call_stacks, supports_instance | |
| ) | |
| return process, self.get_globals_address(target, module) | |
| def connect_to_frame_base_globals(self, supports_instance=True, call_stacks=None): | |
| """ | |
| Connect to a fake stub stopped in a module whose functions are based on | |
| a global holding the address of their frame, and return its thread. | |
| """ | |
| module = self.build_wasm_module( | |
| "test_wasm", | |
| yaml_path="simple_global_frame_base.yaml", | |
| global_values=FRAME_BASE_GLOBALS, | |
| ) | |
| if call_stacks is None: | |
| call_stacks = [ | |
| WasmCallStack([WasmStackFrame(0x019C), WasmStackFrame(0x01E5)]) | |
| ] | |
| memory = FakeMemory(0x10000, 0x20000) | |
| memory.store_bytes(WASM_LOCAL_ADDR, WASM_FRAME_BYTES) | |
| _, process = self.connect_to_modules( | |
| [module], | |
| call_stacks, | |
| supports_instance=supports_instance, | |
| memory=memory, | |
| ) | |
| thread = process.GetThreadAtIndex(0) | |
| self.assertTrue(thread.IsValid()) | |
| return thread | |
| def packets_received(self, prefix): | |
| """ | |
| The packets the client sent that start with the given prefix. An | |
| assertion on the packets themselves names the ones that make it fail, | |
| which an assertion on whether there are any does not. | |
| """ | |
| received = self.server.responder.packetLog.get_received() | |
| return [packet for packet in received if packet.startswith(prefix)] | |
| def global_reads_received(self, instance): | |
| """ | |
| The global reads the client sent that name an instance, or those that | |
| name a frame instead. Both forms share a packet name, so the suffix is | |
| what tells them apart. | |
| """ | |
| return [ | |
| packet | |
| for packet in self.packets_received("qWasmGlobal:") | |
| if (INSTANCE_KEY in packet) == instance | |
| ] | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global(self): | |
| """Test that a WebAssembly global can be read through the address its | |
| module gives it, and that a read it cannot serve fails instead of | |
| returning something plausible.""" | |
| call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] | |
| process, globals_addr = self.connect_to_globals(call_stacks) | |
| # A global is read as a whole value. | |
| error = lldb.SBError() | |
| data = process.ReadMemory(globals_addr + 0, 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x2A) | |
| data = process.ReadMemory(globals_addr + 1, 8, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) | |
| data = process.ReadMemory(globals_addr + 26, 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x33) | |
| # A type narrower than the global it is held in reads the low bytes, | |
| # which is how a char or short global is read. | |
| data = process.ReadMemory(globals_addr + 0, 1, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x2A) | |
| # Reading more than a global holds would have to come from somewhere | |
| # else. The next index is not adjacent storage, so this fails rather | |
| # than returning whatever is nearby. | |
| process.ReadMemory(globals_addr + 0, 8, error) | |
| self.assertFalse(error.Success()) | |
| self.assertIn("4-byte global", error.GetCString()) | |
| # Likewise for a global that does not exist. | |
| process.ReadMemory(globals_addr + 99, 4, error) | |
| self.assertFalse(error.Success()) | |
| # A global is named by the instance holding it. MODULE_ID differs from | |
| # every frame index, so a frame index cannot pass for an instance id. | |
| self.assertPacketLogReceived( | |
| [ | |
| global_read_packet(0), | |
| global_read_packet(1), | |
| global_read_packet(26), | |
| ] | |
| ) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_without_reported_call_stack(self): | |
| """Test that a WebAssembly global can be read while the stub reports no | |
| call stack for the instance holding it.""" | |
| process, globals_addr = self.connect_to_globals([]) | |
| # A thread always has a frame, which LLDB makes from the registers the | |
| # stub reports when the Wasm unwinder contributes none. The program | |
| # counter of that frame belongs to no module, so it is not a frame a | |
| # global can be read through. | |
| thread = process.GetThreadAtIndex(0) | |
| self.assertTrue(thread.IsValid()) | |
| self.assertEqual(1, thread.GetNumFrames()) | |
| self.assertEqual(MyResponder.current_pc, thread.GetFrameAtIndex(0).GetPC()) | |
| error = lldb.SBError() | |
| data = process.ReadMemory(globals_addr + 0, 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x2A) | |
| data = process.ReadMemory(globals_addr + 1, 8, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) | |
| self.assertPacketLogReceived( | |
| [ | |
| global_read_packet(0), | |
| global_read_packet(1), | |
| ] | |
| ) | |
| # The stub was asked for a call stack and reported none, so the reads | |
| # above were served without one. Reading a global through a frame is | |
| # something else, and is what a stub that cannot be told which instance | |
| # to read is limited to. | |
| self.assertNotEqual([], self.packets_received("qWasmCallStack")) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_from_multiple_instances(self): | |
| """Test that a global is read from the instance that holds it when more | |
| than one instance is loaded.""" | |
| module = self.build_wasm_module("test_wasm") | |
| second_module = self.build_wasm_module( | |
| "test_wasm_second", | |
| module_id=SECOND_MODULE_ID, | |
| global_values=SECOND_WASM_GLOBALS, | |
| ) | |
| # Only the first instance is executing, so the frame available to read a | |
| # global through belongs to it and cannot stand in for the other one. | |
| call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] | |
| target, process = self.connect_to_modules([module, second_module], call_stacks) | |
| # Each instance has to be a module of its own for its globals to be its | |
| # own. Collapsing the two would leave one of the load addresses unused. | |
| self.assertEqual(2, target.GetNumModules()) | |
| globals_addr = self.get_globals_address(target, module) | |
| second_globals_addr = self.get_globals_address(target, second_module) | |
| self.assertNotEqual(globals_addr, second_globals_addr) | |
| # The same index in either instance names a global of that instance, and | |
| # the fake stub refuses any instance it did not load, so neither value | |
| # can come from the wrong place. | |
| error = lldb.SBError() | |
| for addr, global_values in [ | |
| (globals_addr, WASM_GLOBALS), | |
| (second_globals_addr, SECOND_WASM_GLOBALS), | |
| ]: | |
| for index, (size, value) in global_values.items(): | |
| data = process.ReadMemory(addr + index, size, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), value) | |
| self.assertPacketLogReceived( | |
| [ | |
| global_read_packet(0), | |
| global_read_packet(1), | |
| global_read_packet(0, SECOND_MODULE_ID), | |
| global_read_packet(1, SECOND_MODULE_ID), | |
| ] | |
| ) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_from_default_instance(self): | |
| """Test that a global is read from the instance a debug session is | |
| created for, which a stub reports at id zero.""" | |
| # An id of zero yields the load address a stub reports when it cannot | |
| # report an instance at all, and an address the running code computed | |
| # carries no id either, so zero has to name an instance. | |
| module = self.build_wasm_module("test_wasm", module_id=0) | |
| self.assertEqual(module.load_address, WASM_OBJECT_ADDRESS) | |
| call_stacks = [WasmCallStack([WasmStackFrame(0x019C, module.load_address)])] | |
| target, process = self.connect_to_modules([module], call_stacks) | |
| globals_addr = self.get_globals_address(target, module) | |
| error = lldb.SBError() | |
| data = process.ReadMemory(globals_addr + 0, 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x2A) | |
| # Zero names that instance rather than standing for no instance, so the | |
| # read is scoped to it instead of falling back to naming a frame. | |
| self.assertPacketLogReceived([global_read_packet(0, module.module_id)]) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_variable_located_through_global(self): | |
| """Test that a variable an outer frame locates through a Wasm global is | |
| read from the instance the frame is executing.""" | |
| thread = self.connect_to_frame_base_globals() | |
| frame1 = thread.GetFrameAtIndex(1) | |
| self.assertTrue(frame1.IsValid()) | |
| self.assertIn("main", frame1.GetFunctionName()) | |
| # A variable relative to that frame base is only reachable by reading the | |
| # global, so these values are what came back from it. | |
| i = frame1.FindVariable("i") | |
| self.assertTrue(i.IsValid()) | |
| self.assertEqual(i.GetValueAsUnsigned(), 1) | |
| j = frame1.FindVariable("j") | |
| self.assertTrue(j.IsValid()) | |
| self.assertEqual(j.GetValueAsUnsigned(), 2) | |
| # The global belongs to the instance the frame is executing, and that | |
| # instance is what names it. The frame the read goes through does not. | |
| self.assertPacketLogReceived( | |
| [global_read_packet(OUTER_FRAME_BASE_GLOBAL_INDEX)] | |
| ) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_variable_located_through_global_in_innermost_frame(self): | |
| """Test that a variable the innermost frame locates through a Wasm global | |
| is read from the instance that frame is executing.""" | |
| # The innermost frame is where a stop leaves the program, and its | |
| # register context is the one the thread carries, which exists before any | |
| # frame does. The instance that frame is executing is therefore only | |
| # known once the stub has been asked for its call stack. | |
| thread = self.connect_to_frame_base_globals() | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertTrue(frame0.IsValid()) | |
| self.assertIn("add", frame0.GetFunctionName()) | |
| a = frame0.FindVariable("a") | |
| self.assertTrue(a.IsValid()) | |
| self.assertEqual(a.GetValueAsUnsigned(), 1) | |
| b = frame0.FindVariable("b") | |
| self.assertTrue(b.IsValid()) | |
| self.assertEqual(b.GetValueAsUnsigned(), 2) | |
| self.assertPacketLogReceived( | |
| [global_read_packet(INNER_FRAME_BASE_GLOBAL_INDEX)] | |
| ) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_variable_located_through_global_without_reported_call_stack(self): | |
| """Test that a variable a frame locates through a Wasm global is read | |
| from the instance that frame is executing while the stub reports no call | |
| stack.""" | |
| # LLDB makes a frame from the registers the stub reports when the Wasm | |
| # unwinder contributes none. The program counter of that frame is the | |
| # only place the instance it executes is recorded, so a read that looked | |
| # for the instance anywhere else would be left without one and fall back | |
| # to naming a frame the stub never reported. | |
| thread = self.connect_to_frame_base_globals(call_stacks=[]) | |
| self.assertEqual(1, thread.GetNumFrames()) | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertTrue(frame0.IsValid()) | |
| self.assertIn("add", frame0.GetFunctionName()) | |
| a = frame0.FindVariable("a") | |
| self.assertTrue(a.IsValid()) | |
| self.assertEqual(a.GetValueAsUnsigned(), 1) | |
| self.assertPacketLogReceived( | |
| [global_read_packet(INNER_FRAME_BASE_GLOBAL_INDEX)] | |
| ) | |
| self.assertEqual([], self.global_reads_received(instance=False)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_variable_located_through_global_legacy_server(self): | |
| """Test that a variable the innermost frame locates through a Wasm global | |
| is read through that frame when the stub cannot be told which instance to | |
| read.""" | |
| thread = self.connect_to_frame_base_globals(supports_instance=False) | |
| frame0 = thread.GetFrameAtIndex(0) | |
| self.assertTrue(frame0.IsValid()) | |
| self.assertIn("add", frame0.GetFunctionName()) | |
| a = frame0.FindVariable("a") | |
| self.assertTrue(a.IsValid()) | |
| self.assertEqual(a.GetValueAsUnsigned(), 1) | |
| # A frame stands in for the instance it is executing, which is all such a | |
| # stub can be asked, so the same global stays within reach through the | |
| # frame. | |
| self.assertPacketLogReceived(["qWasmGlobal:0;0"]) | |
| self.assertEqual([], self.global_reads_received(instance=True)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_legacy_server(self): | |
| """Test that a global is read through the frame executing the instance | |
| that holds it when the stub cannot be told which instance to read.""" | |
| call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] | |
| process, globals_addr = self.connect_to_globals( | |
| call_stacks, supports_instance=False | |
| ) | |
| error = lldb.SBError() | |
| data = process.ReadMemory(globals_addr + 0, 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0x2A) | |
| # A global at an index the frame index cannot be mistaken for, so that | |
| # the frame and the global keep their places in the packet. | |
| data = process.ReadMemory(globals_addr + 1, 8, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), 0xDEADBEEF) | |
| # A stub is only asked for what it advertised, so the form it never | |
| # offered is not tried on it. | |
| self.assertPacketLogReceived(["qWasmGlobal:0;0", "qWasmGlobal:0;1"]) | |
| self.assertEqual([], self.global_reads_received(instance=True)) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_legacy_server_without_reported_call_stack(self): | |
| """Test that a global is out of reach of a stub that can only read a | |
| global through a frame while it reports no call stack.""" | |
| process, globals_addr = self.connect_to_globals([], supports_instance=False) | |
| error = lldb.SBError() | |
| process.ReadMemory(globals_addr + 0, 4, error) | |
| self.assertFalse(error.Success()) | |
| self.assertIn("can only read a global through a frame", error.GetCString()) | |
| self.assertEqual([], self.packets_received("qWasmGlobal:")) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_read_global_legacy_server_other_instance(self): | |
| """Test that a frame executing one instance is not read as a frame of | |
| another when the stub can only read a global through a frame.""" | |
| module = self.build_wasm_module("test_wasm") | |
| second_module = self.build_wasm_module( | |
| "test_wasm_second", | |
| module_id=SECOND_MODULE_ID, | |
| global_values=SECOND_WASM_GLOBALS, | |
| ) | |
| call_stacks = [WasmCallStack([WasmStackFrame(0x019C)])] | |
| target, process = self.connect_to_modules( | |
| [module, second_module], call_stacks, supports_instance=False | |
| ) | |
| self.assertEqual(2, target.GetNumModules()) | |
| # The only frame is executing the first instance, and a frame of one | |
| # instance indexes no globals of another. Reading a global of the second | |
| # instance fails rather than answering with a global of the first. | |
| error = lldb.SBError() | |
| process.ReadMemory(self.get_globals_address(target, second_module), 4, error) | |
| self.assertFalse(error.Success()) | |
| self.assertIn("can only read a global through a frame", error.GetCString()) | |
| # The instance the frame is executing is still within reach. | |
| error = lldb.SBError() | |
| data = process.ReadMemory(self.get_globals_address(target, module), 4, error) | |
| self.assertSuccess(error) | |
| self.assertEqual(int.from_bytes(data, "little"), WASM_GLOBALS[0][1]) | |
| self.assertPacketLogReceived(["qWasmGlobal:0;0"]) | |
| def reload_modules(self, modules, reloaded_modules): | |
| """ | |
| Connect to a fake stub holding the given modules, continue it until it | |
| has reloaded them as the others, and return its target. | |
| """ | |
| self.server.responder = ReloadingResponder(modules, reloaded_modules) | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| self.assertSuccess(process.Continue()) | |
| return target | |
| def modules_in_target(self, target): | |
| """ | |
| The name of every module in the target, together with the address its | |
| image was read from. | |
| """ | |
| return [ | |
| ( | |
| module.GetFileSpec().GetFilename(), | |
| module.GetObjectFileHeaderAddress().GetLoadAddress(target), | |
| ) | |
| for module in target.modules | |
| ] | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_reload_modules(self): | |
| """Test that the modules a stub no longer reports leave the target, | |
| the first one it loaded included.""" | |
| modules = [ | |
| self.build_wasm_module("test_wasm"), | |
| self.build_wasm_module("second_test_wasm", module_id=SECOND_MODULE_ID), | |
| ] | |
| reloaded_modules = [ | |
| self.build_wasm_module("reloaded_test_wasm", module_id=RELOADED_MODULE_ID), | |
| self.build_wasm_module( | |
| "reloaded_second_test_wasm", module_id=RELOADED_SECOND_MODULE_ID | |
| ), | |
| ] | |
| target = self.reload_modules(modules, reloaded_modules) | |
| self.assertEqual( | |
| self.modules_in_target(target), | |
| [(module.name, module.load_address) for module in reloaded_modules], | |
| ) | |
| @skipIfAsan | |
| @skipIfXmlSupportMissing | |
| def test_reload_modules_under_same_name(self): | |
| """Test that a module a stub reloads under the name it had is read | |
| again from where it was reloaded.""" | |
| modules = [ | |
| self.build_wasm_module("test_wasm"), | |
| self.build_wasm_module("second_test_wasm", module_id=SECOND_MODULE_ID), | |
| ] | |
| reloaded_modules = [ | |
| WasmModule(module.obj_path, module.name, module_id=module_id) | |
| for module, module_id in zip( | |
| modules, [RELOADED_MODULE_ID, RELOADED_SECOND_MODULE_ID] | |
| ) | |
| ] | |
| target = self.reload_modules(modules, reloaded_modules) | |
| self.assertEqual( | |
| self.modules_in_target(target), | |
| [(module.name, module.load_address) for module in reloaded_modules], | |
| ) | |
| @skipIfXmlSupportMissing | |
| def test_non_wasm_process(self): | |
| """Test that the plugin falls back to plain GDB remote debugging when | |
| it is requested by name for a process that isn't WebAssembly.""" | |
| class NonWasmResponder(MockGDBServerResponder): | |
| def qHostInfo(self): | |
| return "triple:%s;ptrsize:8;endian:little;" % hex_encode_bytes( | |
| "x86_64-unknown-linux-gnu" | |
| ) | |
| def qfThreadInfo(self): | |
| return "m1" | |
| def haltReason(self): | |
| return "T02thread:1;threads:1;thread-pcs:10001bc00;" | |
| def qXferRead(self, obj, annex, offset, length): | |
| if annex == "target.xml": | |
| return ( | |
| """<?xml version="1.0"?> | |
| <target version="1.0"> | |
| <architecture>i386:x86-64</architecture> | |
| <feature name="org.gnu.gdb.i386.core"> | |
| <reg name="rip" bitsize="64" regnum="0" type="code_ptr" group="general"/> | |
| </feature> | |
| </target>""", | |
| False, | |
| ) | |
| return None, False | |
| self.server.responder = NonWasmResponder() | |
| target = self.dbg.CreateTarget("") | |
| process = self.connect(target, "wasm") | |
| lldbutil.expect_state_changes( | |
| self, self.dbg.GetListener(), process, [lldb.eStateStopped] | |
| ) | |
| self.assertEqual(process.GetPluginName(), "wasm") | |
| self.assertIn("x86_64", target.GetTriple()) | |
| thread = process.GetThreadAtIndex(0) | |
| self.assertTrue(thread.IsValid()) | |
| self.assertEqual(thread.GetFrameAtIndex(0).GetPC(), 0x10001BC00) | |
| self.assertNotIn( | |
| "qWasmCallStack", "".join(self.server.responder.packetLog.get_received()) | |
| ) |