brintos

brintos / llvm-project-archived public Read only

0
0
Text · 21.0 KiB · d9a8dab Raw
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