master
py 198 lines 7.37 KB
Raw
1 # SPDX-License-Identifier: GPL-2.0-or-later
2 #
3 # Reverse debugging test
4 #
5 # Copyright (c) 2020 ISP RAS
6 # Copyright (c) 2025 Linaro Limited
7 #
8 # Author:
9 # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru>
10 # Gustavo Romero <gustavo.romero@linaro.org> (Run without Avocado)
11 #
12 # This work is licensed under the terms of the GNU GPL, version 2 or
13 # later. See the COPYING file in the top-level directory.
14
15 import logging
16 import os
17 from subprocess import check_output
18
19 from qemu_test import LinuxKernelTest, get_qemu_img, GDB, \
20 skipIfMissingEnv, skipIfMissingImports
21 from qemu_test.ports import Ports
22
23
24 class ReverseDebugging(LinuxKernelTest):
25 """
26 Test GDB reverse debugging commands: reverse step and reverse continue.
27 Recording saves the execution of some instructions and makes an initial
28 VM snapshot to allow reverse execution.
29 Replay saves the order of the first instructions and then checks that they
30 are executed backwards in the correct order.
31 After that the execution is replayed to the end, and reverse continue
32 command is checked by setting several breakpoints, and asserting
33 that the execution is stopped at the last of them.
34 """
35
36 STEPS = 10
37
38 def run_vm(self, record, shift, args, replay_path, image_path, port):
39 vm = self.get_vm(name='record' if record else 'replay')
40 vm.set_console()
41 if record:
42 self.log.info('recording the execution...')
43 mode = 'record'
44 else:
45 self.log.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_pc(gdb: GDB):
59 return gdb.cli("print $pc").get_addr()
60
61 @staticmethod
62 def vm_get_icount(vm):
63 return vm.qmp('query-replay')['return']['icount']
64
65 @skipIfMissingImports("pygdbmi") # Required by GDB class
66 @skipIfMissingEnv("QEMU_TEST_GDB")
67 def reverse_debugging(self, gdb_arch, shift=7, args=None, big_endian=False):
68 from qemu_test import GDB
69
70 self.require_accelerator("tcg")
71
72 # create qcow2 for snapshots
73 self.log.info('creating qcow2 image for VM snapshots')
74 image_path = os.path.join(self.workdir, 'disk.qcow2')
75 qemu_img = get_qemu_img(self)
76 out = check_output([qemu_img, 'create', '-f', 'qcow2', image_path, '128M'],
77 encoding='utf8')
78 self.log.info("qemu-img: %s" % out)
79
80 replay_path = os.path.join(self.workdir, 'replay.bin')
81
82 # record the log
83 vm = self.run_vm(True, shift, args, replay_path, image_path, -1)
84 while self.vm_get_icount(vm) <= self.STEPS:
85 pass
86 last_icount = self.vm_get_icount(vm)
87 vm.shutdown()
88
89 self.log.info("recorded log with %s+ steps" % last_icount)
90
91 # replay and run debug commands
92 with Ports() as ports:
93 port = ports.find_free_port()
94 vm = self.run_vm(False, shift, args, replay_path, image_path, port)
95
96 try:
97 self.log.info('Connecting to gdbstub...')
98 gdb_cmd = os.getenv('QEMU_TEST_GDB')
99 gdb = GDB(gdb_cmd)
100 try:
101 if big_endian:
102 gdb.cli("set endian big")
103 self.reverse_debugging_run(gdb, vm, port, gdb_arch, last_icount)
104 finally:
105 self.log.info('exiting gdb and qemu')
106 gdb.exit()
107 vm.shutdown()
108 self.log.info('Test passed.')
109 except GDB.TimeoutError:
110 # Convert a GDB timeout exception into a unittest failure exception.
111 raise self.failureException("Timeout while connecting to or "
112 "communicating with gdbstub...") from None
113 except Exception:
114 # Re-throw exceptions from unittest, like the ones caused by fail(),
115 # skipTest(), etc.
116 raise
117
118 def reverse_debugging_run(self, gdb, vm, port, gdb_arch, last_icount):
119 r = gdb.cli("set architecture").get_log()
120 if gdb_arch not in r:
121 self.skipTest(f"GDB does not support arch '{gdb_arch}'")
122
123 gdb.cli("set debug remote 1")
124
125 c = gdb.cli(f"target remote localhost:{port}").get_console()
126 if not f"Remote debugging using localhost:{port}" in c:
127 self.fail("Could not connect to gdbstub!")
128
129 # Remote debug messages are in 'log' payloads.
130 r = gdb.get_log()
131 if 'ReverseStep+' not in r:
132 self.fail('Reverse step is not supported by QEMU')
133 if 'ReverseContinue+' not in r:
134 self.fail('Reverse continue is not supported by QEMU')
135
136 gdb.cli("set debug remote 0")
137
138 self.log.info('stepping forward')
139 steps = []
140 # record first instruction addresses
141 for _ in range(self.STEPS):
142 pc = self.get_pc(gdb)
143 self.log.info('saving position %x' % pc)
144 steps.append(pc)
145 gdb.cli("stepi")
146
147 # visit the recorded instruction in reverse order
148 self.log.info('stepping backward')
149 for addr in steps[::-1]:
150 self.log.info('found position %x' % addr)
151 gdb.cli("reverse-stepi")
152 pc = self.get_pc(gdb)
153 if pc != addr:
154 self.log.info('Invalid PC (read %x instead of %x)' % (pc, addr))
155 self.fail('Reverse stepping failed!')
156
157 # visit the recorded instruction in forward order
158 self.log.info('stepping forward')
159 for addr in steps:
160 self.log.info('found position %x' % addr)
161 pc = self.get_pc(gdb)
162 if pc != addr:
163 self.log.info('Invalid PC (read %x instead of %x)' % (pc, addr))
164 self.fail('Forward stepping failed!')
165 gdb.cli("stepi")
166
167 # set breakpoints for the instructions just stepped over
168 self.log.info('setting breakpoints')
169 for addr in steps:
170 gdb.cli(f"break *{hex(addr)}")
171
172 # this may hit a breakpoint if first instructions are executed
173 # again
174 self.log.info('continuing execution')
175 vm.qmp('replay-break', icount=last_icount - 1)
176 # continue - will return after pausing
177 # This can stop at the end of the replay-break and gdb gets a SIGINT,
178 # or by re-executing one of the breakpoints and gdb stops at a
179 # breakpoint.
180 gdb.cli("continue")
181
182 if self.vm_get_icount(vm) == last_icount - 1:
183 self.log.info('reached the end (icount %s)' % (last_icount - 1))
184 else:
185 self.log.info('hit a breakpoint again at %x (icount %s)' %
186 (self.get_pc(gdb), self.vm_get_icount(vm)))
187
188 self.log.info('running reverse continue to reach %x' % steps[-1])
189 # reverse continue - will return after stopping at the breakpoint
190 gdb.cli("reverse-continue")
191
192 # assume that none of the first instructions is executed again
193 # breaking the order of the breakpoints
194 pc = self.get_pc(gdb)
195 if pc != steps[-1]:
196 self.fail("'reverse-continue' did not hit the first PC in reverse order!")
197
198 self.log.info('successfully reached %x' % steps[-1])