xref: /netbsd-src/external/gpl3/gdb.old/dist/gdb/testsuite/gdb.python/py-disasm.py (revision 32d1c65c71fbdb65a012e8392a62a757dd6853e9)
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
16import gdb
17import gdb.disassembler
18import struct
19import sys
20
21from 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.
25current_pc = None
26
27
28def is_nop(s):
29    return s == "nop" or s == "nop\t0"
30
31
32# Remove all currently registered disassemblers.
33def 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
39class 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
67class 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
86class 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
105class 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
125class 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
138class 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
145class 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
152class 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
160class 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
168class 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
182class 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
193class 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
204class 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
216class 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
237class 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
245class 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
253class 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
266class 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
330class GlobalNullDisassembler(TestDisassembler):
331    """A disassembler that does not change the output at all."""
332
333    def disassemble(self, info):
334        pass
335
336
337class 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
355class 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
371class 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
387class 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
406class 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
427class 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
448class 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
461class 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
483class 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
493class 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
672def 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
679class 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.
712remove_all_python_disassemblers()
713
714print("Python script imported")
715