1# 2# Copyright (c) 2010-2025 Antmicro 3# 4# This file is licensed under the MIT License. 5# Full license text is available in 'licenses/MIT.txt'. 6# 7 8import ctypes 9import dataclasses 10import re 11 12from enum import IntEnum, auto 13from pathlib import Path 14 15 16ENV_BREAKPOINT = None 17MEMORY_MAPPINGS = [] 18CPU_POINTERS = [] 19GUEST_PC = None 20 21 22class Disassembler: 23 def __init__(self, triple, name, flags=0): 24 library_path = self._get_library_path() 25 assert library_path is not None, 'could not find libllvm-disas.so path' 26 27 self._library = ctypes.CDLL(str(library_path)) 28 self._library.llvm_create_disasm_cpu_with_flags.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint32] 29 self._library.llvm_create_disasm_cpu_with_flags.restype = ctypes.c_void_p 30 31 self._library.llvm_disasm_instruction.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64,ctypes.c_char_p, ctypes.c_uint32] 32 self._library.llvm_disasm_instruction.restype = ctypes.c_int 33 self._library.llvm_disasm_dispose.argtypes = [ctypes.c_void_p] 34 self._context = self._library.llvm_create_disasm_cpu_with_flags( 35 triple, 36 name, 37 flags) 38 assert self._context != 0, 'could not initialize llvm disassembler' 39 40 @classmethod 41 def _get_library_path(cls): 42 if hasattr(cls, '_cached_library_path'): 43 return cls._cached_library_path 44 45 inferior = gdb.selected_inferior() 46 if not inferior: 47 return None 48 49 with open(f'/proc/{inferior.pid}/cmdline', 'rb') as f: 50 cmdline = f.read().split(b'\x00') 51 cmdline = [arg.decode('ascii') for arg in cmdline] 52 53 if len(cmdline) > 1 and cmdline[1].endswith('Renode.dll'): 54 # NOTE: We've built Renode from source code 55 start_path = Path(cmdline[1]) 56 else: 57 # NOTE: Running from bundled binary (e.g. portable) 58 start_path = Path(f'/proc/{inferior.pid}/exe').resolve() 59 60 path = Path(start_path) 61 while not (path / '.renode-root').exists(): 62 parent = path.parent 63 if path == parent: 64 return None 65 path = parent 66 67 cls._cached_library_path = path / 'lib' / 'resources' / 'llvm' / 'libllvm-disas.so' 68 return cls._cached_library_path 69 70 def __del__(self): 71 self._library.llvm_disasm_dispose(self._context) 72 self._context = 0 73 74 def disassemble(self, data: bytes): 75 output = bytes(256) 76 op_len = self._library.llvm_disasm_instruction( 77 self._context, 78 data, 79 len(data), 80 output, 81 len(output)) 82 return output.rstrip(b'\0').decode('ascii', 'ignore').strip(), op_len 83 84 85@dataclasses.dataclass 86class MemoryMapping: 87 REGEX = re.compile(r'\s*0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+0x[0-9a-f]+\s+([^\s]{4})\s+([^\s]*)') 88 89 start: int 90 end: int 91 size: int 92 perms: str 93 path: str 94 95 @property 96 def executable(self): 97 return 'x' in self.perms 98 99 @property 100 def data(self): 101 return 'rw' in self.perms 102 103 def __post_init__(self): 104 for field in dataclasses.fields(self): 105 value = getattr(self, field.name) 106 if field.type is int and not isinstance(value, int): 107 setattr(self, field.name, int(value, 16)) 108 109 110class Architecture(IntEnum): 111 AARCH32 = auto() 112 AARCH64 = auto() 113 RISCV = auto() 114 XTENSA = auto() 115 I386 = auto() 116 UNKNOWN = auto() 117 118 @property 119 def insn_start_words(self): 120 return ({ 121 Architecture.AARCH32: 2, 122 Architecture.AARCH64: 3, 123 Architecture.RISCV: 1, 124 Architecture.XTENSA: 1, 125 Architecture.I386: 2, 126 }).get(self, 1) 127 128 129def source(callable): 130 """Convenience decorator for sourcing gdb commands""" 131 callable() 132 return callable 133 134 135@source 136class Renode(gdb.Command): 137 """Utility functions for debugging Renode""" 138 def __init__(self): 139 super(self.__class__, self).__init__('renode', gdb.COMMAND_USER, prefix=True) 140 141 142@source 143class ConvenienceRenodeReadBytes(gdb.Function): 144 def __init__(self): 145 super(self.__class__, self).__init__('_renode_read_bytes') 146 147 def invoke(self, addr, length): 148 data = read_guest_bytes(int(addr), int(length)) 149 return gdb.Value(data, gdb.lookup_type('uint8_t').array(length - 1)) 150 151 152@source 153class ConvenienceCpu(gdb.Function): 154 def __init__(self): 155 super(self.__class__, self).__init__('_cpu') 156 157 def invoke(self, index): 158 global CPU_POINTERS 159 return gdb.Value(CPU_POINTERS[index]).cast(gdb.lookup_type('CPUState').pointer().pointer()).referenced_value() 160 161 162@source 163class RenodeReadBytes(gdb.Command): 164 """Read bytes from guest memory through SystemBus 165 166 renode read-bytes address length 167 168 This command is wrapper over tlib_read_byte function, which 169 reads bytes using SystemBus.ReadByte method""" 170 171 def __init__(self): 172 super(self.__class__, self).__init__('renode read-bytes', gdb.COMMAND_USER) 173 174 def invoke(self, arg, from_tty): 175 args = gdb.string_to_argv(arg) 176 gdb.execute('p/x $_renode_read_bytes(%s, %s)' % (args[0], args[1])) 177 178 179@source 180class RenodeNextInstruction(gdb.Command): 181 """Creates a breakpoint on next guest instruction 182 183 renode next-instruction [cpu-index] 184 185 Creates a breakpoint on next guest instruction, potentially waiting on 186 new translation block. If <cpu-index> is given, breakpoint will be on 187 next instruction for given cpu. When ommited, current cpu will be used instead""" 188 189 def __init__(self): 190 super(self.__class__, self).__init__('renode next-instruction', gdb.COMMAND_USER) 191 192 def _create_pending_breakpoint(self, cpu): 193 global ENV_BREAKPOINT 194 ENV_BREAKPOINT = gdb.Breakpoint(f'{cpu}->current_tb', gdb.BP_WATCHPOINT, gdb.WP_WRITE, True) 195 196 def invoke(self, arg, from_tty): 197 global GUEST_PC 198 199 cpu = '$current_cpu' 200 if arg: 201 cpu = f'$_cpu({arg})' 202 elif gdb.convenience_variable('current_cpu') is None: 203 gdb.write(f'Could not detect active CPU. Re-run command with specified cpu-index (0-{len(CPU_POINTERS)})\n') 204 return 205 206 current_tb = get_current_tb(cpu) 207 if current_tb is None: 208 self._create_pending_breakpoint(cpu) 209 return 210 211 index = 0 212 if GUEST_PC is not None: 213 for guest_pc, _, _ in current_tb: 214 if guest_pc > GUEST_PC: 215 break 216 index += 1 217 218 if index >= len(current_tb): 219 self._create_pending_breakpoint(cpu) 220 return 221 222 guest, host, _ = current_tb[index] 223 gdb.write(f'Creating hook on guest pc: 0x{guest:x}\n') 224 gdb.execute(f'tbreak *0x{host:x}', from_tty=True) 225 226 227@source 228class RenodePrintTranslationBlock(gdb.Command): 229 """Prints disassembly for current TranslationBlock 230 231 renode print-tb""" 232 233 def __init__(self): 234 super(self.__class__, self).__init__('renode print-tb', gdb.COMMAND_USER) 235 236 def invoke(self, arg, from_tty): 237 current_tb = get_current_tb() 238 if current_tb is None: 239 return 240 241 guest_pc = GUEST_PC or int(gdb.parse_and_eval('$current_cpu->pc').const_value()) 242 243 for guest, host, _ in current_tb: 244 _, instr = disassemble_instruction(guest) 245 instr = instr or 'n/a' 246 current_line = '=>' if guest == guest_pc else ' ' 247 gdb.write(f'{current_line} [0x{host:08x}] 0x{guest:08x}: {instr}\n') 248 249 250def read_host_byte(ptr): 251 return int(gdb.parse_and_eval(f'*(uint8_t*)0x{ptr:x}').const_value()) 252 253 254def read_guest_bytes(ptr, len): 255 b = [] 256 for i in range(len): 257 b.append(read_guest_byte(ptr + i)) 258 return bytes(b) 259 260 261def read_guest_byte(ptr): 262 return int(gdb.parse_and_eval(f'tlib_read_byte_callback$({ptr})').const_value()) 263 264 265def decode_sleb128(ptr): 266 """ 267 This function is based on decode_sleb128 from tlib/arch/translate-all.c 268 """ 269 270 val = 0 271 byte, shift = 0, 0 272 273 while True: 274 byte = read_host_byte(ptr) 275 ptr += 1 276 val |= (byte & 0x7f) << shift 277 val &= 0xffffffff 278 shift += 7 279 280 if (byte & 0x80) == 0: 281 break 282 283 if shift < 32 and (byte & 0x40) != 0: 284 val |= 0xffffffff << shift 285 val &= 0xffffffff 286 287 return val, ptr 288 289 290def get_current_tb(cpu=None): 291 if cpu is None and gdb.convenience_variable('current_cpu') is None: 292 return None 293 294 if cpu is None: 295 cpu = '$current_cpu' 296 297 tb_defined = int(gdb.parse_and_eval(f'{cpu}->current_tb').const_value()) != 0 298 if not tb_defined: 299 return None 300 301 tb_pc = int(gdb.parse_and_eval(f'{cpu}->current_tb->pc').const_value()) 302 host_pc = int(gdb.parse_and_eval(f'{cpu}->current_tb->tc_ptr').const_value()) 303 search_ptr = int(gdb.parse_and_eval(f'{cpu}->current_tb->tc_search').const_value()) 304 num_inst = int(gdb.parse_and_eval(f'{cpu}->current_tb->icount').const_value()) 305 306 insn_start_words = detect_architecture().insn_start_words 307 data = [ tb_pc ] 308 data.extend([0 for _ in range(insn_start_words - 1)]) 309 mapping = [] 310 311 for _ in range(num_inst): 312 for j in range(insn_start_words): 313 data_delta, search_ptr = decode_sleb128(search_ptr) 314 data[j] += data_delta 315 316 host_start = host_pc 317 host_pc_delta, search_ptr = decode_sleb128(search_ptr) 318 host_pc += host_pc_delta 319 mapping.append((data[0], host_start, host_pc)) 320 321 return mapping 322 323 324def get_current_guest_pc(): 325 current_tb = get_current_tb() 326 if current_tb is None: 327 return None 328 329 current_pc = int(gdb.parse_and_eval('$pc').const_value()) 330 for guest, _, host_end in current_tb: 331 if host_end > current_pc: 332 return guest 333 334 return None 335 336 337def disassemble_instruction(ptr): 338 model, triple = get_model_and_triple() 339 if model is None: 340 return None, None 341 342 opcode = read_guest_bytes(ptr, 4) 343 disas = Disassembler(triple.encode('ascii'), model.encode('ascii')) 344 result, op_len = disas.disassemble(opcode) 345 opcode = opcode[:op_len] 346 347 return int.from_bytes(opcode, 'little'), result 348 349 350def get_current_instruction(): 351 guest_pc = get_current_guest_pc() 352 if guest_pc is None: 353 return None, None, None 354 355 opcode, instr = disassemble_instruction(guest_pc) 356 if opcode is None: 357 return guest_pc, None, None 358 359 return guest_pc, opcode, instr 360 361 362def detect_architecture(): 363 # NOTE: Check if arm 364 try: 365 gdb.parse_and_eval('$current_cpu->thumb') 366 try: 367 # NOTE: Check aarch64 368 gdb.parse_and_eval('$current_cpu->aarch64') 369 return Architecture.AARCH64 370 except: 371 return Architecture.AARCH32 372 except: 373 pass 374 375 # NOTE: Check if RISC-V 376 try: 377 gdb.parse_and_eval('$current_cpu->mhartid') 378 return Architecture.RISCV 379 except: 380 pass 381 382 # NOTE: Check if xtensa 383 try: 384 gdb.parse_and_eval('$current_cpu->config') 385 return Architecture.XTENSA 386 except: 387 pass 388 389 # NOTE: Check if I386 390 try: 391 gdb.parse_and_eval('$current_cpu->eip') 392 return Architecture.I386 393 except: 394 pass 395 396 return Architecture.UNKNOWN 397 398 399def get_model_and_triple(): 400 arch = detect_architecture() 401 if arch is Architecture.UNKNOWN: 402 return None, None 403 404 if arch is Architecture.AARCH32 or arch is Architecture.AARCH64: 405 this_cpu_id = int(gdb.parse_and_eval('$current_cpu->cp15.c0_cpuid').const_value()) 406 arm_cpu_names = gdb.parse_and_eval('arm_cpu_names') 407 index = 0 408 409 while arm_cpu_names[index]['name'] != None: 410 cpu_id = int(arm_cpu_names[index]['id'].const_value()) 411 if cpu_id == this_cpu_id: 412 model = str(arm_cpu_names[index]['name'].string()) 413 break 414 index += 1 415 else: 416 return None, None 417 418 if model == 'cortex-r52': 419 triple = 'arm' 420 elif arch is Architecture.AARCH64: 421 triple = 'arm64' 422 else: 423 if 'cortex-m' in model: 424 triple = 'thumb' 425 else: 426 triple = 'armv7a' 427 428 if triple == 'armv7a' and int(gdb.parse_and_eval('$current_cpu->thumb').const_value()) > 0: 429 triple = 'thumb' 430 431 return model, triple 432 433 if arch is Architecture.RISCV: 434 try: 435 gdb.parse_and_eval('get_reg_pointer_64') 436 triple = 'riscv64' 437 model = 'rv64' 438 except: 439 triple = 'riscv32' 440 model = 'rv32' 441 442 misa = gdb.parse_and_eval('$current_cpu->misa_mask') 443 extensions = {chr(ord('a') + index) for index in range(32) if misa & (1 << index) > 0} 444 extensions &= set('imafdcv') 445 446 model += ''.join(extensions) 447 return model, triple 448 449 return None, None 450 451 452def cache_memory_mappings(): 453 global MEMORY_MAPPINGS 454 455 MEMORY_MAPPINGS = [] 456 mappings = gdb.execute('info proc mappings', from_tty=False, to_string=True) 457 for line in mappings.splitlines(): 458 match = MemoryMapping.REGEX.match(line) 459 if match is None: 460 continue 461 mapping = MemoryMapping(*match.groups()) 462 if '-Antmicro.Renode.translate-' not in mapping.path: 463 continue 464 465 MEMORY_MAPPINGS.append(mapping) 466 MEMORY_MAPPINGS.sort(key=lambda m: m.path) 467 468 469def update_cpu_pointers(): 470 global CPU_POINTERS, MEMORY_MAPPINGS 471 472 current_cpu = int(gdb.parse_and_eval('cpu').address) 473 current_cpu_objfile = gdb.current_progspace().objfile_for_address(current_cpu) 474 current_mapping = next((m for m in MEMORY_MAPPINGS if m.data and current_cpu_objfile.filename == m.path), None) 475 if current_mapping is None: 476 return 477 478 offset = current_cpu - current_mapping.start 479 CPU_POINTERS = [m.start + offset for m in MEMORY_MAPPINGS if m.data] 480 481 482def update_current_cpu_variable(): 483 global CPU_POINTERS, MEMORY_MAPPINGS 484 485 if len(CPU_POINTERS) <= 1: 486 # NOTE: If we are debugging single CPU platform, just fallback to cpu 487 gdb.set_convenience_variable('current_cpu', gdb.parse_and_eval('cpu')) 488 return 489 490 current_pc = int(gdb.parse_and_eval('$pc').const_value()) 491 current_mapping = next((m for m in MEMORY_MAPPINGS if current_pc >= m.start and current_pc < m.end), None) 492 if current_mapping is None: 493 return 494 495 index = next(index for index, mapping in enumerate(m for m in MEMORY_MAPPINGS if m.data) if mapping.path == current_mapping.path) 496 current_cpu = gdb.Value(CPU_POINTERS[index]).cast(gdb.lookup_type('CPUState').pointer().pointer()).referenced_value() 497 gdb.set_convenience_variable('current_cpu', current_cpu) 498 499 500def before_prompt(): 501 cache_memory_mappings() 502 update_cpu_pointers() 503 update_current_cpu_variable() 504 505 guest_pc, opcode, instruction = get_current_instruction() 506 if guest_pc is None: 507 return 508 509 global GUEST_PC 510 511 if guest_pc == GUEST_PC: 512 return 513 514 GUEST_PC = guest_pc 515 gdb.set_convenience_variable('guest_pc', guest_pc) 516 517 if opcode is None: 518 return 519 520 instruction = instruction or 'n/a' 521 522 banner = '----- tlib debug ' + '-' * 20 523 gdb.write(banner + '\n') 524 gdb.write(f'Current PC: 0x{guest_pc:x}\n') 525 gdb.write(f'Emulated instruction: {instruction} (0x{opcode:x})\n') 526 gdb.write('-' * len(banner) + '\n') 527 528 529def stop_event(event): 530 if not isinstance(event, gdb.BreakpointEvent): 531 return 532 533 global ENV_BREAKPOINT 534 is_env_breakpoint = any(bkpt == ENV_BREAKPOINT for bkpt in event.breakpoints) 535 if not ENV_BREAKPOINT or not is_env_breakpoint: 536 return 537 538 # NOTE: Update current CPU variable 539 update_current_cpu_variable() 540 541 current_tb = get_current_tb() 542 if current_tb is None: 543 global GUEST_PC 544 GUEST_PC = 0 545 gdb.execute('continue') 546 return 547 548 ENV_BREAKPOINT.delete() 549 ENV_BREAKPOINT = None 550 551 guest, host, _ = current_tb[0] 552 gdb.write(f'Creating hook on guest pc: 0x{guest:x}\n') 553 gdb.Breakpoint(f'*0x{host:x}', gdb.BP_BREAKPOINT, temporary=True) 554 gdb.execute('continue') 555 556 557gdb.events.before_prompt.connect(before_prompt) 558gdb.events.stop.connect(stop_event) 559