]>
Commit | Line | Data |
---|---|---|
be52eca3 PD |
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 | ||
13 | from avocado import skipIf | |
14 | from avocado_qemu import BUILD_DIR | |
15 | from avocado.utils import gdb | |
16 | from avocado.utils import process | |
0395b194 | 17 | from avocado.utils.network.ports import find_free_port |
be52eca3 PD |
18 | from avocado.utils.path import find_command |
19 | from boot_linux_console import LinuxKernelTest | |
20 | ||
21 | class 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 | ||
0395b194 | 37 | def run_vm(self, record, shift, args, replay_path, image_path, port): |
be52eca3 PD |
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' | |
0395b194 | 47 | vm.add_args('-gdb', 'tcp::%d' % port, '-S') |
be52eca3 PD |
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') | |
0395b194 | 113 | port = find_free_port() |
be52eca3 PD |
114 | |
115 | # record the log | |
0395b194 | 116 | vm = self.run_vm(True, shift, args, replay_path, image_path, port) |
be52eca3 PD |
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 | |
0395b194 | 125 | vm = self.run_vm(False, shift, args, replay_path, image_path, port) |
be52eca3 | 126 | logger.info('connecting to gdbstub') |
0395b194 | 127 | g = gdb.GDBRemote('127.0.0.1', port, False, False) |
be52eca3 PD |
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 | logger.info('seeking to the end (icount %s)' % (last_icount - 1)) | |
154 | vm.qmp('replay-break', icount=last_icount - 1) | |
155 | # continue - will return after pausing | |
156 | g.cmd(b'c', b'T02thread:01;') | |
157 | ||
158 | logger.info('setting breakpoints') | |
159 | for addr in steps: | |
160 | # hardware breakpoint at addr with len=1 | |
161 | g.cmd(b'Z1,%x,1' % addr, b'OK') | |
162 | ||
163 | logger.info('running reverse continue to reach %x' % steps[-1]) | |
164 | # reverse continue - will return after stopping at the breakpoint | |
165 | g.cmd(b'bc', b'T05thread:01;') | |
166 | ||
167 | # assume that none of the first instructions is executed again | |
168 | # breaking the order of the breakpoints | |
169 | self.check_pc(g, steps[-1]) | |
170 | logger.info('successfully reached %x' % steps[-1]) | |
171 | ||
172 | logger.info('exitting gdb and qemu') | |
173 | vm.shutdown() | |
174 | ||
175 | class ReverseDebugging_X86_64(ReverseDebugging): | |
176 | REG_PC = 0x10 | |
177 | REG_CS = 0x12 | |
178 | def get_pc(self, g): | |
179 | return self.get_reg_le(g, self.REG_PC) \ | |
180 | + self.get_reg_le(g, self.REG_CS) * 0x10 | |
181 | ||
182 | # unidentified gitlab timeout problem | |
183 | @skipIf(os.getenv('GITLAB_CI'), 'Running on GitLab') | |
184 | def test_x86_64_pc(self): | |
185 | """ | |
186 | :avocado: tags=arch:x86_64 | |
187 | :avocado: tags=machine:pc | |
188 | """ | |
189 | # start with BIOS only | |
190 | self.reverse_debugging() | |
191 | ||
192 | class ReverseDebugging_AArch64(ReverseDebugging): | |
193 | REG_PC = 32 | |
194 | ||
195 | # unidentified gitlab timeout problem | |
196 | @skipIf(os.getenv('GITLAB_CI'), 'Running on GitLab') | |
197 | def test_aarch64_virt(self): | |
198 | """ | |
199 | :avocado: tags=arch:aarch64 | |
200 | :avocado: tags=machine:virt | |
201 | :avocado: tags=cpu:cortex-a53 | |
202 | """ | |
203 | kernel_url = ('https://archives.fedoraproject.org/pub/archive/fedora' | |
204 | '/linux/releases/29/Everything/aarch64/os/images/pxeboot' | |
205 | '/vmlinuz') | |
206 | kernel_hash = '8c73e469fc6ea06a58dc83a628fc695b693b8493' | |
207 | kernel_path = self.fetch_asset(kernel_url, asset_hash=kernel_hash) | |
208 | ||
209 | self.reverse_debugging( | |
210 | args=('-kernel', kernel_path, '-cpu', 'cortex-a53')) |