reverse_debugging.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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.
  10. import os
  11. import logging
  12. from avocado import skipUnless
  13. from avocado_qemu import BUILD_DIR
  14. from avocado.utils import datadrainer
  15. from avocado.utils import gdb
  16. from avocado.utils import process
  17. from avocado.utils.network.ports import find_free_port
  18. from avocado.utils.path import find_command
  19. from boot_linux_console import LinuxKernelTest
  20. class ReverseDebugging(LinuxKernelTest):
  21. """
  22. Test GDB reverse debugging commands: reverse step and reverse continue.
  23. Recording saves the execution of some instructions and makes an initial
  24. VM snapshot to allow reverse execution.
  25. Replay saves the order of the first instructions and then checks that they
  26. are executed backwards in the correct order.
  27. After that the execution is replayed to the end, and reverse continue
  28. command is checked by setting several breakpoints, and asserting
  29. that the execution is stopped at the last of them.
  30. """
  31. timeout = 10
  32. STEPS = 10
  33. endian_is_le = True
  34. def run_vm(self, record, shift, args, replay_path, image_path, port):
  35. logger = logging.getLogger('replay')
  36. vm = self.get_vm()
  37. vm.set_console()
  38. if record:
  39. logger.info('recording the execution...')
  40. mode = 'record'
  41. else:
  42. logger.info('replaying the execution...')
  43. mode = 'replay'
  44. vm.add_args('-gdb', 'tcp::%d' % port, '-S')
  45. vm.add_args('-icount', 'shift=%s,rr=%s,rrfile=%s,rrsnapshot=init' %
  46. (shift, mode, replay_path),
  47. '-net', 'none')
  48. vm.add_args('-drive', 'file=%s,if=none' % image_path)
  49. if args:
  50. vm.add_args(*args)
  51. vm.launch()
  52. console_drainer = datadrainer.LineLogger(vm.console_socket.fileno(),
  53. logger=self.log.getChild('console'),
  54. stop_check=(lambda : not vm.is_running()))
  55. console_drainer.start()
  56. return vm
  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. @staticmethod
  65. def get_reg_be(g, reg):
  66. res = g.cmd(b'p%x' % reg)
  67. return int(res, 16)
  68. def get_reg(self, g, reg):
  69. # value may be encoded in BE or LE order
  70. if self.endian_is_le:
  71. return self.get_reg_le(g, reg)
  72. else:
  73. return self.get_reg_be(g, reg)
  74. def get_pc(self, g):
  75. return self.get_reg(g, self.REG_PC)
  76. def check_pc(self, g, addr):
  77. pc = self.get_pc(g)
  78. if pc != addr:
  79. self.fail('Invalid PC (read %x instead of %x)' % (pc, addr))
  80. @staticmethod
  81. def gdb_step(g):
  82. g.cmd(b's', b'T05thread:01;')
  83. @staticmethod
  84. def gdb_bstep(g):
  85. g.cmd(b'bs', b'T05thread:01;')
  86. @staticmethod
  87. def vm_get_icount(vm):
  88. return vm.qmp('query-replay')['return']['icount']
  89. def reverse_debugging(self, shift=7, args=None):
  90. logger = logging.getLogger('replay')
  91. # create qcow2 for snapshots
  92. logger.info('creating qcow2 image for VM snapshots')
  93. image_path = os.path.join(self.workdir, 'disk.qcow2')
  94. qemu_img = os.path.join(BUILD_DIR, 'qemu-img')
  95. if not os.path.exists(qemu_img):
  96. qemu_img = find_command('qemu-img', False)
  97. if qemu_img is False:
  98. self.cancel('Could not find "qemu-img", which is required to '
  99. 'create the temporary qcow2 image')
  100. cmd = '%s create -f qcow2 %s 128M' % (qemu_img, image_path)
  101. process.run(cmd)
  102. replay_path = os.path.join(self.workdir, 'replay.bin')
  103. port = find_free_port()
  104. # record the log
  105. vm = self.run_vm(True, shift, args, replay_path, image_path, port)
  106. while self.vm_get_icount(vm) <= self.STEPS:
  107. pass
  108. last_icount = self.vm_get_icount(vm)
  109. vm.shutdown()
  110. logger.info("recorded log with %s+ steps" % last_icount)
  111. # replay and run debug commands
  112. vm = self.run_vm(False, shift, args, replay_path, image_path, port)
  113. logger.info('connecting to gdbstub')
  114. g = gdb.GDBRemote('127.0.0.1', port, False, False)
  115. g.connect()
  116. r = g.cmd(b'qSupported')
  117. if b'qXfer:features:read+' in r:
  118. g.cmd(b'qXfer:features:read:target.xml:0,ffb')
  119. if b'ReverseStep+' not in r:
  120. self.fail('Reverse step is not supported by QEMU')
  121. if b'ReverseContinue+' not in r:
  122. self.fail('Reverse continue is not supported by QEMU')
  123. logger.info('stepping forward')
  124. steps = []
  125. # record first instruction addresses
  126. for _ in range(self.STEPS):
  127. pc = self.get_pc(g)
  128. logger.info('saving position %x' % pc)
  129. steps.append(pc)
  130. self.gdb_step(g)
  131. # visit the recorded instruction in reverse order
  132. logger.info('stepping backward')
  133. for addr in steps[::-1]:
  134. self.gdb_bstep(g)
  135. self.check_pc(g, addr)
  136. logger.info('found position %x' % addr)
  137. # visit the recorded instruction in forward order
  138. logger.info('stepping forward')
  139. for addr in steps:
  140. self.check_pc(g, addr)
  141. self.gdb_step(g)
  142. logger.info('found position %x' % addr)
  143. # set breakpoints for the instructions just stepped over
  144. logger.info('setting breakpoints')
  145. for addr in steps:
  146. # hardware breakpoint at addr with len=1
  147. g.cmd(b'Z1,%x,1' % addr, b'OK')
  148. # this may hit a breakpoint if first instructions are executed
  149. # again
  150. logger.info('continuing execution')
  151. vm.qmp('replay-break', icount=last_icount - 1)
  152. # continue - will return after pausing
  153. # This could stop at the end and get a T02 return, or by
  154. # re-executing one of the breakpoints and get a T05 return.
  155. g.cmd(b'c')
  156. if self.vm_get_icount(vm) == last_icount - 1:
  157. logger.info('reached the end (icount %s)' % (last_icount - 1))
  158. else:
  159. logger.info('hit a breakpoint again at %x (icount %s)' %
  160. (self.get_pc(g), self.vm_get_icount(vm)))
  161. logger.info('running reverse continue to reach %x' % steps[-1])
  162. # reverse continue - will return after stopping at the breakpoint
  163. g.cmd(b'bc', b'T05thread:01;')
  164. # assume that none of the first instructions is executed again
  165. # breaking the order of the breakpoints
  166. self.check_pc(g, steps[-1])
  167. logger.info('successfully reached %x' % steps[-1])
  168. logger.info('exiting gdb and qemu')
  169. vm.shutdown()
  170. class ReverseDebugging_X86_64(ReverseDebugging):
  171. """
  172. :avocado: tags=accel:tcg
  173. """
  174. REG_PC = 0x10
  175. REG_CS = 0x12
  176. def get_pc(self, g):
  177. return self.get_reg_le(g, self.REG_PC) \
  178. + self.get_reg_le(g, self.REG_CS) * 0x10
  179. # unidentified gitlab timeout problem
  180. @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
  181. def test_x86_64_pc(self):
  182. """
  183. :avocado: tags=arch:x86_64
  184. :avocado: tags=machine:pc
  185. """
  186. # start with BIOS only
  187. self.reverse_debugging()
  188. class ReverseDebugging_AArch64(ReverseDebugging):
  189. """
  190. :avocado: tags=accel:tcg
  191. """
  192. REG_PC = 32
  193. # unidentified gitlab timeout problem
  194. @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
  195. def test_aarch64_virt(self):
  196. """
  197. :avocado: tags=arch:aarch64
  198. :avocado: tags=machine:virt
  199. :avocado: tags=cpu:cortex-a53
  200. """
  201. kernel_url = ('https://archives.fedoraproject.org/pub/archive/fedora'
  202. '/linux/releases/29/Everything/aarch64/os/images/pxeboot'
  203. '/vmlinuz')
  204. kernel_hash = '8c73e469fc6ea06a58dc83a628fc695b693b8493'
  205. kernel_path = self.fetch_asset(kernel_url, asset_hash=kernel_hash)
  206. self.reverse_debugging(
  207. args=('-kernel', kernel_path))
  208. class ReverseDebugging_ppc64(ReverseDebugging):
  209. """
  210. :avocado: tags=accel:tcg
  211. """
  212. REG_PC = 0x40
  213. # unidentified gitlab timeout problem
  214. @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
  215. def test_ppc64_pseries(self):
  216. """
  217. :avocado: tags=arch:ppc64
  218. :avocado: tags=machine:pseries
  219. :avocado: tags=flaky
  220. """
  221. # SLOF branches back to its entry point, which causes this test
  222. # to take the 'hit a breakpoint again' path. That's not a problem,
  223. # just slightly different than the other machines.
  224. self.endian_is_le = False
  225. self.reverse_debugging()
  226. # See https://gitlab.com/qemu-project/qemu/-/issues/1992
  227. @skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
  228. def test_ppc64_powernv(self):
  229. """
  230. :avocado: tags=arch:ppc64
  231. :avocado: tags=machine:powernv
  232. :avocado: tags=flaky
  233. """
  234. self.endian_is_le = False
  235. self.reverse_debugging()