545 lines · python
1import os2import os.path3import lldb4from lldbsuite.test.lldbtest import *5from lldbsuite.test.gdbclientutils import *6from lldbsuite.test.lldbgdbproxy import *7import lldbgdbserverutils8import re9 10 11class ThreadSnapshot:12 def __init__(self, thread_id, registers):13 self.thread_id = thread_id14 self.registers = registers15 16 17class MemoryBlockSnapshot:18 def __init__(self, address, data):19 self.address = address20 self.data = data21 22 23class StateSnapshot:24 def __init__(self, thread_snapshots, memory):25 self.thread_snapshots = thread_snapshots26 self.memory = memory27 self.thread_id = None28 29 30class RegisterInfo:31 def __init__(self, lldb_index, name, bitsize, little_endian):32 self.lldb_index = lldb_index33 self.name = name34 self.bitsize = bitsize35 self.little_endian = little_endian36 37 38BELOW_STACK_POINTER = 1638439ABOVE_STACK_POINTER = 409640 41BLOCK_SIZE = 102442 43SOFTWARE_BREAKPOINTS = 044HARDWARE_BREAKPOINTS = 145WRITE_WATCHPOINTS = 246 47 48class ReverseTestBase(GDBProxyTestBase):49 """50 Base class for tests that need reverse execution.51 52 This class uses a gdbserver proxy to add very limited reverse-53 execution capability to lldb-server/debugserver for testing54 purposes only.55 56 To use this class, run the inferior forward until some stopping point.57 Then call `start_recording()` and execute forward again until reaching58 a software breakpoint; this class records the state before each execution executes.59 At that point, the server will accept "bc" and "bs" packets to step60 backwards through the state.61 When executing during recording, we only allow single-step and continue without62 delivering a signal, and only software breakpoint stops are allowed.63 64 We assume that while recording is enabled, the only effects of instructions65 are on general-purpose registers (read/written by the 'g' and 'G' packets)66 and on memory bytes between [SP - BELOW_STACK_POINTER, SP + ABOVE_STACK_POINTER).67 """68 69 NO_DEBUG_INFO_TESTCASE = True70 71 """72 A list of StateSnapshots in time order.73 74 There is one snapshot per single-stepped instruction,75 representing the state before that instruction was76 executed. The last snapshot in the list is the77 snapshot before the last instruction was executed.78 This is an undo log; we snapshot a superset of the state that may have79 been changed by the instruction's execution.80 """81 snapshots = None82 recording_enabled = False83 84 breakpoints = None85 86 pc_register_info = None87 sp_register_info = None88 general_purpose_register_info = None89 90 def __init__(self, *args, **kwargs):91 GDBProxyTestBase.__init__(self, *args, **kwargs)92 self.breakpoints = [set(), set(), set(), set(), set()]93 94 def respond(self, packet):95 if not packet:96 raise ValueError("Invalid empty packet")97 if packet == self.server.PACKET_INTERRUPT:98 # Don't send a response. We'll just run to completion.99 return []100 if self.is_command(packet, "qSupported", ":"):101 # Disable multiprocess support in the server and in LLDB102 # since Mac debugserver doesn't support it and we want lldb-server to103 # be consistent with that104 reply = self.pass_through(packet.replace(";multiprocess", ""))105 return reply.replace(";multiprocess", "") + ";ReverseStep+;ReverseContinue+"106 if packet == "c" or packet == "s":107 packet = "vCont;" + packet108 elif (109 packet[0] == "c" or packet[0] == "s" or packet[0] == "C" or packet[0] == "S"110 ):111 raise ValueError(112 "Old-style continuation packets with address or signal not supported yet"113 )114 if self.is_command(packet, "vCont", ";"):115 if self.recording_enabled:116 return self.continue_with_recording(packet)117 snapshots = []118 if packet == "bc":119 return self.reverse_continue()120 if packet == "bs":121 return self.reverse_step()122 if packet == "jThreadsInfo":123 # Suppress this because it contains thread stop reasons which we might124 # need to modify, and we don't want to have to implement that.125 return ""126 if packet[0] == "x":127 # Suppress *binary* reads as results starting with "O" can be mistaken for an output packet128 # by the test server code129 return ""130 if packet[0] == "z" or packet[0] == "Z":131 reply = self.pass_through(packet)132 if reply == "OK":133 self.update_breakpoints(packet)134 return reply135 return GDBProxyTestBase.respond(self, packet)136 137 def start_recording(self):138 self.recording_enabled = True139 self.snapshots = []140 141 def stop_recording(self):142 """143 Don't record when executing foward.144 145 Reverse execution is still supported until the next forward continue.146 """147 self.recording_enabled = False148 149 def is_command(self, packet, cmd, follow_token):150 return packet == cmd or packet[0 : len(cmd) + 1] == cmd + follow_token151 152 def update_breakpoints(self, packet):153 m = re.match("([zZ])([01234]),([0-9a-f]+),([0-9a-f]+)", packet)154 if m is None:155 raise ValueError("Invalid breakpoint packet: " + packet)156 t = int(m.group(2))157 addr = int(m.group(3), 16)158 kind = int(m.group(4), 16)159 if m.group(1) == "Z":160 self.breakpoints[t].add((addr, kind))161 else:162 self.breakpoints[t].discard((addr, kind))163 164 def breakpoint_triggered_at(self, pc):165 if any(addr == pc for addr, kind in self.breakpoints[SOFTWARE_BREAKPOINTS]):166 return True167 if any(addr == pc for addr, kind in self.breakpoints[HARDWARE_BREAKPOINTS]):168 return True169 return False170 171 def watchpoint_triggered(self, new_value_block, current_contents):172 """Returns the address or None."""173 for watch_addr, kind in self.breakpoints[WRITE_WATCHPOINTS]:174 for offset in range(0, kind):175 addr = watch_addr + offset176 if (177 addr >= new_value_block.address178 and addr < new_value_block.address + len(new_value_block.data)179 ):180 index = addr - new_value_block.address181 if (182 new_value_block.data[index * 2 : (index + 1) * 2]183 != current_contents[index * 2 : (index + 1) * 2]184 ):185 return watch_addr186 return None187 188 def continue_with_recording(self, packet):189 self.logger.debug("Continue with recording enabled")190 191 step_packet = "vCont;s"192 if packet == "vCont":193 requested_step = False194 else:195 m = re.match("vCont;(c|s)(.*)", packet)196 if m is None:197 raise ValueError("Unsupported vCont packet: " + packet)198 requested_step = m.group(1) == "s"199 step_packet += m.group(2)200 201 while True:202 snapshot = self.capture_snapshot()203 reply = self.pass_through(step_packet)204 (stop_signal, stop_pairs) = self.parse_stop_reply(reply)205 if stop_signal != 5:206 raise ValueError("Unexpected stop signal: " + reply)207 is_swbreak = False208 thread_id = None209 for key, value in stop_pairs.items():210 if key == "thread":211 thread_id = self.parse_thread_id(value)212 continue213 if re.match("[0-9a-f]+", key):214 continue215 if key == "swbreak" or (key == "reason" and value == "breakpoint"):216 is_swbreak = True217 continue218 if key == "metype":219 reason = self.stop_reason_from_mach_exception(stop_pairs)220 if reason == "breakpoint":221 is_swbreak = True222 elif reason != "singlestep":223 raise ValueError(f"Unsupported stop reason in {reply}")224 continue225 if key in [226 "name",227 "threads",228 "thread-pcs",229 "reason",230 "mecount",231 "medata",232 "memory",233 ]:234 continue235 raise ValueError(f"Unknown stop key '{key}' in {reply}")236 if is_swbreak:237 self.logger.debug("Recording stopped")238 return reply239 if thread_id is None:240 return ValueError("Expected thread ID: " + reply)241 snapshot.thread_id = thread_id242 self.snapshots.append(snapshot)243 if requested_step:244 self.logger.debug("Recording stopped for step")245 return reply246 247 def stop_reason_from_mach_exception(self, stop_pairs):248 # See StopInfoMachException::CreateStopReasonWithMachException.249 if int(stop_pairs["metype"]) != 6: # EXC_BREAKPOINT250 raise ValueError(f"Unsupported exception type {value} in {reply}")251 medata = stop_pairs["medata"]252 arch = self.getArchitecture()253 if arch in ["amd64", "i386", "x86_64"]:254 if int(medata[0], 16) == 2:255 return "breakpoint"256 if int(medata[0], 16) == 1 and int(medata[1], 16) == 0:257 return "singlestep"258 elif arch in ["arm64", "arm64e"]:259 if int(medata[0], 16) == 1 and int(medata[1], 16) != 0:260 return "breakpoint"261 elif int(medata[0], 16) == 1 and int(medata[1], 16) == 0:262 return "singlestep"263 else:264 raise ValueError(f"Unsupported architecture '{arch}'")265 raise ValueError(f"Unsupported exception details in {reply}")266 267 def parse_stop_reply(self, reply):268 if not reply:269 raise ValueError("Invalid empty packet")270 if reply[0] == "T" and len(reply) >= 3:271 result = {}272 for k, v in self.parse_pairs(reply[3:]):273 if k in ["medata", "memory"]:274 if k in result:275 result[k].append(v)276 else:277 result[k] = [v]278 else:279 result[k] = v280 return (int(reply[1:3], 16), result)281 raise ValueError("Unsupported stop reply: " + reply)282 283 def parse_pairs(self, text):284 for pair in text.split(";"):285 if not pair:286 continue287 m = re.match("([^:]+):(.*)", pair)288 if m is None:289 raise ValueError("Invalid pair text: " + text)290 yield (m.group(1), m.group(2))291 292 def capture_snapshot(self):293 """Snapshot all threads and their stack memories."""294 self.ensure_register_info()295 current_thread = self.get_current_thread()296 thread_snapshots = []297 memory = []298 for thread_id in self.get_thread_list():299 registers = {}300 for index in sorted(self.general_purpose_register_info.keys()):301 reply = self.pass_through(f"p{index:x};thread:{thread_id:x};")302 if reply == "" or reply[0] == "E":303 # Mac debugserver tells us about registers that it won't let304 # us actually read. Ignore those registers.305 self.logger.debug(f"Failed to read register {index:x}")306 continue307 registers[index] = reply308 thread_snapshot = ThreadSnapshot(thread_id, registers)309 thread_sp = self.get_register(310 self.sp_register_info, thread_snapshot.registers311 )312 313 # The memory above or below the stack pointer may be mapped, but not314 # both readable and writeable. For example on Arm 32-bit Linux, there315 # is a "[vectors]" mapping above the stack, which can be read but not316 # written to.317 #318 # Therefore, we should limit any reads to the stack region, which we319 # know is readable and writeable.320 region_info = self.get_memory_region_info(thread_sp)321 lower = max(thread_sp - BELOW_STACK_POINTER, region_info["start"])322 upper = min(323 thread_sp + ABOVE_STACK_POINTER,324 region_info["start"] + region_info["size"],325 )326 327 memory += self.read_memory(lower, upper)328 thread_snapshots.append(thread_snapshot)329 self.set_current_thread(current_thread)330 return StateSnapshot(thread_snapshots, memory)331 332 def restore_snapshot(self, snapshot):333 """334 Restore the snapshot during reverse execution.335 336 If this triggers a breakpoint or watchpoint, return the stop reply,337 otherwise None.338 """339 current_thread = self.get_current_thread()340 stop_reasons = []341 for thread_snapshot in snapshot.thread_snapshots:342 thread_id = thread_snapshot.thread_id343 for lldb_index in sorted(thread_snapshot.registers.keys()):344 data = thread_snapshot.registers[lldb_index]345 reply = self.pass_through(346 f"P{lldb_index:x}={data};thread:{thread_id:x};"347 )348 if reply != "OK":349 try:350 reg_name = self.general_purpose_register_info[lldb_index].name351 except KeyError:352 reg_name = f"with index {lldb_index}"353 raise ValueError(f"Can't restore thread register {reg_name}")354 if thread_id == snapshot.thread_id:355 new_pc = self.get_register(356 self.pc_register_info, thread_snapshot.registers357 )358 if self.breakpoint_triggered_at(new_pc):359 stop_reasons.append([("reason", "breakpoint")])360 self.set_current_thread(current_thread)361 for block in snapshot.memory:362 current_memory = self.pass_through(363 f"m{block.address:x},{(len(block.data)//2):x}"364 )365 if not current_memory or current_memory[0] == "E":366 raise ValueError("Can't read back memory")367 reply = self.pass_through(368 f"M{block.address:x},{len(block.data)//2:x}:" + block.data369 )370 if reply != "OK":371 raise ValueError(372 f"Can't restore memory block ranging from 0x{block.address:x} to 0x{block.address+len(block.data):x}."373 )374 watch_addr = self.watchpoint_triggered(block, current_memory)375 if watch_addr is not None:376 stop_reasons.append(377 [("reason", "watchpoint"), ("watch", f"{watch_addr:x}")]378 )379 if stop_reasons:380 pairs = ";".join(f"{key}:{value}" for key, value in stop_reasons[0])381 return f"T05thread:{snapshot.thread_id:x};{pairs};"382 return None383 384 def reverse_step(self):385 if not self.snapshots:386 self.logger.debug("Reverse-step at history boundary")387 return self.history_boundary_reply(self.get_current_thread())388 self.logger.debug("Reverse-step started")389 snapshot = self.snapshots.pop()390 stop_reply = self.restore_snapshot(snapshot)391 self.set_current_thread(snapshot.thread_id)392 self.logger.debug("Reverse-step stopped")393 if stop_reply is None:394 return self.singlestep_stop_reply(snapshot.thread_id)395 return stop_reply396 397 def reverse_continue(self):398 self.logger.debug("Reverse-continue started")399 thread_id = None400 while self.snapshots:401 snapshot = self.snapshots.pop()402 stop_reply = self.restore_snapshot(snapshot)403 thread_id = snapshot.thread_id404 if stop_reply is not None:405 self.set_current_thread(thread_id)406 self.logger.debug("Reverse-continue stopped")407 return stop_reply408 if thread_id is None:409 thread_id = self.get_current_thread()410 else:411 self.set_current_thread(snapshot.thread_id)412 self.logger.debug("Reverse-continue stopped at history boundary")413 return self.history_boundary_reply(thread_id)414 415 def get_current_thread(self):416 reply = self.pass_through("qC")417 return self.parse_thread_id(reply[2:])418 419 def parse_thread_id(self, thread_id):420 m = re.match("([0-9a-f]+)", thread_id)421 if m is None:422 raise ValueError("Invalid thread ID: " + thread_id)423 return int(m.group(1), 16)424 425 def history_boundary_reply(self, thread_id):426 return f"T00thread:{thread_id:x};replaylog:begin;"427 428 def singlestep_stop_reply(self, thread_id):429 return f"T05thread:{thread_id:x};"430 431 def set_current_thread(self, thread_id):432 """433 Set current thread in inner gdbserver.434 """435 if thread_id >= 0:436 self.pass_through(f"Hg{thread_id:x}")437 self.pass_through(f"Hc{thread_id:x}")438 else:439 self.pass_through(f"Hc-1")440 self.pass_through(f"Hg-1")441 442 def get_register(self, register_info, registers):443 if register_info.bitsize % 8 != 0:444 raise ValueError("Register size must be a multiple of 8 bits")445 if register_info.lldb_index not in registers:446 raise ValueError("Register value not captured")447 data = registers[register_info.lldb_index]448 num_bytes = register_info.bitsize // 8449 bytes = []450 for i in range(0, num_bytes):451 bytes.append(int(data[i * 2 : (i + 1) * 2], 16))452 if register_info.little_endian:453 bytes.reverse()454 result = 0455 for byte in bytes:456 result = (result << 8) + byte457 return result458 459 def get_memory_region_info(self, addr):460 reply = self.pass_through(f"qMemoryRegionInfo:{addr:x}")461 if not reply or reply[0] == "E":462 raise RuntimeError("Failed to get memory region info.")463 464 # Valid reply looks like:465 # start:fffcf000;size:21000;permissions:rw;flags:;name:5b737461636b5d;466 values = [v for v in reply.strip().split(";") if v]467 region_info = {}468 for value in values:469 key, value = value.split(470 ":",471 )472 region_info[key] = value473 474 if not ("start" in region_info and "size" in region_info):475 raise RuntimeError("Did not get extent of memory region.")476 477 region_info["start"] = int(region_info["start"], 16)478 region_info["size"] = int(region_info["size"], 16)479 480 return region_info481 482 def read_memory(self, start_addr, end_addr):483 """484 Read a region of memory from the target.485 486 Some of the addresses may extend into memory we cannot read, skip those.487 488 Return a list of blocks containing the valid area(s) in the489 requested range.490 """491 regions = []492 start_addr = start_addr - (start_addr % BLOCK_SIZE)493 if end_addr % BLOCK_SIZE > 0:494 end_addr = end_addr - (end_addr % BLOCK_SIZE) + BLOCK_SIZE495 for addr in range(start_addr, end_addr, BLOCK_SIZE):496 reply = self.pass_through(f"m{addr:x},{(BLOCK_SIZE - 1):x}")497 if reply and reply[0] != "E":498 block = MemoryBlockSnapshot(addr, reply)499 regions.append(block)500 return regions501 502 def ensure_register_info(self):503 if self.general_purpose_register_info is not None:504 return505 reply = self.pass_through("qHostInfo")506 little_endian = any(507 kv == ("endian", "little") for kv in self.parse_pairs(reply)508 )509 self.general_purpose_register_info = {}510 lldb_index = 0511 while True:512 reply = self.pass_through(f"qRegisterInfo{lldb_index:x}")513 if not reply or reply[0] == "E":514 break515 info = {k: v for k, v in self.parse_pairs(reply)}516 reg_info = RegisterInfo(517 lldb_index, info["name"], int(info["bitsize"]), little_endian518 )519 if (520 info["set"] == "General Purpose Registers"521 and not "container-regs" in info522 ):523 self.general_purpose_register_info[lldb_index] = reg_info524 if "generic" in info:525 if info["generic"] == "pc":526 self.pc_register_info = reg_info527 elif info["generic"] == "sp":528 self.sp_register_info = reg_info529 lldb_index += 1530 if self.pc_register_info is None or self.sp_register_info is None:531 raise ValueError("Can't find generic pc or sp register")532 533 def get_thread_list(self):534 threads = []535 reply = self.pass_through("qfThreadInfo")536 while True:537 if not reply:538 raise ValueError("Missing reply packet")539 if reply[0] == "m":540 for id in reply[1:].split(","):541 threads.append(self.parse_thread_id(id))542 elif reply[0] == "l":543 return threads544 reply = self.pass_through("qsThreadInfo")545