1# Reverse debugging test 2# 3# Copyright (c) 2020 ISP RAS 4# 5# Author: 6# Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru> 7# 8# This work is licensed under the terms of the GNU GPL, version 2 or 9# later. See the COPYING file in the top-level directory. 10import os 11import logging 12 13from avocado import skipUnless 14from avocado_qemu import BUILD_DIR 15from avocado.utils import datadrainer 16from avocado.utils import gdb 17from avocado.utils import process 18from avocado.utils.network.ports import find_free_port 19from avocado.utils.path import find_command 20from boot_linux_console import LinuxKernelTest 21 22class ReverseDebugging(LinuxKernelTest): 23 """ 24 Test GDB reverse debugging commands: reverse step and reverse continue. 25 Recording saves the execution of some instructions and makes an initial 26 VM snapshot to allow reverse execution. 27 Replay saves the order of the first instructions and then checks that they 28 are executed backwards in the correct order. 29 After that the execution is replayed to the end, and reverse continue 30 command is checked by setting several breakpoints, and asserting 31 that the execution is stopped at the last of them. 32 """ 33 34 timeout = 10 35 STEPS = 10 36 endian_is_le = True 37 38 def run_vm(self, record, shift, args, replay_path, image_path, port): 39 logger = logging.getLogger('replay') 40 vm = self.get_vm() 41 vm.set_console() 42 if record: 43 logger.info('recording the execution...') 44 mode = 'record' 45 else: 46 logger.info('replaying the execution...') 47 mode = 'replay' 48 vm.add_args('-gdb', 'tcp::%d' % port, '-S') 49 vm.add_args('-icount', 'shift=%s,rr=%s,rrfile=%s,rrsnapshot=init' % 50 (shift, mode, replay_path), 51 '-net', 'none') 52 vm.add_args('-drive', 'file=%s,if=none' % image_path) 53 if args: 54 vm.add_args(*args) 55 vm.launch() 56 console_drainer = datadrainer.LineLogger(vm.console_socket.fileno(), 57 logger=self.log.getChild('console'), 58 stop_check=(lambda : not vm.is_running())) 59 console_drainer.start() 60 return vm 61 62 @staticmethod 63 def get_reg_le(g, reg): 64 res = g.cmd(b'p%x' % reg) 65 num = 0 66 for i in range(len(res))[-2::-2]: 67 num = 0x100 * num + int(res[i:i + 2], 16) 68 return num 69 70 @staticmethod 71 def get_reg_be(g, reg): 72 res = g.cmd(b'p%x' % reg) 73 return int(res, 16) 74 75 def get_reg(self, g, reg): 76 # value may be encoded in BE or LE order 77 if self.endian_is_le: 78 return self.get_reg_le(g, reg) 79 else: 80 return self.get_reg_be(g, reg) 81 82 def get_pc(self, g): 83 return self.get_reg(g, self.REG_PC) 84 85 def check_pc(self, g, addr): 86 pc = self.get_pc(g) 87 if pc != addr: 88 self.fail('Invalid PC (read %x instead of %x)' % (pc, addr)) 89 90 @staticmethod 91 def gdb_step(g): 92 g.cmd(b's', b'T05thread:01;') 93 94 @staticmethod 95 def gdb_bstep(g): 96 g.cmd(b'bs', b'T05thread:01;') 97 98 @staticmethod 99 def vm_get_icount(vm): 100 return vm.qmp('query-replay')['return']['icount'] 101 102 def reverse_debugging(self, shift=7, args=None): 103 logger = logging.getLogger('replay') 104 105 # create qcow2 for snapshots 106 logger.info('creating qcow2 image for VM snapshots') 107 image_path = os.path.join(self.workdir, 'disk.qcow2') 108 qemu_img = os.path.join(BUILD_DIR, 'qemu-img') 109 if not os.path.exists(qemu_img): 110 qemu_img = find_command('qemu-img', False) 111 if qemu_img is False: 112 self.cancel('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 port = find_free_port() 119 120 # record the log 121 vm = self.run_vm(True, shift, args, replay_path, image_path, port) 122 while self.vm_get_icount(vm) <= self.STEPS: 123 pass 124 last_icount = self.vm_get_icount(vm) 125 vm.shutdown() 126 127 logger.info("recorded log with %s+ steps" % last_icount) 128 129 # replay and run debug commands 130 vm = self.run_vm(False, shift, args, replay_path, image_path, port) 131 logger.info('connecting to gdbstub') 132 g = gdb.GDBRemote('127.0.0.1', port, False, False) 133 g.connect() 134 r = g.cmd(b'qSupported') 135 if b'qXfer:features:read+' in r: 136 g.cmd(b'qXfer:features:read:target.xml:0,ffb') 137 if b'ReverseStep+' not in r: 138 self.fail('Reverse step is not supported by QEMU') 139 if b'ReverseContinue+' not in r: 140 self.fail('Reverse continue is not supported by QEMU') 141 142 logger.info('stepping forward') 143 steps = [] 144 # record first instruction addresses 145 for _ in range(self.STEPS): 146 pc = self.get_pc(g) 147 logger.info('saving position %x' % pc) 148 steps.append(pc) 149 self.gdb_step(g) 150 151 # visit the recorded instruction in reverse order 152 logger.info('stepping backward') 153 for addr in steps[::-1]: 154 self.gdb_bstep(g) 155 self.check_pc(g, addr) 156 logger.info('found position %x' % addr) 157 158 # visit the recorded instruction in forward order 159 logger.info('stepping forward') 160 for addr in steps: 161 self.check_pc(g, addr) 162 self.gdb_step(g) 163 logger.info('found position %x' % addr) 164 165 # set breakpoints for the instructions just stepped over 166 logger.info('setting breakpoints') 167 for addr in steps: 168 # hardware breakpoint at addr with len=1 169 g.cmd(b'Z1,%x,1' % addr, b'OK') 170 171 # this may hit a breakpoint if first instructions are executed 172 # again 173 logger.info('continuing execution') 174 vm.qmp('replay-break', icount=last_icount - 1) 175 # continue - will return after pausing 176 # This could stop at the end and get a T02 return, or by 177 # re-executing one of the breakpoints and get a T05 return. 178 g.cmd(b'c') 179 if self.vm_get_icount(vm) == last_icount - 1: 180 logger.info('reached the end (icount %s)' % (last_icount - 1)) 181 else: 182 logger.info('hit a breakpoint again at %x (icount %s)' % 183 (self.get_pc(g), self.vm_get_icount(vm))) 184 185 logger.info('running reverse continue to reach %x' % steps[-1]) 186 # reverse continue - will return after stopping at the breakpoint 187 g.cmd(b'bc', b'T05thread:01;') 188 189 # assume that none of the first instructions is executed again 190 # breaking the order of the breakpoints 191 self.check_pc(g, steps[-1]) 192 logger.info('successfully reached %x' % steps[-1]) 193 194 logger.info('exiting gdb and qemu') 195 vm.shutdown() 196 197class ReverseDebugging_X86_64(ReverseDebugging): 198 """ 199 :avocado: tags=accel:tcg 200 """ 201 202 REG_PC = 0x10 203 REG_CS = 0x12 204 def get_pc(self, g): 205 return self.get_reg_le(g, self.REG_PC) \ 206 + self.get_reg_le(g, self.REG_CS) * 0x10 207 208 # unidentified gitlab timeout problem 209 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 210 211 def test_x86_64_pc(self): 212 """ 213 :avocado: tags=arch:x86_64 214 :avocado: tags=machine:pc 215 """ 216 # start with BIOS only 217 self.reverse_debugging() 218 219class ReverseDebugging_AArch64(ReverseDebugging): 220 """ 221 :avocado: tags=accel:tcg 222 """ 223 224 REG_PC = 32 225 226 # unidentified gitlab timeout problem 227 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 228 229 def test_aarch64_virt(self): 230 """ 231 :avocado: tags=arch:aarch64 232 :avocado: tags=machine:virt 233 :avocado: tags=cpu:cortex-a53 234 """ 235 kernel_url = ('https://archives.fedoraproject.org/pub/archive/fedora' 236 '/linux/releases/29/Everything/aarch64/os/images/pxeboot' 237 '/vmlinuz') 238 kernel_hash = '8c73e469fc6ea06a58dc83a628fc695b693b8493' 239 kernel_path = self.fetch_asset(kernel_url, asset_hash=kernel_hash) 240 241 self.reverse_debugging( 242 args=('-kernel', kernel_path)) 243 244class ReverseDebugging_ppc64(ReverseDebugging): 245 """ 246 :avocado: tags=accel:tcg 247 """ 248 249 REG_PC = 0x40 250 251 # unidentified gitlab timeout problem 252 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 253 254 def test_ppc64_pseries(self): 255 """ 256 :avocado: tags=arch:ppc64 257 :avocado: tags=machine:pseries 258 :avocado: tags=flaky 259 """ 260 # SLOF branches back to its entry point, which causes this test 261 # to take the 'hit a breakpoint again' path. That's not a problem, 262 # just slightly different than the other machines. 263 self.endian_is_le = False 264 self.reverse_debugging() 265 266 # See https://gitlab.com/qemu-project/qemu/-/issues/1992 267 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 268 269 def test_ppc64_powernv(self): 270 """ 271 :avocado: tags=arch:ppc64 272 :avocado: tags=machine:powernv 273 :avocado: tags=flaky 274 """ 275 self.endian_is_le = False 276 self.reverse_debugging() 277