aboutsummaryrefslogtreecommitdiff
path: root/lldb/packages/Python/lldbsuite/test/lldbreverse.py
blob: d9a8daba3772d3416551bb942122c3705e06507f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
import os
import os.path
import lldb
from lldbsuite.test.lldbtest import *
from lldbsuite.test.gdbclientutils import *
from lldbsuite.test.lldbgdbproxy import *
import lldbgdbserverutils
import re


class ThreadSnapshot:
    def __init__(self, thread_id, registers):
        self.thread_id = thread_id
        self.registers = registers


class MemoryBlockSnapshot:
    def __init__(self, address, data):
        self.address = address
        self.data = data


class StateSnapshot:
    def __init__(self, thread_snapshots, memory):
        self.thread_snapshots = thread_snapshots
        self.memory = memory
        self.thread_id = None


class RegisterInfo:
    def __init__(self, lldb_index, name, bitsize, little_endian):
        self.lldb_index = lldb_index
        self.name = name
        self.bitsize = bitsize
        self.little_endian = little_endian


BELOW_STACK_POINTER = 16384
ABOVE_STACK_POINTER = 4096

BLOCK_SIZE = 1024

SOFTWARE_BREAKPOINTS = 0
HARDWARE_BREAKPOINTS = 1
WRITE_WATCHPOINTS = 2


