| """ | |
| Check that when data is written over a software breakpoint site, it does not | |
| corrupt the breakpoint instruction, and is later written to memory when the | |
| breakpoint is removed. | |
| """ | |
| import lldb | |
| from lldbsuite.test.lldbtest import * | |
| import lldbsuite.test.lldbutil as lldbutil | |
| from lldbsuite.test.decorators import * | |
| from lldbsuite.test.lldbgdbproxy import GDBProxyTestBase | |
| class TestCases(object): | |
| NO_DEBUG_INFO_TESTCASE = True | |
| def run_to_start(self): | |
| is_proxy = isinstance(self, GDBProxyTestBase) | |
| self.build() | |
| exe = self.getBuildArtifact("a.out") | |
| target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) | |
| if is_proxy: | |
| self.connect(target) | |
| self.line = line_number("main.c", "// break here") | |
| lldbutil.run_break_set_by_file_and_line( | |
| self, "main.c", self.line, num_expected_locations=1, loc_exact=True | |
| ) | |
| self.runCmd("continue" if is_proxy else "run") | |
| self.expect( | |
| "thread list", | |
| STOPPED_DUE_TO_BREAKPOINT, | |
| substrs=["stopped", "stop reason = breakpoint"], | |
| ) | |
| loop_start_breakpoint_addr = ( | |
| target.breakpoints[0].GetLocationAtIndex(0).GetLoadAddress() | |
| ) | |
| return target, target.GetProcess(), loop_start_breakpoint_addr | |
| # Could not find a way to make place_break_here visible to lldb on Windows. | |
| @skipIfWindows | |
| @skipIfOutOfTreeDebugserver | |
| def test_write_over_breakpoint(self): | |
| target, process, loop_start_breakpoint_addr = self.run_to_start() | |
| # Memory operations and breakpoint actions must be sent to the server | |
| # right away instead of waiting for the next continue event. | |
| self.runCmd("settings set target.process.disable-memory-cache on") | |
| # Breakpoints set by address are never delayed but to make this test | |
| # less fragile if later changed, disable delayed breakpoints entirely. | |
| self.runCmd("settings set target.process.use-delayed-breakpoints false") | |
| # At this point we are stopped at the start of the for loop. | |
| # We will set a further breakpoint in foo, and this is the one we will | |
| # test by writing over it then continuing to it. | |
| # We could use the same breakpoint, but arranging for dead code | |
| # immediately before it is more risky. | |
| # The breakpoint must be on an exact address, because we do not want | |
| # lldb to adjust it based on the debug information for prologues | |
| # and epilogues. | |
| symbol_contexts = target.FindSymbols("place_break_here") | |
| self.assertEqual(1, len(symbol_contexts)) | |
| label_symbol = symbol_contexts[0].GetSymbol() | |
| self.assertTrue(label_symbol.IsValid()) | |
| bkpt_address = label_symbol.GetStartAddress().GetLoadAddress(target) | |
| # lldb-server has its algorithm unit tested as part of | |
| # NativeProcessProtocol, but debugserver does not use that. So we will | |
| # do a few different types of overwrite here so we have coverage for both | |
| # debug servers. | |
| # | |
| # We cannot be sure what the software break size will be, so I'm assuming | |
| # 4 bytes because that's what Arm/AArch64 use. This means we may not be | |
| # getting full coverage on Thumb or x86 but the test should still pass | |
| # there. | |
| # | |
| # We assume that the instruction immediately before the breakpoint in | |
| # foo is dead code, so we are allowed to corrupt it. | |
| writes = [ | |
| # Up to but not over breakpoint. | |
| (-4, 4), | |
| # Over start of breakpoint. | |
| (-2, 4), | |
| # Exactly over breakpoint. | |
| (0, 4), | |
| # Over end of breakpoint. | |
| (2, 4), | |
| # Immediately after breakpoint. | |
| (4, 4), | |
| # From before to after breakpoint. | |
| (-4, 12), | |
| ] | |
| for write_offset, write_size in writes: | |
| # Place a breakpoint immediately after the dead code in foo. | |
| bkpt = target.BreakpointCreateByAddress(bkpt_address) | |
| self.assertTrue(bkpt.IsValid()) | |
| self.assertEqual(bkpt.GetNumLocations(), 1) | |
| self.assertFalse(bkpt.IsHardware()) | |
| self.assertEqual(bkpt.GetLocationAtIndex(0).GetLoadAddress(), bkpt_address) | |
| check_address = bkpt_address + write_offset | |
| # Read around the breakpoint site. We assume that read subsitution | |
| # is working, so the data here is the original contents of memory | |
| # without the trap instruction. | |
| err = lldb.SBError() | |
| original_data = bytearray( | |
| process.ReadMemory(check_address, write_size, err) | |
| ) | |
| self.assertSuccess(err) | |
| self.assertEqual(len(original_data), write_size) | |
| # Write around/in/over the breakpoint site. | |
| write_data = bytearray(range(write_size)) | |
| wrote = process.WriteMemory(check_address, write_data, err) | |
| self.assertSuccess(err) | |
| self.assertEqual(wrote, write_size) | |
| # The data overlapping the breakpoint site should be in that breakpoint's | |
| # saved data. So it will appear as if all the data was written to memory, | |
| # even though it was not yet. | |
| after_write = bytearray(process.ReadMemory(check_address, write_size, err)) | |
| self.assertSuccess(err) | |
| self.assertEqual(after_write, write_data) | |
| # The instruction in memory should still be intact so we can continue | |
| # to the breakpoint. | |
| process.Continue() | |
| thread = process.thread[0] | |
| self.assertState(process.GetState(), lldb.eStateStopped) | |
| self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) | |
| # Should be stopped at the breakpoint we placed in foo. This proves that | |
| # the breakpoint instruction was intact. | |
| self.assertEqual( | |
| bkpt_address, | |
| thread.selected_frame.GetPC(), | |
| ) | |
| # When the breakpoint is removed the saved bytes will be written to | |
| # memory. | |
| self.assertTrue(target.BreakpointDelete(bkpt.GetID())) | |
| data = process.ReadMemory(check_address, write_size, err) | |
| self.assertSuccess(err) | |
| self.assertEqual(bytearray(data), write_data) | |
| # Restore the original instruction data. The dead code before the | |
| # breakpoint is ok but the instructions after it must be put back | |
| # so we can continue. | |
| wrote = process.WriteMemory(check_address, original_data, err) | |
| self.assertSuccess(err) | |
| self.assertEqual(wrote, write_size) | |
| # Continue back to the start of the loop. | |
| process.Continue() | |
| self.assertState(process.GetState(), lldb.eStateStopped) | |
| self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) | |
| self.assertEqual(loop_start_breakpoint_addr, thread.selected_frame.GetPC()) | |
| def test_write_over_uncommitted_breakpoint(self): | |
| self.runCmd("settings set target.process.use-delayed-breakpoints true") | |
| target, process, loop_start_breakpoint_addr = self.run_to_start() | |
| bkpt = target.BreakpointCreateByName("foo") | |
| self.assertTrue(bkpt.IsValid()) | |
| self.assertEqual(bkpt.GetNumLocations(), 1) | |
| self.assertFalse(bkpt.IsHardware()) | |
| # At this point only lldb knows about the breakpoint, it has not been | |
| # sent to the debug server yet. It is treated as software with a 0 size | |
| # breakpoint site. | |
| bkpt_address = bkpt.GetLocationAtIndex(0).GetLoadAddress() | |
| # The largest breakpoint instruction we know of is 4 bytes. | |
| read_size = 4 | |
| err = lldb.SBError() | |
| original_data = bytearray(process.ReadMemory(bkpt_address, read_size, err)) | |
| self.assertSuccess(err) | |
| self.assertEqual(len(original_data), read_size) | |
| # The smallest break instruction we know about is 1 byte, so we will | |
| # write only 1 byte. The value is ideally != byte 1 of the break and | |
| # != to the value currently in memory. 0xcd is different to x86's 0xcc, | |
| # and not used by any other platform we support. 0xdc is the fallback | |
| # and if the original value is also 0xcd. | |
| write_data = bytearray([0xCD if original_data[0] != 0xCD else 0xDC]) | |
| # Write something over the breakpoint. Use a single byte since x86's | |
| # break is a single byte and we do not want to corrupt other code. | |
| # LLDB used to crash at this point. | |
| err = lldb.SBError() | |
| wrote = process.WriteMemory(bkpt_address, write_data, err) | |
| self.assertSuccess(err) | |
| self.assertEqual(wrote, 1) | |
| def check_memory(): | |
| err = lldb.SBError() | |
| got = bytearray(process.ReadMemory(bkpt_address, read_size, err)) | |
| self.assertSuccess(err) | |
| self.assertEqual(len(got), read_size) | |
| read_expected = write_data + original_data[1:] | |
| self.assertEqual(got, read_expected) | |
| # We should see the original data with our new single byte at the start. | |
| check_memory() | |
| # The instruction in memory should still be intact so we can continue | |
| # to the breakpoint. | |
| process.Continue() | |
| thread = process.thread[0] | |
| self.assertState(process.GetState(), lldb.eStateStopped) | |
| self.assertStopReason(thread.GetStopReason(), lldb.eStopReasonBreakpoint) | |
| # Should be stopped at the breakpoint we placed in foo. This proves that | |
| # the breakpoint instruction was intact. | |
| self.assertEqual( | |
| bkpt_address, | |
| thread.selected_frame.GetPC(), | |
| ) | |
| # We should see the new first byte still. At this point the debug server | |
| # should be managing the breakpoint, but this checks that the handover | |
| # was done correctly. | |
| check_memory() | |
| @skipIfRemote | |
| # x86, x86_64, s390x and all versions of Windows report a breakpoint address | |
| # after the breakpoint and the test fails due to | |
| # https://github.com/llvm/llvm-project/issues/222284. | |
| @skipIf(archs=["i386", "x86_64", "s390x"]) | |
| @skipIfWindows | |
| class WriteOverLLDBManagedSoftwareBreakpoint(GDBProxyTestBase, TestCases): | |
| def respond(self, packet): | |
| # Pretend that the server cannot handle breakpoints, which means lldb | |
| # must manage them. | |
| if packet.startswith("jMultiBreakpoint"): | |
| return "" | |
| elif packet.startswith("Z"): | |
| return "" | |
| return super().respond(packet) | |
| class WriteOverExternalSoftwareBreakpoint(TestBase, TestCases): | |
| pass |