1 # Copyright (C) 2021-2023 Free Software Foundation, Inc.
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.
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.
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/>.
17 import gdb
.disassembler
21 from gdb
.disassembler
import Disassembler
, DisassemblerResult
23 # A global, holds the program-counter address at which we should
24 # perform the extra disassembly that this script provides.
29 return s
== "nop" or s
== "nop\t0"
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)
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."""
47 super().__init
__("TestDisassembler")
49 if current_pc
== None:
50 raise gdb
.GdbError("no current_pc set")
52 def __call__(self
, info
):
55 if info
.address
!= current_pc
:
58 return self
.disassemble(info
)
63 def disassemble(self
, info
):
64 raise NotImplementedError("override the disassemble method")
67 class GlobalPreInfoDisassembler(TestDisassembler
):
68 """Check the attributes of DisassembleInfo before disassembly has occurred."""
70 def disassemble(self
, info
):
72 ar
= info
.architecture
75 raise gdb
.GdbError("invalid address")
77 if not isinstance(ar
, gdb
.Architecture
):
78 raise gdb
.GdbError("invalid architecture type")
80 result
= gdb
.disassembler
.builtin_disassemble(info
)
82 text
= result
.string
+ "\t## ad = 0x%x, ar = %s" % (ad
, ar
.name())
83 return DisassemblerResult(result
.length
, text
)
86 class GlobalPostInfoDisassembler(TestDisassembler
):
87 """Check the attributes of DisassembleInfo after disassembly has occurred."""
89 def disassemble(self
, info
):
90 result
= gdb
.disassembler
.builtin_disassemble(info
)
93 ar
= info
.architecture
96 raise gdb
.GdbError("invalid address")
98 if not isinstance(ar
, gdb
.Architecture
):
99 raise gdb
.GdbError("invalid architecture type")
101 text
= result
.string
+ "\t## ad = 0x%x, ar = %s" % (ad
, ar
.name())
102 return DisassemblerResult(result
.length
, text
)
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."""
110 def disassemble(self
, info
):
111 result
= gdb
.disassembler
.builtin_disassemble(info
)
117 v
= bytes(info
.read_memory(1, o
))[0]
118 if sys
.version_info
[0] < 3:
119 v
= struct
.unpack("<B", v
)
121 text
= result
.string
+ "\t## bytes = %s" % str
122 return DisassemblerResult(result
.length
, text
)
125 class GlobalAddrDisassembler(TestDisassembler
):
126 """Check the gdb.format_address method."""
128 def disassemble(self
, info
):
129 result
= gdb
.disassembler
.builtin_disassemble(info
)
130 arch
= info
.architecture
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
)
138 class GdbErrorEarlyDisassembler(TestDisassembler
):
139 """Raise a GdbError instead of performing any disassembly."""
141 def disassemble(self
, info
):
142 raise gdb
.GdbError("GdbError instead of a result")
145 class RuntimeErrorEarlyDisassembler(TestDisassembler
):
146 """Raise a RuntimeError instead of performing any disassembly."""
148 def disassemble(self
, info
):
149 raise RuntimeError("RuntimeError instead of a result")
152 class GdbErrorLateDisassembler(TestDisassembler
):
153 """Raise a GdbError after calling the builtin disassembler."""
155 def disassemble(self
, info
):
156 result
= gdb
.disassembler
.builtin_disassemble(info
)
157 raise gdb
.GdbError("GdbError after builtin disassembler")
160 class RuntimeErrorLateDisassembler(TestDisassembler
):
161 """Raise a RuntimeError after calling the builtin disassembler."""
163 def disassemble(self
, info
):
164 result
= gdb
.disassembler
.builtin_disassemble(info
)
165 raise RuntimeError("RuntimeError after builtin disassembler")
168 class MemoryErrorEarlyDisassembler(TestDisassembler
):
169 """Throw a memory error, ignore the error and disassemble."""
171 def disassemble(self
, info
):
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
)
182 class MemoryErrorLateDisassembler(TestDisassembler
):
183 """Throw a memory error after calling the builtin disassembler, but
184 before we return a result."""
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")
193 class RethrowMemoryErrorDisassembler(TestDisassembler
):
194 """Catch and rethrow a memory error."""
196 def disassemble(self
, info
):
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")
204 class ResultOfWrongType(TestDisassembler
):
205 """Return something that is not a DisassemblerResult from disassemble method"""
208 def __init__(self
, length
, string
):
212 def disassemble(self
, info
):
213 return self
.Blah(1, "ABC")
216 class ResultWrapper(gdb
.disassembler
.DisassemblerResult
):
217 def __init__(self
, length
, string
, length_x
=None, string_x
=None):
218 super().__init
__(length
, string
)
220 self
.__length
= length
222 self
.__length
= length_x
224 self
.__string
= string
226 self
.__string
= string_x
237 class ResultWithInvalidLength(TestDisassembler
):
238 """Return a result object with an invalid length."""
240 def disassemble(self
, info
):
241 result
= gdb
.disassembler
.builtin_disassemble(info
)
242 return ResultWrapper(result
.length
, result
.string
, 0)
245 class ResultWithInvalidString(TestDisassembler
):
246 """Return a result object with an empty string."""
248 def disassemble(self
, info
):
249 result
= gdb
.disassembler
.builtin_disassemble(info
)
250 return ResultWrapper(result
.length
, result
.string
, None, "")
253 class TaggingDisassembler(TestDisassembler
):
254 """A simple disassembler that just tags the output."""
256 def __init__(self
, tag
):
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
)
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.
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."""
274 # This is where we cache the DisassembleInfo objects.
275 cached_insn_disas
= []
277 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
278 def __init__(self
, info
):
279 super().__init
__(info
)
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
)
292 """Check that all of the methods on the cached DisassembleInfo trigger an
294 for info
in GlobalCachingDisassembler
.cached_insn_disas
:
295 assert isinstance(info
, gdb
.disassembler
.DisassembleInfo
)
296 assert not info
.is_valid()
299 raise gdb
.GdbError("DisassembleInfo.address is still valid")
300 except RuntimeError as e
:
301 assert str(e
) == "DisassembleInfo is no longer valid."
304 "DisassembleInfo.address raised an unexpected exception"
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."
314 "DisassembleInfo.architecture raised an unexpected exception"
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."
324 "DisassembleInfo.read raised an unexpected exception"
330 class GlobalNullDisassembler(TestDisassembler
):
331 """A disassembler that does not change the output at all."""
333 def disassemble(self
, info
):
337 class ReadMemoryMemoryErrorDisassembler(TestDisassembler
):
338 """Raise a MemoryError exception from the DisassembleInfo.read_memory
341 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
342 def __init__(self
, info
):
343 super().__init
__(info
)
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)
350 def disassemble(self
, info
):
351 info
= self
.MyInfo(info
)
352 return gdb
.disassembler
.builtin_disassemble(info
)
355 class ReadMemoryGdbErrorDisassembler(TestDisassembler
):
356 """Raise a GdbError exception from the DisassembleInfo.read_memory
359 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
360 def __init__(self
, info
):
361 super().__init
__(info
)
363 def read_memory(self
, length
, offset
):
364 raise gdb
.GdbError("read_memory raised GdbError")
366 def disassemble(self
, info
):
367 info
= self
.MyInfo(info
)
368 return gdb
.disassembler
.builtin_disassemble(info
)
371 class ReadMemoryRuntimeErrorDisassembler(TestDisassembler
):
372 """Raise a RuntimeError exception from the DisassembleInfo.read_memory
375 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
376 def __init__(self
, info
):
377 super().__init
__(info
)
379 def read_memory(self
, length
, offset
):
380 raise RuntimeError("read_memory raised RuntimeError")
382 def disassemble(self
, info
):
383 info
= self
.MyInfo(info
)
384 return gdb
.disassembler
.builtin_disassemble(info
)
387 class ReadMemoryCaughtMemoryErrorDisassembler(TestDisassembler
):
388 """Raise a MemoryError exception from the DisassembleInfo.read_memory
389 method, catch this in the outer disassembler."""
391 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
392 def __init__(self
, info
):
393 super().__init
__(info
)
395 def read_memory(self
, length
, offset
):
396 raise gdb
.MemoryError(0x1234)
398 def disassemble(self
, info
):
399 info
= self
.MyInfo(info
)
401 return gdb
.disassembler
.builtin_disassemble(info
)
402 except gdb
.MemoryError:
406 class ReadMemoryCaughtGdbErrorDisassembler(TestDisassembler
):
407 """Raise a GdbError exception from the DisassembleInfo.read_memory
408 method, catch this in the outer disassembler."""
410 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
411 def __init__(self
, info
):
412 super().__init
__(info
)
414 def read_memory(self
, length
, offset
):
415 raise gdb
.GdbError("exception message")
417 def disassemble(self
, info
):
418 info
= self
.MyInfo(info
)
420 return gdb
.disassembler
.builtin_disassemble(info
)
421 except gdb
.GdbError
as e
:
422 if e
.args
[0] == "exception message":
427 class ReadMemoryCaughtRuntimeErrorDisassembler(TestDisassembler
):
428 """Raise a RuntimeError exception from the DisassembleInfo.read_memory
429 method, catch this in the outer disassembler."""
431 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
432 def __init__(self
, info
):
433 super().__init
__(info
)
435 def read_memory(self
, length
, offset
):
436 raise RuntimeError("exception message")
438 def disassemble(self
, info
):
439 info
= self
.MyInfo(info
)
441 return gdb
.disassembler
.builtin_disassemble(info
)
442 except RuntimeError as e
:
443 if e
.args
[0] == "exception message":
448 class MemorySourceNotABufferDisassembler(TestDisassembler
):
449 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
450 def __init__(self
, info
):
451 super().__init
__(info
)
453 def read_memory(self
, length
, offset
):
456 def disassemble(self
, info
):
457 info
= self
.MyInfo(info
)
458 return gdb
.disassembler
.builtin_disassemble(info
)
461 class MemorySourceBufferTooLongDisassembler(TestDisassembler
):
462 """The read memory returns too many bytes."""
464 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
465 def __init__(self
, info
):
466 super().__init
__(info
)
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
475 bytes([int.from_bytes(x
, "little") for x
in (list(buffer[0:]) * 2)])
478 def disassemble(self
, info
):
479 info
= self
.MyInfo(info
)
480 return gdb
.disassembler
.builtin_disassemble(info
)
483 class BuiltinDisassembler(Disassembler
):
484 """Just calls the builtin disassembler."""
487 super().__init
__("BuiltinDisassembler")
489 def __call__(self
, info
):
490 return gdb
.disassembler
.builtin_disassemble(info
)
493 class AnalyzingDisassembler(Disassembler
):
494 class MyInfo(gdb
.disassembler
.DisassembleInfo
):
495 """Wrapper around builtin DisassembleInfo type that overrides the
496 read_memory method."""
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.
502 The length (END - START) should be the same as the length
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
)
511 self
._nop
_bytes
= nop_bytes
513 def _read_replacement(self
, length
, offset
):
514 """Return a slice of the buffer representing the replacement nop
517 assert self
._nop
_bytes
is not None
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")
525 # Return only the slice of the nop instruction as requested.
530 def read_memory(self
, length
, offset
=0):
531 """Callback used by the builtin disassembler to read the contents of
534 # If this request is within the region we are replacing with 'nop'
535 # instructions, then call the helper function to perform that
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
)
542 # Otherwise, we just forward this request to the default read memory
544 return super().read_memory(length
, offset
)
548 super().__init
__("AnalyzingDisassembler")
550 # Details about the instructions found during the first disassembler
552 self
._pass
_1_length
= []
553 self
._pass
_1_insn
= []
554 self
._pass
_1_address
= []
556 # The start and end address for the instruction we will replace with
557 # one or more 'nop' instructions during pass two.
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
566 # A flag that indicates if we are in the first or second pass of
567 # this disassembler test.
568 self
._first
_pass
= True
570 # The disassembled instructions collected during the second pass.
571 self
._pass
_2_insn
= []
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.
579 def __call__(self
, info
):
580 """Called to perform the disassembly."""
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
)
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
)
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.
597 self
._pass
_1_length
.append(result
.length
)
598 self
._pass
_1_insn
.append(result
.string
)
599 self
._pass
_1_address
.append(info
.address
)
601 self
._pass
_2_insn
.append(result
.string
)
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)."""
609 if self
._nop
_index
is None:
610 raise gdb
.GdbError("no nop was found")
612 nop_idx
= self
._nop
_index
613 nop_length
= self
._pass
_1_length
[nop_idx
]
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.
619 for idx
in range(len(self
._pass
_1_length
)):
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
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
633 if replace_idx
is None:
634 for idx
in range(len(self
._pass
_1_length
)):
638 and not is_nop(self
._pass
_1_insn
[idx
])
639 and self
._pass
_1_length
[idx
] == self
._pass
_1_length
[nop_idx
]
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")
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
))
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
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")
672 def add_global_disassembler(dis_class
):
673 """Create an instance of DIS_CLASS and register it as a global disassembler."""
675 gdb
.disassembler
.register_disassembler(dis
, None)
679 class InvalidDisassembleInfo(gdb
.disassembler
.DisassembleInfo
):
680 """An attempt to create a DisassembleInfo sub-class without calling
681 the parent class init method.
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.
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."""
692 assert current_pc
is not None
703 def architecture(self
):
704 return gdb
.selected_inferior().architecture()
708 return gdb
.selected_inferior().progspace
711 # Start with all disassemblers removed.
712 remove_all_python_disassemblers()
714 print("Python script imported")