1 # Reverse debugging test
3 # Copyright (c) 2020 ISP RAS
6 # Pavel Dovgalyuk <Pavel.Dovgalyuk@ispras.ru>
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.
13 from avocado
import skipUnless
14 from avocado_qemu
import BUILD_DIR
15 from avocado
.utils
import datadrainer
16 from avocado
.utils
import gdb
17 from avocado
.utils
import process
18 from avocado
.utils
.network
.ports
import find_free_port
19 from avocado
.utils
.path
import find_command
20 from boot_linux_console
import LinuxKernelTest
22 class ReverseDebugging(LinuxKernelTest
):
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.
38 def run_vm(self
, record
, shift
, args
, replay_path
, image_path
, port
):
39 logger
= logging
.getLogger('replay')
43 logger
.info('recording the execution...')
46 logger
.info('replaying the execution...')
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
),
52 vm
.add_args('-drive', 'file=%s,if=none' % image_path
)
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()
63 def get_reg_le(g
, reg
):
64 res
= g
.cmd(b
'p%x' % reg
)
66 for i
in range(len(res
))[-2::-2]:
67 num
= 0x100 * num
+ int(res
[i
:i
+ 2], 16)
71 def get_reg_be(g
, reg
):
72 res
= g
.cmd(b
'p%x' % reg
)
75 def get_reg(self
, g
, reg
):
76 # value may be encoded in BE or LE order
78 return self
.get_reg_le(g
, reg
)
80 return self
.get_reg_be(g
, reg
)
83 return self
.get_reg(g
, self
.REG_PC
)
85 def check_pc(self
, g
, addr
):
88 self
.fail('Invalid PC (read %x instead of %x)' % (pc
, addr
))
92 g
.cmd(b
's', b
'T05thread:01;')
96 g
.cmd(b
'bs', b
'T05thread:01;')
99 def vm_get_icount(vm
):
100 return vm
.qmp('query-replay')['return']['icount']
102 def reverse_debugging(self
, shift
=7, args
=None):
103 logger
= logging
.getLogger('replay')
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
)
117 replay_path
= os
.path
.join(self
.workdir
, 'replay.bin')
118 port
= find_free_port()
121 vm
= self
.run_vm(True, shift
, args
, replay_path
, image_path
, port
)
122 while self
.vm_get_icount(vm
) <= self
.STEPS
:
124 last_icount
= self
.vm_get_icount(vm
)
127 logger
.info("recorded log with %s+ steps" % last_icount
)
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)
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')
142 logger
.info('stepping forward')
144 # record first instruction addresses
145 for _
in range(self
.STEPS
):
147 logger
.info('saving position %x' % pc
)
151 # visit the recorded instruction in reverse order
152 logger
.info('stepping backward')
153 for addr
in steps
[::-1]:
155 self
.check_pc(g
, addr
)
156 logger
.info('found position %x' % addr
)
158 # visit the recorded instruction in forward order
159 logger
.info('stepping forward')
161 self
.check_pc(g
, addr
)
163 logger
.info('found position %x' % addr
)
165 # set breakpoints for the instructions just stepped over
166 logger
.info('setting breakpoints')
168 # hardware breakpoint at addr with len=1
169 g
.cmd(b
'Z1,%x,1' % addr
, b
'OK')
171 # this may hit a breakpoint if first instructions are executed
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.
179 if self
.vm_get_icount(vm
) == last_icount
- 1:
180 logger
.info('reached the end (icount %s)' % (last_icount
- 1))
182 logger
.info('hit a breakpoint again at %x (icount %s)' %
183 (self
.get_pc(g
), self
.vm_get_icount(vm
)))
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;')
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])
194 logger
.info('exiting gdb and qemu')
197 class ReverseDebugging_X86_64(ReverseDebugging
):
199 :avocado: tags=accel:tcg
205 return self
.get_reg_le(g
, self
.REG_PC
) \
206 + self
.get_reg_le(g
, self
.REG_CS
) * 0x10
208 # unidentified gitlab timeout problem
209 @skipUnless(os
.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
211 def test_x86_64_pc(self
):
213 :avocado: tags=arch:x86_64
214 :avocado: tags=machine:pc
216 # start with BIOS only
217 self
.reverse_debugging()
219 class ReverseDebugging_AArch64(ReverseDebugging
):
221 :avocado: tags=accel:tcg
226 # unidentified gitlab timeout problem
227 @skipUnless(os
.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
229 def test_aarch64_virt(self
):
231 :avocado: tags=arch:aarch64
232 :avocado: tags=machine:virt
233 :avocado: tags=cpu:cortex-a53
235 kernel_url
= ('https://archives.fedoraproject.org/pub/archive/fedora'
236 '/linux/releases/29/Everything/aarch64/os/images/pxeboot'
238 kernel_hash
= '8c73e469fc6ea06a58dc83a628fc695b693b8493'
239 kernel_path
= self
.fetch_asset(kernel_url
, asset_hash
=kernel_hash
)
241 self
.reverse_debugging(
242 args
=('-kernel', kernel_path
))
244 class ReverseDebugging_ppc64(ReverseDebugging
):
246 :avocado: tags=accel:tcg
251 # unidentified gitlab timeout problem
252 @skipUnless(os
.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
254 def test_ppc64_pseries(self
):
256 :avocado: tags=arch:ppc64
257 :avocado: tags=machine:pseries
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()
266 # See https://gitlab.com/qemu-project/qemu/-/issues/1992
267 @skipUnless(os
.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
269 def test_ppc64_powernv(self
):
271 :avocado: tags=arch:ppc64
272 :avocado: tags=machine:powernv
275 self
.endian_is_le
= False
276 self
.reverse_debugging()