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