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