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 def test_x86_64_pc(self): 211 """ 212 :avocado: tags=arch:x86_64 213 :avocado: tags=machine:pc 214 """ 215 # start with BIOS only 216 self.reverse_debugging() 217 218class ReverseDebugging_AArch64(ReverseDebugging): 219 """ 220 :avocado: tags=accel:tcg 221 """ 222 223 REG_PC = 32 224 225 # unidentified gitlab timeout problem 226 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 227 def test_aarch64_virt(self): 228 """ 229 :avocado: tags=arch:aarch64 230 :avocado: tags=machine:virt 231 :avocado: tags=cpu:cortex-a53 232 """ 233 kernel_url = ('https://archives.fedoraproject.org/pub/archive/fedora' 234 '/linux/releases/29/Everything/aarch64/os/images/pxeboot' 235 '/vmlinuz') 236 kernel_hash = '8c73e469fc6ea06a58dc83a628fc695b693b8493' 237 kernel_path = self.fetch_asset(kernel_url, asset_hash=kernel_hash) 238 239 self.reverse_debugging( 240 args=('-kernel', kernel_path)) 241 242class ReverseDebugging_ppc64(ReverseDebugging): 243 """ 244 :avocado: tags=accel:tcg 245 """ 246 247 REG_PC = 0x40 248 249 # unidentified gitlab timeout problem 250 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 251 def test_ppc64_pseries(self): 252 """ 253 :avocado: tags=arch:ppc64 254 :avocado: tags=machine:pseries 255 :avocado: tags=flaky 256 """ 257 # SLOF branches back to its entry point, which causes this test 258 # to take the 'hit a breakpoint again' path. That's not a problem, 259 # just slightly different than the other machines. 260 self.endian_is_le = False 261 self.reverse_debugging() 262 263 # See https://gitlab.com/qemu-project/qemu/-/issues/1992 264 @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab') 265 def test_ppc64_powernv(self): 266 """ 267 :avocado: tags=arch:ppc64 268 :avocado: tags=machine:powernv 269 :avocado: tags=flaky 270 """ 271 self.endian_is_le = False 272 self.reverse_debugging() 273