class ReverseTestBase(GDBProxyTestBase):
    """
    Base class for tests that need reverse execution.

    This class uses a gdbserver proxy to add very limited reverse-
    execution capability to lldb-server/debugserver for testing
    purposes only.

    To use this class, run the inferior forward until some stopping point.
    Then call `start_recording()` and execute forward again until reaching
    a software breakpoint; this class records the state before each execution executes.
    At that point, the server will accept "bc" and "bs" packets to step
    backwards through the state.
    When executing during recording, we only allow single-step and continue without
    delivering a signal, and only software breakpoint stops are allowed.

    We assume that while recording is enabled, the only effects of instructions
    are on general-purpose registers (read/written by the 'g' and 'G' packets)
    and on memory bytes between [SP - BELOW_STACK_POINTER, SP + ABOVE_STACK_POINTER).
    """

    NO_DEBUG_INFO_TESTCASE = True

    """
    A list of StateSnapshots in time order.

    There is one snapshot per single-stepped instruction,
    representing the state before that instruction was
    executed. The last snapshot in the list is the
    snapshot before the last instruction was executed.
    This is an undo log; we snapshot a superset of the state that may have
    been changed by the instruction's execution.
    """
    snapshots = None
    recording_enabled = False

    breakpoints = None

    pc_register_info = None
    sp_register_info = None
    general_purpose_register_info = None

    def __init__(self, *args, **kwargs):
        GDBProxyTestBase.__init__(self, *args, **kwargs)
        self.breakpoints = [set(), set(), set(), set(), set()]

    def respond(self, packet):
        if not packet:
            raise ValueError("Invalid empty packet")
        if packet == self.server.PACKET_INTERRUPT:
            # Don't send a response. We'll just run to completion.
            return []
        if self.is_command(packet, "qSupported", ":"):
            # Disable multiprocess support in the server and in LLDB
            # since Mac debugserver doesn't support it and we want lldb-server to
            # be consistent with that
            reply = self.pass_through(packet.replace(";multiprocess", ""))
            return reply.replace(";multiprocess", "") + ";ReverseStep+;ReverseContinue+"
        if packet == "c" or packet == "s":
            packet = "vCont;" + packet
        elif (
            packet[0] == "c" or packet[0] == "s" or packet[0] == "C" or packet[0] == "S"
        ):
            raise ValueError(
                "Old-style continuation packets with address or signal not supported yet"
            )
        if self.is_command(packet, "vCont", ";"):
            if self.recording_enabled:
                return self.continue_with_recording(packet)
            snapshots = []
        if packet == "bc":
            return self.reverse_continue()
        if packet == "bs":
            return self.reverse_step()
        if packet == "jThreadsInfo":
            # Suppress this because it contains thread stop reasons which we might
            # need to modify, and we don't want to have to implement that.
            return ""
        if packet[0] == "x":
            # Suppress *binary* reads as results starting with "O" can be mistaken for an output packet
            # by the test server code
            return ""
        if packet[0] == "z" or packet[0] == "Z":
            reply = self.pass_through(packet)
            if reply == "OK":
                self.update_breakpoints(packet)
            return reply
        return GDBProxyTestBase.respond(self, packet)

    def start_recording(self):
        self.recording_enabled = True
        self.snapshots = []

    def stop_recording(self):
        """
        Don't record when executing foward.

        Reverse execution is still supported until the next forward continue.
        """
        self.recording_enabled = False

    def is_command(self, packet, cmd, follow_token):
        return packet == cmd or packet[0 : len(cmd) + 1] == cmd + follow_token

    def update_breakpoints(self, packet):
        m = re.match("([zZ])([01234]),([0-9a-f]+),([0-9a-f]+)", packet)
        if m is None:
            raise ValueError("Invalid breakpoint packet: " + packet)
        t = int(m.group(2))
        addr = int(m.group(3), 16)
        kind = int(m.group(4), 16)
        if m.group(1) == "Z":
            self.breakpoints[t].add((addr, kind))
        else:
            self.breakpoints[t].discard((addr, kind))

    def breakpoint_triggered_at(self, pc):
        if any(addr == pc for addr, kind in self.breakpoints[SOFTWARE_BREAKPOINTS]):
            return True
        if any(addr == pc for addr, kind in self.breakpoints[HARDWARE_BREAKPOINTS]):
            return True
        return False

    def watchpoint_triggered(self, new_value_block, current_contents):
        """Returns the address or None."""
        for watch_addr, kind in self.breakpoints[WRITE_WATCHPOINTS]:
            for offset in range(0, kind):
                addr = watch_addr + offset
                if (
                    addr >= new_value_block.address
                    and addr < new_value_block.address + len(new_value_block.data)
                ):
                    index = addr - new_value_block.address
                    if (
                        new_value_block.data[index * 2 : (index + 1) * 2]
                        != current_contents[index * 2 : (index + 1) * 2]
                    ):
                        return watch_addr
        return None

    def continue_with_recording(self, packet):
        self.logger.debug("Continue with recording enabled")

        step_packet = "vCont;s"
        if packet == "vCont":
            requested_step = False
        else:
            m = re.match("vCont;(c|s)(.*)", packet)
            if m is None:
                raise ValueError("Unsupported vCont packet: " + packet)
            requested_step = m.group(1) == "s"
            step_packet += m.group(2)

        while True:
            snapshot = self.capture_snapshot()
            reply = self.pass_through(step_packet)
            (stop_signal, stop_pairs) = self.parse_stop_reply(reply)
            if stop_signal != 5:
                raise ValueError("Unexpected stop signal: " + reply)
            is_swbreak = False
            thread_id = None
            for key, value in stop_pairs.items():
                if key == "thread":
                    thread_id = self.parse_thread_id(value)
                    continue
                if re.match("[0-9a-f]+", key):
                    continue
                if key == "swbreak" or (key == "reason" and value == "breakpoint"):
                    is_swbreak = True
                    continue
                if key == "metype":
                    reason = self.stop_reason_from_mach_exception(stop_pairs)
                    if reason == "breakpoint":
                        is_swbreak = True
                    elif reason != "singlestep":
                        raise ValueError(f"Unsupported stop reason in {reply}")
                    continue
                if key in [
                    "name",
                    "threads",
                    "thread-pcs",
                    "reason",
                    "mecount",
                    "medata",
                    "memory",
                ]:
                    continue
                raise ValueError(f"Unknown stop key '{key}' in {reply}")
            if is_swbreak:
                self.logger.debug("Recording stopped")
                return reply
            if thread_id is None:
                return ValueError("Expected thread ID: " + reply)
            snapshot.thread_id = thread_id
            self.snapshots.append(snapshot)
            if requested_step:
                self.logger.debug("Recording stopped for step")
                return reply

    def stop_reason_from_mach_exception(self, stop_pairs):
        # See StopInfoMachException::CreateStopReasonWithMachException.
        if int(stop_pairs["metype"]) != 6:  # EXC_BREAKPOINT
            raise ValueError(f"Unsupported exception type {value} in {reply}")
        medata = stop_pairs["medata"]
        arch = self.getArchitecture()
        if arch in ["amd64", "i386", "x86_64"]:
            if int(medata[0], 16) == 2:
                return "breakpoint"
            if int(medata[0], 16) == 1 and int(medata[1], 16) == 0:
                return "singlestep"
        elif arch in ["arm64", "arm64e"]:
            if int(medata[0], 16) == 1 and int(medata[1], 16) != 0:
                return "breakpoint"
            elif int(medata[0], 16) == 1 and int(medata[1], 16) == 0:
                return "singlestep"
        else:
            raise ValueError(f"Unsupported architecture '{arch}'")
        raise ValueError(f"Unsupported exception details in {reply}")

    def parse_stop_reply(self, reply):
        if not reply:
            raise ValueError("Invalid empty packet")
        if reply[0] == "T" and len(reply) >= 3:
            result = {}
            for k, v in self.parse_pairs(reply[3:]):
                if k in ["medata", "memory"]:
                    if k in result:
                        result[k].append(v)
                    else:
                        result[k] = [v]
                else:
                    result[k] = v
            return (int(reply[1:3], 16), result)
        raise ValueError("Unsupported stop reply: " + reply)

    def parse_pairs(self, text):
        for pair in text.split(";"):
            if not pair:
                continue
            m = re.match("([^:]+):(.*)", pair)
            if m is None:
                raise ValueError("Invalid pair text: " + text)
            yield (m.group(1), m.group(2))

    def capture_snapshot(self):
        """Snapshot all threads and their stack memories."""
        self.ensure_register_info()
        current_thread = self.get_current_thread()
        thread_snapshots = []
        memory = []
        for thread_id in self.get_thread_list():
            registers = {}
            for index in sorted(self.general_purpose_register_info.keys()):
                reply = self.pass_through(f"p{index:x};thread:{thread_id:x};")
                if reply == "" or reply[0] == "E":
                    # Mac debugserver tells us about registers that it won't let
                    # us actually read. Ignore those registers.
                    self.logger.debug(f"Failed to read register {index:x}")
                    continue
                registers[index] = reply
            thread_snapshot = ThreadSnapshot(thread_id, registers)
            thread_sp = self.get_register(
                self.sp_register_info, thread_snapshot.registers
            )

            # The memory above or below the stack pointer may be mapped, but not
            # both readable and writeable. For example on Arm 32-bit Linux, there
            # is a "[vectors]" mapping above the stack, which can be read but not
            # written to.
            #
            # Therefore, we should limit any reads to the stack region, which we
            # know is readable and writeable.
            region_info = self.get_memory_region_info(thread_sp)
            lower = max(thread_sp - BELOW_STACK_POINTER, region_info["start"])
            upper = min(
                thread_sp + ABOVE_STACK_POINTER,
                region_info["start"] + region_info["size"],
            )

            memory += self.read_memory(lower, upper)
            thread_snapshots.append(thread_snapshot)
        self.set_current_thread(current_thread)
        return StateSnapshot(thread_snapshots, memory)

    def restore_snapshot(self, snapshot):
        """
        Restore the snapshot during reverse execution.

        If this triggers a breakpoint or watchpoint, return the stop reply,
        otherwise None.
        """
        current_thread = self.get_current_thread()
        stop_reasons = []
        for thread_snapshot in snapshot.thread_snapshots:
            thread_id = thread_snapshot.thread_id
            for lldb_index in sorted(thread_snapshot.registers.keys()):
                data = thread_snapshot.registers[lldb_index]
                reply = self.pass_through(
                    f"P{lldb_index:x}={data};thread:{thread_id:x};"
                )
                if reply != "OK":
                    try:
                        reg_name = self.general_purpose_register_info[lldb_index].name
                    except KeyError:
                        reg_name = f"with index {lldb_index}"
                    raise ValueError(f"Can't restore thread register {reg_name}")
            if thread_id == snapshot.thread_id:
                new_pc = self.get_register(
                    self.pc_register_info, thread_snapshot.registers
                )
                if self.breakpoint_triggered_at(new_pc):
                    stop_reasons.append([("reason", "breakpoint")])
        self.set_current_thread(current_thread)
        for block in snapshot.memory:
            current_memory = self.pass_through(
                f"m{block.address:x},{(len(block.data)//2):x}"
            )
            if not current_memory or current_memory[0] == "E":
                raise ValueError("Can't read back memory")
            reply = self.pass_through(
                f"M{block.address:x},{len(block.data)//2:x}:" + block.data
            )
            if reply != "OK":
                raise ValueError(
                    f"Can't restore memory block ranging from 0x{block.address:x} to 0x{block.address+len(block.data):x}."
                )
            watch_addr = self.watchpoint_triggered(block, current_memory)
            if watch_addr is not None:
                stop_reasons.append(
                    [("reason", "watchpoint"), ("watch", f"{watch_addr:x}")]
                )
        if stop_reasons:
            pairs = ";".join(f"{key}:{value}" for key, value in stop_reasons[0])
            return f"T05thread:{snapshot.thread_id:x};{pairs};"
        return None

    def reverse_step(self):
        if not self.snapshots:
            self.logger.debug("Reverse-step at history boundary")
            return self.history_boundary_reply(self.get_current_thread())
        self.logger.debug("Reverse-step started")
        snapshot = self.snapshots.pop()
        stop_reply = self.restore_snapshot(snapshot)
        self.set_current_thread(snapshot.thread_id)
        self.logger.debug("Reverse-step stopped")
        if stop_reply is None:
            return self.singlestep_stop_reply(snapshot.thread_id)
        return stop_reply

    def reverse_continue(self):
        self.logger.debug("Reverse-continue started")
        thread_id = None
        while self.snapshots:
            snapshot = self.snapshots.pop()
            stop_reply = self.restore_snapshot(snapshot)
            thread_id = snapshot.thread_id
            if stop_reply is not None:
                self.set_current_thread(thread_id)
                self.logger.debug("Reverse-continue stopped")
                return stop_reply
        if thread_id is None:
            thread_id = self.get_current_thread()
        else:
            self.set_current_thread(snapshot.thread_id)
        self.logger.debug("Reverse-continue stopped at history boundary")
        return self.history_boundary_reply(thread_id)

    def get_current_thread(self):
        reply = self.pass_through("qC")
        return self.parse_thread_id(reply[2:])

    def parse_thread_id(self, thread_id):
        m = re.match("([0-9a-f]+)", thread_id)
        if m is None:
            raise ValueError("Invalid thread ID: " + thread_id)
        return int(m.group(1), 16)

    def history_boundary_reply(self, thread_id):
        return f"T00thread:{thread_id:x};replaylog:begin;"

    def singlestep_stop_reply(self, thread_id):
        return f"T05thread:{thread_id:x};"

    def set_current_thread(self, thread_id):
        """
        Set current thread in inner gdbserver.
        """
        if thread_id >= 0:
            self.pass_through(f"Hg{thread_id:x}")
            self.pass_through(f"Hc{thread_id:x}")
        else:
            self.pass_through(f"Hc-1")
            self.pass_through(f"Hg-1")

    def get_register(self, register_info, registers):
        if register_info.bitsize % 8 != 0:
            raise ValueError("Register size must be a multiple of 8 bits")
        if register_info.lldb_index not in registers:
            raise ValueError("Register value not captured")
        data = registers[register_info.lldb_index]
        num_bytes = register_info.bitsize // 8
        bytes = []
        for i in range(0, num_bytes):
            bytes.append(int(data[i * 2 : (i + 1) * 2], 16))
        if register_info.little_endian:
            bytes.reverse()
        result = 0
        for byte in bytes:
            result = (result << 8) + byte
        return result

    def get_memory_region_info(self, addr):
        reply = self.pass_through(f"qMemoryRegionInfo:{addr:x}")
        if not reply or reply[0] == "E":
            raise RuntimeError("Failed to get memory region info.")

        # Valid reply looks like:
        # start:fffcf000;size:21000;permissions:rw;flags:;name:5b737461636b5d;
        values = [v for v in reply.strip().split(";") if v]
        region_info = {}
        for value in values:
            key, value = value.split(
                ":",
            )
            region_info[key] = value

        if not ("start" in region_info and "size" in region_info):
            raise RuntimeError("Did not get extent of memory region.")

        region_info["start"] = int(region_info["start"], 16)
        region_info["size"] = int(region_info["size"], 16)

        return region_info

    def read_memory(self, start_addr, end_addr):
        """
        Read a region of memory from the target.

        Some of the addresses may extend into memory we cannot read, skip those.

        Return a list of blocks containing the valid area(s) in the
        requested range.
        """
        regions = []
        start_addr = start_addr - (start_addr % BLOCK_SIZE)
        if end_addr % BLOCK_SIZE > 0:
            end_addr = end_addr - (end_addr % BLOCK_SIZE) + BLOCK_SIZE
        for addr in range(start_addr, end_addr, BLOCK_SIZE):
            reply = self.pass_through(f"m{addr:x},{(BLOCK_SIZE - 1):x}")
            if reply and reply[0] != "E":
                block = MemoryBlockSnapshot(addr, reply)
                regions.append(block)
        return regions

    def ensure_register_info(self):
        if self.general_purpose_register_info is not None:
            return
        reply = self.pass_through("qHostInfo")
        little_endian = any(
            kv == ("endian", "little") for kv in self.parse_pairs(reply)
        )
        self.general_purpose_register_info = {}
        lldb_index = 0
        while True:
            reply = self.pass_through(f"qRegisterInfo{lldb_index:x}")
            if not reply or reply[0] == "E":
                break
            info = {k: v for k, v in self.parse_pairs(reply)}
            reg_info = RegisterInfo(
                lldb_index, info["name"], int(info["bitsize"]), little_endian
            )
            if (
                info["set"] == "General Purpose Registers"
                and not "container-regs" in info
            ):
                self.general_purpose_register_info[lldb_index] = reg_info
            if "generic" in info:
                if info["generic"] == "pc":
                    self.pc_register_info = reg_info
                elif info["generic"] == "sp":
                    self.sp_register_info = reg_info
            lldb_index += 1
        if self.pc_register_info is None or self.sp_register_info is None:
            raise ValueError("Can't find generic pc or sp register")

    def get_thread_list(self):
        threads = []
        reply = self.pass_through("qfThreadInfo")
        while True:
            if not reply:
                raise ValueError("Missing reply packet")
            if reply[0] == "m":
                for id in reply[1:].split(","):
                    threads.append(self.parse_thread_id(id))
            elif reply[0] == "l":
                return threads
            reply = self.pass_through("qsThreadInfo")