1# Reverse debugging test 2# 3# SPDX-License-Identifier: GPL-2.0-or-later 4# 5# Copyright (c) 2020 ISP RAS 6# 7# Author: 8# Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> 9# 10# This work is licensed under the terms of the GNU GPL, version 2 or 11# later. See the COPYING file in the top-level directory. 12import os 13import logging 14 15from qemu_test import LinuxKernelTest, get_qemu_img 16from qemu_test.ports import Ports 17 18 19class ReverseDebugging(LinuxKernelTest): 20 """ 21 Test GDB reverse debugging commands: reverse step and reverse continue. 22 Recording saves the execution of some instructions and makes an initial 23 VM snapshot to allow reverse execution. 24 Replay saves the order of the first instructions and then checks that they 25 are executed backwards in the correct order. 26 After that the execution is replayed to the end, and reverse continue 27 command is checked by setting several breakpoints, and asserting 28 that the execution is stopped at the last of them. 29 """ 30 31 timeout = 10 32 STEPS = 10 33 endian_is_le = True 34 35 def run_vm(self, record, shift, args, replay_path, image_path, port): 36 from avocado.utils import datadrainer 37 38 logger = logging.getLogger('replay') 39 vm = self.get_vm(name='record' if record else 'replay') 40 vm.set_console() 41 if record: 42 logger.info('recording the execution...') 43 mode = 'record' 44 else: 45 logger.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 console_drainer = datadrainer.LineLogger(vm.console_socket.fileno(), 56 logger=self.log.getChild('console'), 57 stop_check=(lambda : not vm.is_running())) 58 console_drainer.start() 59 return vm 60 61 @staticmethod 62 def get_reg_le(g, reg): 63 res = g.cmd(b'p%x' % reg) 64 num = 0 65 for i in range(len(res))[-2::-2]: 66 num = 0x100 * num + int(res[i:i + 2], 16) 67 return num 68 69 @staticmethod 70 def get_reg_be(g, reg): 71 res = g.cmd(b'p%x' % reg) 72 return int(res, 16) 73 74 def get_reg(self, g, reg): 75 # value may be encoded in BE or LE order 76 if self.endian_is_le: 77 return self.get_reg_le(g, reg) 78 else: 79 return self.get_reg_be(g, reg) 80 81 def get_pc(self, g): 82 return self.get_reg(g, self.REG_PC) 83 84 def check_pc(self, g, addr): 85 pc = self.get_pc(g) 86 if pc != addr: 87 self.fail('Invalid PC (read %x instead of %x)' % (pc, addr)) 88 89 @staticmethod 90 def gdb_step(g): 91 g.cmd(b's', b'T05thread:01;') 92 93 @staticmethod 94 def gdb_bstep(g): 95 g.cmd(b'bs', b'T05thread:01;') 96 97 @staticmethod 98 def vm_get_icount(vm): 99 return vm.qmp('query-replay')['return']['icount'] 100 101 def reverse_debugging(self, shift=7, args=None): 102 from avocado.utils import gdb 103 from avocado.utils import process 104 105 logger = logging.getLogger('replay') 106 107 # create qcow2 for snapshots 108 logger.info('creating qcow2 image for VM snapshots') 109 image_path = os.path.join(self.workdir, 'disk.qcow2') 110 qemu_img = get_qemu_img(self) 111 if qemu_img is None: 112 self.skipTest('Could not find "qemu-img", which is required to ' 113 'create the temporary qcow2 image') 114 cmd = '%s create -f qcow2 %s 128M' % (qemu_img, image_path) 115 process.run(cmd) 116 117 replay_path = os.path.join(self.workdir, 'replay.bin') 118 119 # record the log 120 vm = self.run_vm(True, shift, args, replay_path, image_path, -1) 121 while self.vm_get_icount(vm) <= self.STEPS: 122 pass 123 last_icount = self.vm_get_icount(vm) 124 vm.shutdown() 125 126 logger.info("recorded log with %s+ steps" % last_icount) 127 128 # replay and run debug commands 129 with Ports() as ports: 130 port = ports.find_free_port() 131 vm = self.run_vm(False, shift, args, replay_path, image_path, port) 132 logger.info('connecting to gdbstub') 133 g = gdb.GDBRemote('127.0.0.1', port, False, False) 134 g.connect() 135 r = g.cmd(b'qSupported') 136 if b'qXfer:features:read+' in r: 137 g.cmd(b'qXfer:features:read:target.xml:0,ffb') 138 if b'ReverseStep+' not in r: 139 self.fail('Reverse step is not supported by QEMU') 140 if b'ReverseContinue+' not in r: 141 self.fail('Reverse continue is not supported by QEMU') 142 143 logger.info('stepping forward') 144 steps = [] 145 # record first instruction addresses 146 for _ in range(self.STEPS): 147 pc = self.get_pc(g) 148 logger.info('saving position %x' % pc) 149 steps.append(pc) 150 self.gdb_step(g) 151 152 # visit the recorded instruction in reverse order 153 logger.info('stepping backward') 154 for addr in steps[::-1]: 155 self.gdb_bstep(g) 156 self.check_pc(g, addr) 157 logger.info('found position %x' % addr) 158 159 # visit the recorded instruction in forward order 160 logger.info('stepping forward') 161 for addr in steps: 162 self.check_pc(g, addr) 163 self.gdb_step(g) 164 logger.info('found position %x' % addr) 165 166 # set breakpoints for the instructions just stepped over 167 logger.info('setting breakpoints') 168 for addr in steps: 169 # hardware breakpoint at addr with len=1 170 g.cmd(b'Z1,%x,1' % addr, b'OK') 171 172 # this may hit a breakpoint if first instructions are executed 173 # again 174 logger.info('continuing execution') 175 vm.qmp('replay-break', icount=last_icount - 1) 176 # continue - will return after pausing 177 # This could stop at the end and get a T02 return, or by 178 # re-executing one of the breakpoints and get a T05 return. 179 g.cmd(b'c') 180 if self.vm_get_icount(vm) == last_icount - 1: 181 logger.info('reached the end (icount %s)' % (last_icount - 1)) 182 else: 183 logger.info('hit a breakpoint again at %x (icount %s)' % 184 (self.get_pc(g), self.vm_get_icount(vm))) 185 186 logger.info('running reverse continue to reach %x' % steps[-1]) 187 # reverse continue - will return after stopping at the breakpoint 188 g.cmd(b'bc', b'T05thread:01;') 189 190 # assume that none of the first instructions is executed again 191 # breaking the order of the breakpoints 192 self.check_pc(g, steps[-1]) 193 logger.info('successfully reached %x' % steps[-1]) 194 195 logger.info('exiting gdb and qemu') 196 vm.shutdown() 197