| # 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): | |
| vm = self.get_vm(name='record' if record else 'replay') | |
| vm.set_console() | |
| if record: | |
| self.log.info('recording the execution...') | |
| mode = 'record' | |
| else: | |
| self.log.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, big_endian=False): | |
| from qemu_test import GDB | |
| self.require_accelerator("tcg") | |
| # create qcow2 for snapshots | |
| self.log.info('creating qcow2 image for VM snapshots') | |
| image_path = os.path.join(self.workdir, 'disk.qcow2') | |
| qemu_img = get_qemu_img(self) | |
| out = check_output([qemu_img, 'create', '-f', 'qcow2', image_path, '128M'], | |
| encoding='utf8') | |
| self.log.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() | |
| self.log.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: | |
| self.log.info('Connecting to gdbstub...') | |
| gdb_cmd = os.getenv('QEMU_TEST_GDB') | |
| gdb = GDB(gdb_cmd) | |
| try: | |
| if big_endian: | |
| gdb.cli("set endian big") | |
| self.reverse_debugging_run(gdb, vm, port, gdb_arch, last_icount) | |
| finally: | |
| self.log.info('exiting gdb and qemu') | |
| gdb.exit() | |
| vm.shutdown() | |
| self.log.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, gdb, vm, port, gdb_arch, last_icount): | |
| 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") | |
| self.log.info('stepping forward') | |
| steps = [] | |
| # record first instruction addresses | |
| for _ in range(self.STEPS): | |
| pc = self.get_pc(gdb) | |
| self.log.info('saving position %x' % pc) | |
| steps.append(pc) | |
| gdb.cli("stepi") | |
| # visit the recorded instruction in reverse order | |
| self.log.info('stepping backward') | |
| for addr in steps[::-1]: | |
| self.log.info('found position %x' % addr) | |
| gdb.cli("reverse-stepi") | |
| pc = self.get_pc(gdb) | |
| if pc != addr: | |
| self.log.info('Invalid PC (read %x instead of %x)' % (pc, addr)) | |
| self.fail('Reverse stepping failed!') | |
| # visit the recorded instruction in forward order | |
| self.log.info('stepping forward') | |
| for addr in steps: | |
| self.log.info('found position %x' % addr) | |
| pc = self.get_pc(gdb) | |
| if pc != addr: | |
| self.log.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 | |
| self.log.info('setting breakpoints') | |
| for addr in steps: | |
| gdb.cli(f"break *{hex(addr)}") | |
| # this may hit a breakpoint if first instructions are executed | |
| # again | |
| self.log.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: | |
| self.log.info('reached the end (icount %s)' % (last_icount - 1)) | |
| else: | |
| self.log.info('hit a breakpoint again at %x (icount %s)' % | |
| (self.get_pc(gdb), self.vm_get_icount(vm))) | |
| self.log.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!") | |
| self.log.info('successfully reached %x' % steps[-1]) |