aboutsummaryrefslogtreecommitdiff
path: root/tests/functional/reverse_debugging.py
blob: 68cfcb398564ad2a585a6804debdc20fdd57de17 (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
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Reverse debugging test
#
# Copyright (c) 2020 ISP RAS
# Copyright (c) 2025 Linaro Limited
#
# Author:
#  Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru>
#  Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado)
#
# This work is licensed under the terms of the GNU GPL, version 2 or
# later.  See the COPYING file in the top-level directory.

import logging
import os
from subprocess import check_output

from qemu_test import LinuxKernelTest, get_qemu_img, GDB, \
    skipIfMissingEnv, skipIfMissingImports
from qemu_test.ports import Ports


class ReverseDebugging(LinuxKernelTest):
    """
    Test GDB reverse debugging commands: reverse step and reverse continue.
    Recording saves the execution of some instructions and makes an initial
    VM snapshot to allow reverse execution.
    Replay saves the order of the first instructions and then checks that they
    are executed backwards in the correct order.
    After that the execution is replayed to the end, and reverse continue
    command is checked by setting several breakpoints, and asserting
    that the execution is stopped at the last of them.
    """

    STEPS = 10

    def run_vm(self, record, shift, args, replay_path, image_path, port):
        logger = logging.getLogger('replay')
        vm = self.get_vm(name='record' if record else 'replay')
        vm.set_console()
        if record:
            logger.info('recording the execution...')
            mode = 'record'
        else:
            logger.info('replaying the execution...')
            mode = 'replay'
            vm.add_args('-gdb', 'tcp::%d' % port, '-S')
        vm.add_args('-icount', 'shift=%s,rr=%s,rrfile=%s,rrsnapshot=init' %
                    (shift, mode, replay_path),
                    '-net', 'none')
        vm.add_args('-drive', 'file=%s,if=none' % image_path)
        if args:
            vm.add_args(*args)
        vm.launch()
        return vm

    @staticmethod
    def get_pc(gdb: GDB):
        return gdb.cli("print $pc").get_addr()

    @staticmethod
    def vm_get_icount(vm):
        return vm.qmp('query-replay')['return']['icount']

    @skipIfMissingImports("pygdbmi") # Required by GDB class
    @skipIfMissingEnv("QEMU_TEST_GDB")
    def reverse_debugging(self, gdb_arch, shift=7, args=None):
        from qemu_test import GDB

        logger = logging.getLogger('replay')

        # create qcow2 for snapshots
        logger.info('creating qcow2 image for VM snapshots')
        image_path = os.path.join(self.workdir, 'disk.qcow2')
        qemu_img = get_qemu_img(self)
        if qemu_img is None:
            self.skipTest('Could not find "qemu-img", which is required to '
                          'create the temporary qcow2 image')
        out = check_output([qemu_img, 'create', '-f', 'qcow2', image_path, '128M'],
                           encoding='utf8')
        logger.info("qemu-img: %s" % out)

        replay_path = os.path.join(self.workdir, 'replay.bin')

        # record the log
        vm = self.run_vm(True, shift, args, replay_path, image_path, -1)
        while self.vm_get_icount(vm) <= self.STEPS:
            pass
        last_icount = self.vm_get_icount(vm)
        vm.shutdown()

        logger.info("recorded log with %s+ steps" % last_icount)

        # replay and run debug commands
        with Ports() as ports:
            port = ports.find_free_port()
            vm = self.run_vm(False, shift, args, replay_path, image_path, port)

        try:
            logger.info('Connecting to gdbstub...')
            self.reverse_debugging_run(vm, port, gdb_arch, last_icount)
            logger.info('Test passed.')
        except GDB.TimeoutError:
            # Convert a GDB timeout exception into a unittest failure exception.
            raise self.failureException("Timeout while connecting to or "
                                        "communicating with gdbstub...") from None
        except Exception:
            # Re-throw exceptions from unittest, like the ones caused by fail(),
            # skipTest(), etc.
            raise

    def reverse_debugging_run(self, vm, port, gdb_arch, last_icount):
        logger = logging.getLogger('replay')

        gdb_cmd = os.getenv('QEMU_TEST_GDB')
        gdb = GDB(gdb_cmd)

        r = gdb.cli("set architecture").get_log()
        if gdb_arch not in r:
            self.skipTest(f"GDB does not support arch '{gdb_arch}'")

        gdb.cli("set debug remote 1")

        c = gdb.cli(f"target remote localhost:{port}").get_console()
        if not f"Remote debugging using localhost:{port}" in c:
            self.fail("Could not connect to gdbstub!")

        # Remote debug messages are in 'log' payloads.
        r = gdb.get_log()
        if 'ReverseStep+' not in r:
            self.fail('Reverse step is not supported by QEMU')
        if 'ReverseContinue+' not in r:
            self.fail('Reverse continue is not supported by QEMU')

        gdb.cli("set debug remote 0")

        logger.info('stepping forward')
        steps = []
        # record first instruction addresses
        for _ in range(self.STEPS):
            pc = self.get_pc(gdb)
            logger.info('saving position %x' % pc)
            steps.append(pc)
            gdb.cli("stepi")

        # visit the recorded instruction in reverse order
        logger.info('stepping backward')
        for addr in steps[::-1]:
            logger.info('found position %x' % addr)
            gdb.cli("reverse-stepi")
            pc = self.get_pc(gdb)
            if pc != addr:
                logger.info('Invalid PC (read %x instead of %x)' % (pc, addr))
                self.fail('Reverse stepping failed!')

        # visit the recorded instruction in forward order
        logger.info('stepping forward')
        for addr in steps:
            logger.info('found position %x' % addr)
            pc = self.get_pc(gdb)
            if pc != addr:
                logger.info('Invalid PC (read %x instead of %x)' % (pc, addr))
                self.fail('Forward stepping failed!')
            gdb.cli("stepi")

        # set breakpoints for the instructions just stepped over
        logger.info('setting breakpoints')
        for addr in steps:
            gdb.cli(f"break *{hex(addr)}")

        # this may hit a breakpoint if first instructions are executed
        # again
        logger.info('continuing execution')
        vm.qmp('replay-break', icount=last_icount - 1)
        # continue - will return after pausing
        # This can stop at the end of the replay-break and gdb gets a SIGINT,
        # or by re-executing one of the breakpoints and gdb stops at a
        # breakpoint.
        gdb.cli("continue")

        if self.vm_get_icount(vm) == last_icount - 1:
            logger.info('reached the end (icount %s)' % (last_icount - 1))
        else:
            logger.info('hit a breakpoint again at %x (icount %s)' %
                        (self.get_pc(gdb), self.vm_get_icount(vm)))

        logger.info('running reverse continue to reach %x' % steps[-1])
        # reverse continue - will return after stopping at the breakpoint
        gdb.cli("reverse-continue")

        # assume that none of the first instructions is executed again
        # breaking the order of the breakpoints
        pc = self.get_pc(gdb)
        if pc != steps[-1]:
            self.fail("'reverse-continue' did not hit the first PC in reverse order!")

        logger.info('successfully reached %x' % steps[-1])

        logger.info('exiting gdb and qemu')
        gdb.exit()
        vm.shutdown()