xref: /qemu/tests/functional/reverse_debugging.py (revision 951ededf12a89534195cf5c5210242a169a85656)
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