qemu/tests/avocado/reverse_debugging.py
<<
>>
Prefs
   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        logger.info('seeking to the end (icount %s)' % (last_icount - 1))
 154        vm.qmp('replay-break', icount=last_icount - 1)
 155        # continue - will return after pausing
 156        g.cmd(b'c', b'T02thread:01;')
 157
 158        logger.info('setting breakpoints')
 159        for addr in steps:
 160            # hardware breakpoint at addr with len=1
 161            g.cmd(b'Z1,%x,1' % addr, b'OK')
 162
 163        logger.info('running reverse continue to reach %x' % steps[-1])
 164        # reverse continue - will return after stopping at the breakpoint
 165        g.cmd(b'bc', b'T05thread:01;')
 166
 167        # assume that none of the first instructions is executed again
 168        # breaking the order of the breakpoints
 169        self.check_pc(g, steps[-1])
 170        logger.info('successfully reached %x' % steps[-1])
 171
 172        logger.info('exitting gdb and qemu')
 173        vm.shutdown()
 174
 175class ReverseDebugging_X86_64(ReverseDebugging):
 176    REG_PC = 0x10
 177    REG_CS = 0x12
 178    def get_pc(self, g):
 179        return self.get_reg_le(g, self.REG_PC) \
 180            + self.get_reg_le(g, self.REG_CS) * 0x10
 181
 182    # unidentified gitlab timeout problem
 183    @skipIf(os.getenv('GITLAB_CI'), 'Running on GitLab')
 184    def test_x86_64_pc(self):
 185        """
 186        :avocado: tags=arch:x86_64
 187        :avocado: tags=machine:pc
 188        """
 189        # start with BIOS only
 190        self.reverse_debugging()
 191
 192class ReverseDebugging_AArch64(ReverseDebugging):
 193    REG_PC = 32
 194
 195    # unidentified gitlab timeout problem
 196    @skipIf(os.getenv('GITLAB_CI'), 'Running on GitLab')
 197    def test_aarch64_virt(self):
 198        """
 199        :avocado: tags=arch:aarch64
 200        :avocado: tags=machine:virt
 201        :avocado: tags=cpu:cortex-a53
 202        """
 203        kernel_url = ('https://archives.fedoraproject.org/pub/archive/fedora'
 204                      '/linux/releases/29/Everything/aarch64/os/images/pxeboot'
 205                      '/vmlinuz')
 206        kernel_hash = '8c73e469fc6ea06a58dc83a628fc695b693b8493'
 207        kernel_path = self.fetch_asset(kernel_url, asset_hash=kernel_hash)
 208
 209        self.reverse_debugging(
 210            args=('-kernel', kernel_path))
 211