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