]> git.ipfire.org Git - thirdparty/binutils-gdb.git/blob - gdb/testsuite/gdb.python/py-disasm.py
Update copyright year range in header of all files managed by GDB
[thirdparty/binutils-gdb.git] / gdb / testsuite / gdb.python / py-disasm.py
1 # Copyright (C) 2021-2023 Free Software Foundation, Inc.
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 import gdb
17 import gdb.disassembler
18 import struct
19 import sys
20
21 from gdb.disassembler import Disassembler, DisassemblerResult
22
23 # A global, holds the program-counter address at which we should
24 # perform the extra disassembly that this script provides.
25 current_pc = None
26
27
28 def is_nop(s):
29 return s == "nop" or s == "nop\t0"
30
31
32 # Remove all currently registered disassemblers.
33 def remove_all_python_disassemblers():
34 for a in gdb.architecture_names():
35 gdb.disassembler.register_disassembler(None, a)
36 gdb.disassembler.register_disassembler(None, None)
37
38
39 class TestDisassembler(Disassembler):
40 """A base class for disassemblers within this script to inherit from.
41 Implements the __call__ method and ensures we only do any
42 disassembly wrapping for the global CURRENT_PC."""
43
44 def __init__(self):
45 global current_pc
46
47 super().__init__("TestDisassembler")
48 self.__info = None
49 if current_pc == None:
50 raise gdb.GdbError("no current_pc set")
51
52 def __call__(self, info):
53 global current_pc
54
55 if info.address != current_pc:
56 return None
57 self.__info = info
58 return self.disassemble(info)
59
60 def get_info(self):
61 return self.__info
62
63 def disassemble(self, info):
64 raise NotImplementedError("override the disassemble method")
65
66
67 class GlobalPreInfoDisassembler(TestDisassembler):
68 """Check the attributes of DisassembleInfo before disassembly has occurred."""
69
70 def disassemble(self, info):
71 ad = info.address
72 ar = info.architecture
73
74 if ad != current_pc:
75 raise gdb.GdbError("invalid address")
76
77 if not isinstance(ar, gdb.Architecture):
78 raise gdb.GdbError("invalid architecture type")
79
80 result = gdb.disassembler.builtin_disassemble(info)
81
82 text = result.string + "\t## ad = 0x%x, ar = %s" % (ad, ar.name())
83 return DisassemblerResult(result.length, text)
84
85
86 class GlobalPostInfoDisassembler(TestDisassembler):
87 """Check the attributes of DisassembleInfo after disassembly has occurred."""
88
89 def disassemble(self, info):
90 result = gdb.disassembler.builtin_disassemble(info)
91
92 ad = info.address
93 ar = info.architecture
94
95 if ad != current_pc:
96 raise gdb.GdbError("invalid address")
97
98 if not isinstance(ar, gdb.Architecture):
99 raise gdb.GdbError("invalid architecture type")
100
101 text = result.string + "\t## ad = 0x%x, ar = %s" % (ad, ar.name())
102 return DisassemblerResult(result.length, text)
103
104
105 class GlobalReadDisassembler(TestDisassembler):
106 """Check the DisassembleInfo.read_memory method. Calls the builtin
107 disassembler, then reads all of the bytes of this instruction, and
108 adds them as a comment to the disassembler output."""
109
110 def disassemble(self, info):
111 result = gdb.disassembler.builtin_disassemble(info)
112 len = result.length
113 str = ""
114 for o in range(len):
115 if str != "":
116 str += " "
117 v = bytes(info.read_memory(1, o))[0]
118 if sys.version_info[0] < 3:
119 v = struct.unpack("<B", v)
120 str += "0x%02x" % v
121 text = result.string + "\t## bytes = %s" % str
122 return DisassemblerResult(result.length, text)
123
124
125 class GlobalAddrDisassembler(TestDisassembler):
126 """Check the gdb.format_address method."""
127
128 def disassemble(self, info):
129 result = gdb.disassembler.builtin_disassemble(info)
130 arch = info.architecture
131 addr = info.address
132 program_space = info.progspace
133 str = gdb.format_address(addr, program_space, arch)
134 text = result.string + "\t## addr = %s" % str
135 return DisassemblerResult(result.length, text)
136
137
138 class GdbErrorEarlyDisassembler(TestDisassembler):
139 """Raise a GdbError instead of performing any disassembly."""
140
141 def disassemble(self, info):
142 raise gdb.GdbError("GdbError instead of a result")
143
144
145 class RuntimeErrorEarlyDisassembler(TestDisassembler):
146 """Raise a RuntimeError instead of performing any disassembly."""
147
148 def disassemble(self, info):
149 raise RuntimeError("RuntimeError instead of a result")
150
151
152 class GdbErrorLateDisassembler(TestDisassembler):
153 """Raise a GdbError after calling the builtin disassembler."""
154
155 def disassemble(self, info):
156 result = gdb.disassembler.builtin_disassemble(info)
157 raise gdb.GdbError("GdbError after builtin disassembler")
158
159
160 class RuntimeErrorLateDisassembler(TestDisassembler):
161 """Raise a RuntimeError after calling the builtin disassembler."""
162
163 def disassemble(self, info):
164 result = gdb.disassembler.builtin_disassemble(info)
165 raise RuntimeError("RuntimeError after builtin disassembler")
166
167
168 class MemoryErrorEarlyDisassembler(TestDisassembler):
169 """Throw a memory error, ignore the error and disassemble."""
170
171 def disassemble(self, info):
172 tag = "## FAIL"
173 try:
174 info.read_memory(1, -info.address + 2)
175 except gdb.MemoryError:
176 tag = "## AFTER ERROR"
177 result = gdb.disassembler.builtin_disassemble(info)
178 text = result.string + "\t" + tag
179 return DisassemblerResult(result.length, text)
180
181
182 class MemoryErrorLateDisassembler(TestDisassembler):
183 """Throw a memory error after calling the builtin disassembler, but
184 before we return a result."""
185
186 def disassemble(self, info):
187 result = gdb.disassembler.builtin_disassemble(info)
188 # The following read will throw an error.
189 info.read_memory(1, -info.address + 2)
190 return DisassemblerResult(1, "BAD")
191
192
193 class RethrowMemoryErrorDisassembler(TestDisassembler):
194 """Catch and rethrow a memory error."""
195
196 def disassemble(self, info):
197 try:
198 info.read_memory(1, -info.address + 2)
199 except gdb.MemoryError as e:
200 raise gdb.MemoryError("cannot read code at address 0x2")
201 return DisassemblerResult(1, "BAD")
202
203
204 class ResultOfWrongType(TestDisassembler):
205 """Return something that is not a DisassemblerResult from disassemble method"""
206
207 class Blah:
208 def __init__(self, length, string):
209 self.length = length
210 self.string = string
211
212 def disassemble(self, info):
213 return self.Blah(1, "ABC")
214
215
216 class ResultWrapper(gdb.disassembler.DisassemblerResult):
217 def __init__(self, length, string, length_x=None, string_x=None):
218 super().__init__(length, string)
219 if length_x is None:
220 self.__length = length
221 else:
222 self.__length = length_x
223 if string_x is None:
224 self.__string = string
225 else:
226 self.__string = string_x
227
228 @property
229 def length(self):
230 return self.__length
231
232 @property
233 def string(self):
234 return self.__string
235
236
237 class ResultWithInvalidLength(TestDisassembler):
238 """Return a result object with an invalid length."""
239
240 def disassemble(self, info):
241 result = gdb.disassembler.builtin_disassemble(info)
242 return ResultWrapper(result.length, result.string, 0)
243
244
245 class ResultWithInvalidString(TestDisassembler):
246 """Return a result object with an empty string."""
247
248 def disassemble(self, info):
249 result = gdb.disassembler.builtin_disassemble(info)
250 return ResultWrapper(result.length, result.string, None, "")
251
252
253 class TaggingDisassembler(TestDisassembler):
254 """A simple disassembler that just tags the output."""
255
256 def __init__(self, tag):
257 super().__init__()
258 self._tag = tag
259
260 def disassemble(self, info):
261 result = gdb.disassembler.builtin_disassemble(info)
262 text = result.string + "\t## tag = %s" % self._tag
263 return DisassemblerResult(result.length, text)
264
265
266 class GlobalCachingDisassembler(TestDisassembler):
267 """A disassembler that caches the DisassembleInfo that is passed in,
268 as well as a copy of the original DisassembleInfo.
269
270 Once the call into the disassembler is complete then the
271 DisassembleInfo objects become invalid, and any calls into them
272 should trigger an exception."""
273
274 # This is where we cache the DisassembleInfo objects.
275 cached_insn_disas = []
276
277 class MyInfo(gdb.disassembler.DisassembleInfo):
278 def __init__(self, info):
279 super().__init__(info)
280
281 def disassemble(self, info):
282 """Disassemble the instruction, add a CACHED comment to the output,
283 and cache the DisassembleInfo so that it is not garbage collected."""
284 GlobalCachingDisassembler.cached_insn_disas.append(info)
285 GlobalCachingDisassembler.cached_insn_disas.append(self.MyInfo(info))
286 result = gdb.disassembler.builtin_disassemble(info)
287 text = result.string + "\t## CACHED"
288 return DisassemblerResult(result.length, text)
289
290 @staticmethod
291 def check():
292 """Check that all of the methods on the cached DisassembleInfo trigger an
293 exception."""
294 for info in GlobalCachingDisassembler.cached_insn_disas:
295 assert isinstance(info, gdb.disassembler.DisassembleInfo)
296 assert not info.is_valid()
297 try:
298 val = info.address
299 raise gdb.GdbError("DisassembleInfo.address is still valid")
300 except RuntimeError as e:
301 assert str(e) == "DisassembleInfo is no longer valid."
302 except:
303 raise gdb.GdbError(
304 "DisassembleInfo.address raised an unexpected exception"
305 )
306
307 try:
308 val = info.architecture
309 raise gdb.GdbError("DisassembleInfo.architecture is still valid")
310 except RuntimeError as e:
311 assert str(e) == "DisassembleInfo is no longer valid."
312 except:
313 raise gdb.GdbError(
314 "DisassembleInfo.architecture raised an unexpected exception"
315 )
316
317 try:
318 val = info.read_memory(1, 0)
319 raise gdb.GdbError("DisassembleInfo.read is still valid")
320 except RuntimeError as e:
321 assert str(e) == "DisassembleInfo is no longer valid."
322 except:
323 raise gdb.GdbError(
324 "DisassembleInfo.read raised an unexpected exception"
325 )
326
327 print("PASS")
328
329
330 class GlobalNullDisassembler(TestDisassembler):
331 """A disassembler that does not change the output at all."""
332
333 def disassemble(self, info):
334 pass
335
336
337 class ReadMemoryMemoryErrorDisassembler(TestDisassembler):
338 """Raise a MemoryError exception from the DisassembleInfo.read_memory
339 method."""
340
341 class MyInfo(gdb.disassembler.DisassembleInfo):
342 def __init__(self, info):
343 super().__init__(info)
344
345 def read_memory(self, length, offset):
346 # Throw a memory error with a specific address. We don't
347 # expect this address to show up in the output though.
348 raise gdb.MemoryError(0x1234)
349
350 def disassemble(self, info):
351 info = self.MyInfo(info)
352 return gdb.disassembler.builtin_disassemble(info)
353
354
355 class ReadMemoryGdbErrorDisassembler(TestDisassembler):
356 """Raise a GdbError exception from the DisassembleInfo.read_memory
357 method."""
358
359 class MyInfo(gdb.disassembler.DisassembleInfo):
360 def __init__(self, info):
361 super().__init__(info)
362
363 def read_memory(self, length, offset):
364 raise gdb.GdbError("read_memory raised GdbError")
365
366 def disassemble(self, info):
367 info = self.MyInfo(info)
368 return gdb.disassembler.builtin_disassemble(info)
369
370
371 class ReadMemoryRuntimeErrorDisassembler(TestDisassembler):
372 """Raise a RuntimeError exception from the DisassembleInfo.read_memory
373 method."""
374
375 class MyInfo(gdb.disassembler.DisassembleInfo):
376 def __init__(self, info):
377 super().__init__(info)
378
379 def read_memory(self, length, offset):
380 raise RuntimeError("read_memory raised RuntimeError")
381
382 def disassemble(self, info):
383 info = self.MyInfo(info)
384 return gdb.disassembler.builtin_disassemble(info)
385
386
387 class ReadMemoryCaughtMemoryErrorDisassembler(TestDisassembler):
388 """Raise a MemoryError exception from the DisassembleInfo.read_memory
389 method, catch this in the outer disassembler."""
390
391 class MyInfo(gdb.disassembler.DisassembleInfo):
392 def __init__(self, info):
393 super().__init__(info)
394
395 def read_memory(self, length, offset):
396 raise gdb.MemoryError(0x1234)
397
398 def disassemble(self, info):
399 info = self.MyInfo(info)
400 try:
401 return gdb.disassembler.builtin_disassemble(info)
402 except gdb.MemoryError:
403 return None
404
405
406 class ReadMemoryCaughtGdbErrorDisassembler(TestDisassembler):
407 """Raise a GdbError exception from the DisassembleInfo.read_memory
408 method, catch this in the outer disassembler."""
409
410 class MyInfo(gdb.disassembler.DisassembleInfo):
411 def __init__(self, info):
412 super().__init__(info)
413
414 def read_memory(self, length, offset):
415 raise gdb.GdbError("exception message")
416
417 def disassemble(self, info):
418 info = self.MyInfo(info)
419 try:
420 return gdb.disassembler.builtin_disassemble(info)
421 except gdb.GdbError as e:
422 if e.args[0] == "exception message":
423 return None
424 raise e
425
426
427 class ReadMemoryCaughtRuntimeErrorDisassembler(TestDisassembler):
428 """Raise a RuntimeError exception from the DisassembleInfo.read_memory
429 method, catch this in the outer disassembler."""
430
431 class MyInfo(gdb.disassembler.DisassembleInfo):
432 def __init__(self, info):
433 super().__init__(info)
434
435 def read_memory(self, length, offset):
436 raise RuntimeError("exception message")
437
438 def disassemble(self, info):
439 info = self.MyInfo(info)
440 try:
441 return gdb.disassembler.builtin_disassemble(info)
442 except RuntimeError as e:
443 if e.args[0] == "exception message":
444 return None
445 raise e
446
447
448 class MemorySourceNotABufferDisassembler(TestDisassembler):
449 class MyInfo(gdb.disassembler.DisassembleInfo):
450 def __init__(self, info):
451 super().__init__(info)
452
453 def read_memory(self, length, offset):
454 return 1234
455
456 def disassemble(self, info):
457 info = self.MyInfo(info)
458 return gdb.disassembler.builtin_disassemble(info)
459
460
461 class MemorySourceBufferTooLongDisassembler(TestDisassembler):
462 """The read memory returns too many bytes."""
463
464 class MyInfo(gdb.disassembler.DisassembleInfo):
465 def __init__(self, info):
466 super().__init__(info)
467
468 def read_memory(self, length, offset):
469 buffer = super().read_memory(length, offset)
470 # Create a new memory view made by duplicating BUFFER. This
471 # will trigger an error as GDB expects a buffer of exactly
472 # LENGTH to be returned, while this will return a buffer of
473 # 2*LENGTH.
474 return memoryview(
475 bytes([int.from_bytes(x, "little") for x in (list(buffer[0:]) * 2)])
476 )
477
478 def disassemble(self, info):
479 info = self.MyInfo(info)
480 return gdb.disassembler.builtin_disassemble(info)
481
482
483 class BuiltinDisassembler(Disassembler):
484 """Just calls the builtin disassembler."""
485
486 def __init__(self):
487 super().__init__("BuiltinDisassembler")
488
489 def __call__(self, info):
490 return gdb.disassembler.builtin_disassemble(info)
491
492
493 class AnalyzingDisassembler(Disassembler):
494 class MyInfo(gdb.disassembler.DisassembleInfo):
495 """Wrapper around builtin DisassembleInfo type that overrides the
496 read_memory method."""
497
498 def __init__(self, info, start, end, nop_bytes):
499 """INFO is the DisassembleInfo we are wrapping. START and END are
500 addresses, and NOP_BYTES should be a memoryview object.
501
502 The length (END - START) should be the same as the length
503 of NOP_BYTES.
504
505 Any memory read requests outside the START->END range are
506 serviced normally, but any attempt to read within the
507 START->END range will return content from NOP_BYTES."""
508 super().__init__(info)
509 self._start = start
510 self._end = end
511 self._nop_bytes = nop_bytes
512
513 def _read_replacement(self, length, offset):
514 """Return a slice of the buffer representing the replacement nop
515 instructions."""
516
517 assert self._nop_bytes is not None
518 rb = self._nop_bytes
519
520 # If this request is outside of a nop instruction then we don't know
521 # what to do, so just raise a memory error.
522 if offset >= len(rb) or (offset + length) > len(rb):
523 raise gdb.MemoryError("invalid length and offset combination")
524
525 # Return only the slice of the nop instruction as requested.
526 s = offset
527 e = offset + length
528 return rb[s:e]
529
530 def read_memory(self, length, offset=0):
531 """Callback used by the builtin disassembler to read the contents of
532 memory."""
533
534 # If this request is within the region we are replacing with 'nop'
535 # instructions, then call the helper function to perform that
536 # replacement.
537 if self._start is not None:
538 assert self._end is not None
539 if self.address >= self._start and self.address < self._end:
540 return self._read_replacement(length, offset)
541
542 # Otherwise, we just forward this request to the default read memory
543 # implementation.
544 return super().read_memory(length, offset)
545
546 def __init__(self):
547 """Constructor."""
548 super().__init__("AnalyzingDisassembler")
549
550 # Details about the instructions found during the first disassembler
551 # pass.
552 self._pass_1_length = []
553 self._pass_1_insn = []
554 self._pass_1_address = []
555
556 # The start and end address for the instruction we will replace with
557 # one or more 'nop' instructions during pass two.
558 self._start = None
559 self._end = None
560
561 # The index in the _pass_1_* lists for where the nop instruction can
562 # be found, also, the buffer of bytes that make up a nop instruction.
563 self._nop_index = None
564 self._nop_bytes = None
565
566 # A flag that indicates if we are in the first or second pass of
567 # this disassembler test.
568 self._first_pass = True
569
570 # The disassembled instructions collected during the second pass.
571 self._pass_2_insn = []
572
573 # A copy of _pass_1_insn that has been modified to include the extra
574 # 'nop' instructions we plan to insert during the second pass. This
575 # is then checked against _pass_2_insn after the second disassembler
576 # pass has completed.
577 self._check = []
578
579 def __call__(self, info):
580 """Called to perform the disassembly."""
581
582 # Override the info object, this provides access to our
583 # read_memory function.
584 info = self.MyInfo(info, self._start, self._end, self._nop_bytes)
585 result = gdb.disassembler.builtin_disassemble(info)
586
587 # Record some informaiton about the first 'nop' instruction we find.
588 if self._nop_index is None and is_nop(result.string):
589 self._nop_index = len(self._pass_1_length)
590 # The offset in the following read_memory call defaults to 0.
591 self._nop_bytes = info.read_memory(result.length)
592
593 # Record information about each instruction that is disassembled.
594 # This test is performed in two passes, and we need different
595 # information in each pass.
596 if self._first_pass:
597 self._pass_1_length.append(result.length)
598 self._pass_1_insn.append(result.string)
599 self._pass_1_address.append(info.address)
600 else:
601 self._pass_2_insn.append(result.string)
602
603 return result
604
605 def find_replacement_candidate(self):
606 """Call this after the first disassembly pass. This identifies a suitable
607 instruction to replace with 'nop' instruction(s)."""
608
609 if self._nop_index is None:
610 raise gdb.GdbError("no nop was found")
611
612 nop_idx = self._nop_index
613 nop_length = self._pass_1_length[nop_idx]
614
615 # First we look for an instruction that is larger than a nop
616 # instruction, but whose length is an exact multiple of the nop
617 # instruction's length.
618 replace_idx = None
619 for idx in range(len(self._pass_1_length)):
620 if (
621 idx > 0
622 and idx != nop_idx
623 and not is_nop(self._pass_1_insn[idx])
624 and self._pass_1_length[idx] > self._pass_1_length[nop_idx]
625 and self._pass_1_length[idx] % self._pass_1_length[nop_idx] == 0
626 ):
627 replace_idx = idx
628 break
629
630 # If we still don't have a replacement candidate, then search again,
631 # this time looking for an instruciton that is the same length as a
632 # nop instruction.
633 if replace_idx is None:
634 for idx in range(len(self._pass_1_length)):
635 if (
636 idx > 0
637 and idx != nop_idx
638 and not is_nop(self._pass_1_insn[idx])
639 and self._pass_1_length[idx] == self._pass_1_length[nop_idx]
640 ):
641 replace_idx = idx
642 break
643
644 # Weird, the nop instruction must be larger than every other
645 # instruction, or all instructions are 'nop'?
646 if replace_idx is None:
647 raise gdb.GdbError("can't find an instruction to replace")
648
649 # Record the instruction range that will be replaced with 'nop'
650 # instructions, and mark that we are now on the second pass.
651 self._start = self._pass_1_address[replace_idx]
652 self._end = self._pass_1_address[replace_idx] + self._pass_1_length[replace_idx]
653 self._first_pass = False
654 print("Replace from 0x%x to 0x%x with NOP" % (self._start, self._end))
655
656 # Finally, build the expected result. Create the _check list, which
657 # is a copy of _pass_1_insn, but replace the instruction we
658 # identified above with a series of 'nop' instructions.
659 self._check = list(self._pass_1_insn)
660 nop_count = int(self._pass_1_length[replace_idx] / self._pass_1_length[nop_idx])
661 nop_insn = self._pass_1_insn[nop_idx]
662 nops = [nop_insn] * nop_count
663 self._check[replace_idx : (replace_idx + 1)] = nops
664
665 def check(self):
666 """Call this after the second disassembler pass to validate the output."""
667 if self._check != self._pass_2_insn:
668 raise gdb.GdbError("mismatch")
669 print("PASS")
670
671
672 def add_global_disassembler(dis_class):
673 """Create an instance of DIS_CLASS and register it as a global disassembler."""
674 dis = dis_class()
675 gdb.disassembler.register_disassembler(dis, None)
676 return dis
677
678
679 class InvalidDisassembleInfo(gdb.disassembler.DisassembleInfo):
680 """An attempt to create a DisassembleInfo sub-class without calling
681 the parent class init method.
682
683 Attempts to use instances of this class should throw an error
684 saying that the DisassembleInfo is not valid, despite this class
685 having all of the required attributes.
686
687 The reason why this class will never be valid is that an internal
688 field (within the C++ code) can't be initialized without calling
689 the parent class init method."""
690
691 def __init__(self):
692 assert current_pc is not None
693
694 def is_valid(self):
695 return True
696
697 @property
698 def address(self):
699 global current_pc
700 return current_pc
701
702 @property
703 def architecture(self):
704 return gdb.selected_inferior().architecture()
705
706 @property
707 def progspace(self):
708 return gdb.selected_inferior().progspace
709
710
711 # Start with all disassemblers removed.
712 remove_all_python_disassemblers()
713
714 print("Python script imported")