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