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