仿真平台内核初版 -tlib库 包含<sparc arm riscv powerPC>
This commit is contained in:
24
tools/tlib_helper/README.md
Normal file
24
tools/tlib_helper/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# GDB helper functions for debugging tlib
|
||||
|
||||
`tlib_helper` is a collection of helper functions that give the user a better context of the whole Renode simulation while debugging the translated guest code execution in [tlib](https://github.com/antmicro/tlib).
|
||||
|
||||
## Features
|
||||
|
||||
* Create breakpoint on next guest instruction,
|
||||
* Disassemble current instruction,
|
||||
* Disassemble current Translation Block,
|
||||
* Read bytes from the system bus,
|
||||
* Support for multiple CPUs through the `$_cpu(index)` convenience function,
|
||||
* Access to current guest PC through the `$guest_pc` variable.
|
||||
|
||||
## Usage
|
||||
|
||||
All functionality is self-contained in the single `gdbscript.py`, therefore it is enough to just source the script in GDB using the `source` command. For example, to attach GDB to already running dotnet-built Renode instance the following command can be used:
|
||||
|
||||
```sh
|
||||
$ gdb -ex 'source tools/tlib_helper/gdbscript.py' \
|
||||
-ex 'handle SIG34 nostop noprint' \
|
||||
-p $(pgrep --full 'dotnet.*Renode.dll')"
|
||||
```
|
||||
|
||||
Additional information about commands can be accessed through `help renode` GDB command.
|
||||
546
tools/tlib_helper/gdbscript.py
Normal file
546
tools/tlib_helper/gdbscript.py
Normal file
@@ -0,0 +1,546 @@
|
||||
#
|
||||
# Copyright (c) 2010-2025 Antmicro
|
||||
#
|
||||
# This file is licensed under the MIT License.
|
||||
# Full license text is available in 'licenses/MIT.txt'.
|
||||
#
|
||||
|
||||
import ctypes
|
||||
import dataclasses
|
||||
import re
|
||||
|
||||
from enum import IntEnum, auto
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ENV_BREAKPOINT = None
|
||||
MEMORY_MAPPINGS = []
|
||||
CPU_POINTERS = []
|
||||
GUEST_PC = None
|
||||
|
||||
|
||||
class Disassembler:
|
||||
def __init__(self, triple, name, flags=0):
|
||||
library_path = self._get_library_path()
|
||||
assert library_path is not None, 'could not find libllvm-disas.so path'
|
||||
|
||||
self._library = ctypes.CDLL(str(library_path))
|
||||
self._library.llvm_create_disasm_cpu_with_flags.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint32]
|
||||
self._library.llvm_create_disasm_cpu_with_flags.restype = ctypes.c_void_p
|
||||
|
||||
self._library.llvm_disasm_instruction.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64,ctypes.c_char_p, ctypes.c_uint32]
|
||||
self._library.llvm_disasm_instruction.restype = ctypes.c_int
|
||||
self._library.llvm_disasm_dispose.argtypes = [ctypes.c_void_p]
|
||||
self._context = self._library.llvm_create_disasm_cpu_with_flags(
|
||||
triple,
|
||||
name,
|
||||
flags)
|
||||
assert self._context != 0, 'could not initialize llvm disassembler'
|
||||
|
||||
@classmethod
|
||||
def _get_library_path(cls):
|
||||
if hasattr(cls, '_cached_library_path'):
|
||||
return cls._cached_library_path
|
||||
|
||||
inferior = gdb.selected_inferior()
|
||||
if not inferior:
|
||||
return None
|
||||
|
||||
with open(f'/proc/{inferior.pid}/cmdline', 'rb') as f:
|
||||
cmdline = f.read().split(b'\x00')
|
||||
cmdline = [arg.decode('ascii') for arg in cmdline]
|
||||
|
||||
if len(cmdline) > 1 and cmdline[1].endswith('Renode.dll'):
|
||||
# NOTE: We've built Renode from source code
|
||||
start_path = Path(cmdline[1])
|
||||
else:
|
||||
# NOTE: Running from bundled binary (e.g. portable)
|
||||
start_path = Path(f'/proc/{inferior.pid}/exe').resolve()
|
||||
|
||||
path = Path(start_path)
|
||||
while not (path / '.renode-root').exists():
|
||||
parent = path.parent
|
||||
if path == parent:
|
||||
return None
|
||||
path = parent
|
||||
|
||||
cls._cached_library_path = path / 'lib' / 'resources' / 'llvm' / 'libllvm-disas.so'
|
||||
return cls._cached_library_path
|
||||
|
||||
def __del__(self):
|
||||
self._library.llvm_disasm_dispose(self._context)
|
||||
self._context = 0
|
||||
|
||||
def disassemble(self, data: bytes):
|
||||
output = bytes(256)
|
||||
op_len = self._library.llvm_disasm_instruction(
|
||||
self._context,
|
||||
data,
|
||||
len(data),
|
||||
output,
|
||||
len(output))
|
||||
return output.rstrip(b'\0').decode('ascii', 'ignore').strip(), op_len
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MemoryMapping:
|
||||
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]*)')
|
||||
|
||||
start: int
|
||||
end: int
|
||||
size: int
|
||||
perms: str
|
||||
path: str
|
||||
|
||||
@property
|
||||
def executable(self):
|
||||
return 'x' in self.perms
|
||||
|
||||
def __post_init__(self):
|
||||
for field in dataclasses.fields(self):
|
||||
value = getattr(self, field.name)
|
||||
if field.type is int and not isinstance(value, int):
|
||||
setattr(self, field.name, int(value, 16))
|
||||
|
||||
|
||||
class Architecture(IntEnum):
|
||||
AARCH32 = auto()
|
||||
AARCH64 = auto()
|
||||
RISCV = auto()
|
||||
XTENSA = auto()
|
||||
I386 = auto()
|
||||
UNKNOWN = auto()
|
||||
|
||||
@property
|
||||
def insn_start_words(self):
|
||||
return ({
|
||||
Architecture.AARCH32: 2,
|
||||
Architecture.AARCH64: 3,
|
||||
Architecture.RISCV: 1,
|
||||
Architecture.XTENSA: 1,
|
||||
Architecture.I386: 2,
|
||||
}).get(self, 1)
|
||||
|
||||
@property
|
||||
def pc(self):
|
||||
expr = ({
|
||||
Architecture.AARCH32: 'cpu->regs[15]',
|
||||
}).get(self, 'cpu->pc')
|
||||
return int(gdb.parse_and_eval(expr).const_value())
|
||||
|
||||
|
||||
def source(callable):
|
||||
"""Convenience decorator for sourcing gdb commands"""
|
||||
callable()
|
||||
return callable
|
||||
|
||||
|
||||
@source
|
||||
class Renode(gdb.Command):
|
||||
"""Utility functions for debugging Renode"""
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('renode', gdb.COMMAND_USER, prefix=True)
|
||||
|
||||
|
||||
@source
|
||||
class ConvenienceRenodeReadBytes(gdb.Function):
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('_renode_read_bytes')
|
||||
|
||||
def invoke(self, addr, length):
|
||||
data = read_guest_bytes(int(addr), int(length))
|
||||
return gdb.Value(data, gdb.lookup_type('uint8_t').array(length - 1))
|
||||
|
||||
|
||||
@source
|
||||
class ConvenienceCpu(gdb.Function):
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('_cpu')
|
||||
|
||||
def invoke(self, index):
|
||||
return gdb.Value(CPU_POINTERS[index]).referenced_value()
|
||||
|
||||
|
||||
@source
|
||||
class RenodeReadBytes(gdb.Command):
|
||||
"""Read bytes from guest memory through SystemBus
|
||||
|
||||
renode read-bytes address length
|
||||
|
||||
This command is wrapper over tlib_read_byte function, which
|
||||
reads bytes using SystemBus.ReadByte method"""
|
||||
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('renode read-bytes', gdb.COMMAND_USER)
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
args = gdb.string_to_argv(arg)
|
||||
gdb.execute('p/x $_renode_read_bytes(%s, %s)' % (args[0], args[1]))
|
||||
|
||||
|
||||
@source
|
||||
class RenodeNextInstruction(gdb.Command):
|
||||
"""Creates a breakpoint on next guest instruction
|
||||
|
||||
renode next-instruction [cpu-index]
|
||||
|
||||
Creates a breakpoint on next guest instruction, potentially waiting on
|
||||
new translation block. If <cpu-index> is given, breakpoint will be on
|
||||
next instruction for given cpu. When ommited, current cpu will be used instead"""
|
||||
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('renode next-instruction', gdb.COMMAND_USER)
|
||||
|
||||
def _create_pending_breakpoint(self, cpu):
|
||||
global ENV_BREAKPOINT
|
||||
ENV_BREAKPOINT = gdb.Breakpoint(f'{cpu}->current_tb', gdb.BP_WATCHPOINT, gdb.WP_WRITE, True)
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
cpu = 'cpu'
|
||||
if arg:
|
||||
cpu = f'$_cpu({arg})'
|
||||
|
||||
current_tb = get_current_tb(cpu)
|
||||
if current_tb is None:
|
||||
self._create_pending_breakpoint(cpu)
|
||||
return
|
||||
|
||||
index = 0
|
||||
if GUEST_PC is not None:
|
||||
for guest_pc, _, _ in current_tb:
|
||||
if guest_pc > GUEST_PC:
|
||||
break
|
||||
index += 1
|
||||
|
||||
if index >= len(current_tb):
|
||||
self._create_pending_breakpoint(cpu)
|
||||
return
|
||||
|
||||
guest, host, _ = current_tb[index]
|
||||
gdb.write(f'Creating hook on guest pc: 0x{guest:x}\n')
|
||||
gdb.execute(f'tbreak *0x{host:x}', from_tty=True)
|
||||
|
||||
|
||||
@source
|
||||
class RenodePrintTranslationBlock(gdb.Command):
|
||||
"""Prints disassembly for current TranslationBlock
|
||||
|
||||
renode print-tb"""
|
||||
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__('renode print-tb', gdb.COMMAND_USER)
|
||||
|
||||
def invoke(self, arg, from_tty):
|
||||
current_tb = get_current_tb()
|
||||
if current_tb is None:
|
||||
return
|
||||
|
||||
guest_pc = GUEST_PC or detect_architecture().pc
|
||||
|
||||
for guest, host, _ in current_tb:
|
||||
_, instr = disassemble_instruction(guest)
|
||||
instr = instr or 'n/a'
|
||||
current_line = '=>' if guest == guest_pc else ' '
|
||||
gdb.write(f'{current_line} [0x{host:08x}] 0x{guest:08x}: {instr}\n')
|
||||
|
||||
|
||||
def read_host_byte(ptr):
|
||||
return int(gdb.parse_and_eval(f'*(uint8_t*)0x{ptr:x}').const_value())
|
||||
|
||||
|
||||
def read_guest_bytes(ptr, len):
|
||||
b = []
|
||||
for i in range(len):
|
||||
b.append(read_guest_byte(ptr + i))
|
||||
return bytes(b)
|
||||
|
||||
|
||||
def read_guest_byte(ptr):
|
||||
return int(
|
||||
gdb.parse_and_eval(
|
||||
f"tlib_read_byte_callback$({ptr}, "
|
||||
+ f"cpu_get_state_for_memory_transaction(cpu, {ptr}, ACCESS_DATA_LOAD))"
|
||||
).const_value()
|
||||
)
|
||||
|
||||
|
||||
def decode_sleb128(ptr):
|
||||
"""
|
||||
This function is based on decode_sleb128 from tlib/arch/translate-all.c
|
||||
"""
|
||||
|
||||
val = 0
|
||||
byte, shift = 0, 0
|
||||
|
||||
while True:
|
||||
byte = read_host_byte(ptr)
|
||||
ptr += 1
|
||||
val |= (byte & 0x7f) << shift
|
||||
val &= 0xffffffff
|
||||
shift += 7
|
||||
|
||||
if (byte & 0x80) == 0:
|
||||
break
|
||||
|
||||
if shift < 32 and (byte & 0x40) != 0:
|
||||
val |= 0xffffffff << shift
|
||||
val &= 0xffffffff
|
||||
|
||||
return val, ptr
|
||||
|
||||
|
||||
def get_current_tb(cpu='cpu'):
|
||||
tb_defined = int(gdb.parse_and_eval(f'{cpu}->current_tb').const_value()) != 0
|
||||
if not tb_defined:
|
||||
return None
|
||||
|
||||
tb_pc = int(gdb.parse_and_eval(f'{cpu}->current_tb->pc').const_value())
|
||||
host_pc = int(gdb.parse_and_eval(f'{cpu}->current_tb->tc_ptr').const_value())
|
||||
search_ptr = int(gdb.parse_and_eval(f'{cpu}->current_tb->tc_search').const_value())
|
||||
num_inst = int(gdb.parse_and_eval(f'{cpu}->current_tb->icount').const_value())
|
||||
|
||||
insn_start_words = detect_architecture().insn_start_words
|
||||
data = [ tb_pc ]
|
||||
data.extend([0 for _ in range(insn_start_words - 1)])
|
||||
mapping = []
|
||||
|
||||
for _ in range(num_inst):
|
||||
for j in range(insn_start_words):
|
||||
data_delta, search_ptr = decode_sleb128(search_ptr)
|
||||
data[j] += data_delta
|
||||
|
||||
host_start = host_pc
|
||||
host_pc_delta, search_ptr = decode_sleb128(search_ptr)
|
||||
host_pc += host_pc_delta
|
||||
mapping.append((data[0], host_start, host_pc))
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def get_current_guest_pc():
|
||||
current_tb = get_current_tb()
|
||||
if current_tb is None:
|
||||
return None
|
||||
|
||||
current_pc = int(gdb.parse_and_eval('$pc').const_value())
|
||||
for guest, _, host_end in current_tb:
|
||||
if host_end > current_pc:
|
||||
return guest
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def disassemble_instruction(ptr):
|
||||
model, triple = get_model_and_triple()
|
||||
if model is None:
|
||||
return None, None
|
||||
|
||||
opcode = read_guest_bytes(ptr, 4)
|
||||
disas = Disassembler(triple.encode('ascii'), model.encode('ascii'))
|
||||
result, op_len = disas.disassemble(opcode)
|
||||
opcode = opcode[:op_len]
|
||||
|
||||
return int.from_bytes(opcode, 'little'), result
|
||||
|
||||
|
||||
def get_current_instruction():
|
||||
guest_pc = get_current_guest_pc()
|
||||
if guest_pc is None:
|
||||
return None, None, None
|
||||
|
||||
opcode, instr = disassemble_instruction(guest_pc)
|
||||
if opcode is None:
|
||||
return guest_pc, None, None
|
||||
|
||||
return guest_pc, opcode, instr
|
||||
|
||||
|
||||
def detect_architecture():
|
||||
# NOTE: Check if arm
|
||||
try:
|
||||
gdb.parse_and_eval('cpu->thumb')
|
||||
try:
|
||||
# NOTE: Check aarch64
|
||||
gdb.parse_and_eval('cpu->aarch64')
|
||||
return Architecture.AARCH64
|
||||
except:
|
||||
return Architecture.AARCH32
|
||||
except:
|
||||
pass
|
||||
|
||||
# NOTE: Check if RISC-V
|
||||
try:
|
||||
gdb.parse_and_eval('cpu->mhartid')
|
||||
return Architecture.RISCV
|
||||
except:
|
||||
pass
|
||||
|
||||
# NOTE: Check if xtensa
|
||||
try:
|
||||
gdb.parse_and_eval('cpu->config')
|
||||
return Architecture.XTENSA
|
||||
except:
|
||||
pass
|
||||
|
||||
# NOTE: Check if I386
|
||||
try:
|
||||
gdb.parse_and_eval('cpu->eip')
|
||||
return Architecture.I386
|
||||
except:
|
||||
pass
|
||||
|
||||
return Architecture.UNKNOWN
|
||||
|
||||
|
||||
def get_model_and_triple():
|
||||
arch = detect_architecture()
|
||||
if arch is Architecture.UNKNOWN:
|
||||
return None, None
|
||||
|
||||
if arch is Architecture.AARCH32 or arch is Architecture.AARCH64:
|
||||
this_cpu_addr = gdb.parse_and_eval('&cpu')
|
||||
this_cpu_id = int(gdb.parse_and_eval('cpu->cp15.c0_cpuid').const_value())
|
||||
# For some reason parse_and_eval('arm_cpu_names') can pick up the wrong one, so use the
|
||||
# per-objfile lookup.
|
||||
cpu_names_sym = get_symbol_within_library('arm_cpu_names', this_cpu_addr)
|
||||
if cpu_names_sym is None:
|
||||
return None, None
|
||||
arm_cpu_names = cpu_names_sym.value()
|
||||
index = 0
|
||||
|
||||
while arm_cpu_names[index]['name'] != 0:
|
||||
cpu_id = int(arm_cpu_names[index]['id'].const_value())
|
||||
if cpu_id == this_cpu_id:
|
||||
model = str(arm_cpu_names[index]['name'].string())
|
||||
break
|
||||
index += 1
|
||||
else:
|
||||
return None, None
|
||||
|
||||
if model == 'cortex-r52':
|
||||
triple = 'arm'
|
||||
elif arch is Architecture.AARCH64:
|
||||
triple = 'arm64'
|
||||
else:
|
||||
if 'cortex-m' in model:
|
||||
triple = 'thumb'
|
||||
else:
|
||||
triple = 'armv7a'
|
||||
|
||||
if triple == 'armv7a' and int(gdb.parse_and_eval('cpu->thumb').const_value()) > 0:
|
||||
triple = 'thumb'
|
||||
|
||||
return model, triple
|
||||
|
||||
if arch is Architecture.RISCV:
|
||||
try:
|
||||
gdb.parse_and_eval('get_reg_pointer_64')
|
||||
triple = 'riscv64'
|
||||
model = 'rv64'
|
||||
except:
|
||||
triple = 'riscv32'
|
||||
model = 'rv32'
|
||||
|
||||
misa = gdb.parse_and_eval('cpu->misa_mask')
|
||||
extensions = {chr(ord('a') + index) for index in range(32) if misa & (1 << index) > 0}
|
||||
extensions &= set('imafdcv')
|
||||
|
||||
model += ''.join(extensions)
|
||||
return model, triple
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def cache_memory_mappings():
|
||||
global MEMORY_MAPPINGS
|
||||
|
||||
MEMORY_MAPPINGS = []
|
||||
mappings = gdb.execute('info proc mappings', from_tty=False, to_string=True)
|
||||
for line in mappings.splitlines():
|
||||
match = MemoryMapping.REGEX.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
mapping = MemoryMapping(*match.groups())
|
||||
if '-Antmicro.Renode.translate-' not in mapping.path:
|
||||
continue
|
||||
if not mapping.executable:
|
||||
# objfile_for_address() returns None when used with an address from the segment that
|
||||
# contains the ELF headers (the first one), so skip all segments except the one that
|
||||
# contains .text (the only executable mapping backed by the translation libary file)
|
||||
continue
|
||||
|
||||
MEMORY_MAPPINGS.append(mapping)
|
||||
MEMORY_MAPPINGS.sort(key=lambda m: m.path)
|
||||
|
||||
|
||||
def get_symbol_within_library(sym, lib_address):
|
||||
objfile = gdb.current_progspace().objfile_for_address(lib_address)
|
||||
if objfile is None:
|
||||
return None
|
||||
|
||||
return objfile.lookup_global_symbol(sym) or objfile.lookup_static_symbol(sym)
|
||||
|
||||
|
||||
def update_cpu_pointers():
|
||||
global CPU_POINTERS
|
||||
|
||||
CPU_POINTERS = [
|
||||
get_symbol_within_library("cpu", m.start).value().address for m in MEMORY_MAPPINGS
|
||||
]
|
||||
|
||||
|
||||
def before_prompt():
|
||||
cache_memory_mappings()
|
||||
update_cpu_pointers()
|
||||
|
||||
guest_pc, opcode, instruction = get_current_instruction()
|
||||
if guest_pc is None:
|
||||
return
|
||||
|
||||
global GUEST_PC
|
||||
|
||||
if guest_pc == GUEST_PC:
|
||||
return
|
||||
|
||||
GUEST_PC = guest_pc
|
||||
gdb.set_convenience_variable('guest_pc', guest_pc)
|
||||
|
||||
if opcode is None:
|
||||
return
|
||||
|
||||
instruction = instruction or 'n/a'
|
||||
|
||||
banner = '----- tlib debug ' + '-' * 20
|
||||
gdb.write(banner + '\n')
|
||||
gdb.write(f'Current PC: 0x{guest_pc:x}\n')
|
||||
gdb.write(f'Emulated instruction: {instruction} (0x{opcode:x})\n')
|
||||
gdb.write('-' * len(banner) + '\n')
|
||||
|
||||
|
||||
def stop_event(event):
|
||||
if not isinstance(event, gdb.BreakpointEvent):
|
||||
return
|
||||
|
||||
global ENV_BREAKPOINT
|
||||
is_env_breakpoint = any(bkpt == ENV_BREAKPOINT for bkpt in event.breakpoints)
|
||||
if not ENV_BREAKPOINT or not is_env_breakpoint:
|
||||
return
|
||||
|
||||
current_tb = get_current_tb()
|
||||
if current_tb is None:
|
||||
global GUEST_PC
|
||||
GUEST_PC = 0
|
||||
gdb.execute('continue')
|
||||
return
|
||||
|
||||
ENV_BREAKPOINT.delete()
|
||||
ENV_BREAKPOINT = None
|
||||
|
||||
guest, host, _ = current_tb[0]
|
||||
gdb.write(f'Creating hook on guest pc: 0x{guest:x}\n')
|
||||
gdb.Breakpoint(f'*0x{host:x}', gdb.BP_BREAKPOINT, temporary=True)
|
||||
gdb.execute('continue')
|
||||
|
||||
|
||||
gdb.events.before_prompt.connect(before_prompt)
|
||||
gdb.events.stop.connect(stop_event)
|
||||
Reference in New Issue
Block a user