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('exitting 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