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