xref: /openbmc/qemu/tests/functional/reverse_debugging.py (revision 18f6f30b0089b470f3e737637a86dfb81ebd6eae)
1# SPDX-License-Identifier: GPL-2.0-or-later
2#
3# Reverse debugging test
4#
5# Copyright (c) 2020 ISP RAS
6# Copyright (c) 2025 Linaro Limited
7#
8# Author:
9#  Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru>
10#  Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado)
11#
12# This work is licensed under the terms of the GNU GPL, version 2 or
13# later.  See the COPYING file in the top-level directory.
14
15import logging
16import os
17from subprocess import check_output
18
19from qemu_test import LinuxKernelTest, get_qemu_img, GDB, \
20    skipIfMissingEnv, skipIfMissingImports
21from qemu_test.ports import Ports
22
23
24class ReverseDebugging(LinuxKernelTest):
25    """
26    Test GDB reverse debugging commands: reverse step and reverse continue.
27    Recording saves the execution of some instructions and makes an initial
28    VM snapshot to allow reverse execution.
29    Replay saves the order of the first instructions and then checks that they
30    are executed backwards in the correct order.
31    After that the execution is replayed to the end, and reverse continue
32    command is checked by setting several breakpoints, and asserting
33    that the execution is stopped at the last of them.
34    """
35
36    STEPS = 10
37
38    def run_vm(self, record, shift, args, replay_path, image_path, port):
39        vm = self.get_vm(name='record' if record else 'replay')
40        vm.set_console()
41        if record:
42            self.log.info('recording the execution...')
43            mode = 'record'
44        else:
45            self.log.info('replaying the execution...')
46            mode = 'replay'
47            vm.add_args('-gdb', 'tcp::%d' % port, '-S')
48        vm.add_args('-icount', 'shift=%s,rr=%s,rrfile=%s,rrsnapshot=init' %
49                    (shift, mode, replay_path),
50                    '-net', 'none')
51        vm.add_args('-drive', 'file=%s,if=none' % image_path)
52        if args:
53            vm.add_args(*args)
54        vm.launch()
55        return vm
56
57    @staticmethod
58    def get_pc(gdb: GDB):
59        return gdb.cli("print $pc").get_addr()
60
61    @staticmethod
62    def vm_get_icount(vm):
63        return vm.qmp('query-replay')['return']['icount']
64
65    @skipIfMissingImports("pygdbmi") # Required by GDB class
66    @skipIfMissingEnv("QEMU_TEST_GDB")
67    def reverse_debugging(self, gdb_arch, shift=7, args=None):
68        from qemu_test import GDB
69
70        # create qcow2 for snapshots
71        self.log.info('creating qcow2 image for VM snapshots')
72        image_path = os.path.join(self.workdir, 'disk.qcow2')
73        qemu_img = get_qemu_img(self)
74        if qemu_img is None:
75            self.skipTest('Could not find "qemu-img", which is required to '
76                          'create the temporary qcow2 image')
77        out = check_output([qemu_img, 'create', '-f', 'qcow2', image_path, '128M'],
78                           encoding='utf8')
79        self.log.info("qemu-img: %s" % out)
80
81        replay_path = os.path.join(self.workdir, 'replay.bin')
82
83        # record the log
84        vm = self.run_vm(True, shift, args, replay_path, image_path, -1)
85        while self.vm_get_icount(vm) <= self.STEPS:
86            pass
87        last_icount = self.vm_get_icount(vm)
88        vm.shutdown()
89
90        self.log.info("recorded log with %s+ steps" % last_icount)
91
92        # replay and run debug commands
93        with Ports() as ports:
94            port = ports.find_free_port()
95            vm = self.run_vm(False, shift, args, replay_path, image_path, port)
96
97        try:
98            self.log.info('Connecting to gdbstub...')
99            gdb_cmd = os.getenv('QEMU_TEST_GDB')
100            gdb = GDB(gdb_cmd)
101            try:
102                self.reverse_debugging_run(gdb, vm, port, gdb_arch, last_icount)
103            finally:
104                self.log.info('exiting gdb and qemu')
105                gdb.exit()
106                vm.shutdown()
107            self.log.info('Test passed.')
108        except GDB.TimeoutError:
109            # Convert a GDB timeout exception into a unittest failure exception.
110            raise self.failureException("Timeout while connecting to or "
111                                        "communicating with gdbstub...") from None
112        except Exception:
113            # Re-throw exceptions from unittest, like the ones caused by fail(),
114            # skipTest(), etc.
115            raise
116
117    def reverse_debugging_run(self, gdb, vm, port, gdb_arch, last_icount):
118        r = gdb.cli("set architecture").get_log()
119        if gdb_arch not in r:
120            self.skipTest(f"GDB does not support arch '{gdb_arch}'")
121
122        gdb.cli("set debug remote 1")
123
124        c = gdb.cli(f"target remote localhost:{port}").get_console()
125        if not f"Remote debugging using localhost:{port}" in c:
126            self.fail("Could not connect to gdbstub!")
127
128        # Remote debug messages are in 'log' payloads.
129        r = gdb.get_log()
130        if 'ReverseStep+' not in r:
131            self.fail('Reverse step is not supported by QEMU')
132        if 'ReverseContinue+' not in r:
133            self.fail('Reverse continue is not supported by QEMU')
134
135        gdb.cli("set debug remote 0")
136
137        self.log.info('stepping forward')
138        steps = []
139        # record first instruction addresses
140        for _ in range(self.STEPS):
141            pc = self.get_pc(gdb)
142            self.log.info('saving position %x' % pc)
143            steps.append(pc)
144            gdb.cli("stepi")
145
146        # visit the recorded instruction in reverse order
147        self.log.info('stepping backward')
148        for addr in steps[::-1]:
149            self.log.info('found position %x' % addr)
150            gdb.cli("reverse-stepi")
151            pc = self.get_pc(gdb)
152            if pc != addr:
153                self.log.info('Invalid PC (read %x instead of %x)' % (pc, addr))
154                self.fail('Reverse stepping failed!')
155
156        # visit the recorded instruction in forward order
157        self.log.info('stepping forward')
158        for addr in steps:
159            self.log.info('found position %x' % addr)
160            pc = self.get_pc(gdb)
161            if pc != addr:
162                self.log.info('Invalid PC (read %x instead of %x)' % (pc, addr))
163                self.fail('Forward stepping failed!')
164            gdb.cli("stepi")
165
166        # set breakpoints for the instructions just stepped over
167        self.log.info('setting breakpoints')
168        for addr in steps:
169            gdb.cli(f"break *{hex(addr)}")
170
171        # this may hit a breakpoint if first instructions are executed
172        # again
173        self.log.info('continuing execution')
174        vm.qmp('replay-break', icount=last_icount - 1)
175        # continue - will return after pausing
176        # This can stop at the end of the replay-break and gdb gets a SIGINT,
177        # or by re-executing one of the breakpoints and gdb stops at a
178        # breakpoint.
179        gdb.cli("continue")
180
181        if self.vm_get_icount(vm) == last_icount - 1:
182            self.log.info('reached the end (icount %s)' % (last_icount - 1))
183        else:
184            self.log.info('hit a breakpoint again at %x (icount %s)' %
185                        (self.get_pc(gdb), self.vm_get_icount(vm)))
186
187        self.log.info('running reverse continue to reach %x' % steps[-1])
188        # reverse continue - will return after stopping at the breakpoint
189        gdb.cli("reverse-continue")
190
191        # assume that none of the first instructions is executed again
192        # breaking the order of the breakpoints
193        pc = self.get_pc(gdb)
194        if pc != steps[-1]:
195            self.fail("'reverse-continue' did not hit the first PC in reverse order!")
196
197        self.log.info('successfully reached %x' % steps[-1])
198