1#!/usr/bin/env python3 2# 3# Copyright (c) 2010-2025 Antmicro 4# 5# This file is licensed under the MIT License. 6# Full license text is available in 'licenses/MIT.txt'. 7# 8 9import argparse 10import platform 11import sys 12import os 13import gzip 14import typing 15from enum import Enum 16 17from ctypes import cdll, c_char_p, POINTER, c_void_p, c_ubyte, c_uint64, c_byte, c_size_t, cast 18 19# Allow directly using this as a script, without installation 20try: 21 import execution_tracer.dwarf as dwarf 22 import execution_tracer.coverview_integration as coverview_integration 23except ImportError: 24 import dwarf 25 import coverview_integration 26 27FILE_SIGNATURE = b"ReTrace" 28FILE_VERSION = b"\x04" 29HEADER_LENGTH = 10 30MEMORY_ACCESS_LENGTH = 25 31RISCV_VECTOR_CONFIGURATION_LENGTH = 16 32 33 34class AdditionalDataType(Enum): 35 Empty = 0 36 MemoryAccess = 1 37 RiscVVectorConfiguration = 2 38 39 40class MemoryAccessType(Enum): 41 MemoryIORead = 0 42 MemoryIOWrite = 1 43 MemoryRead = 2 44 MemoryWrite = 3 45 InsnFetch = 4 46 47class Header(): 48 def __init__(self, pc_length, has_opcodes, extra_length=0, uses_thumb_flag=False, triple_and_model=None): 49 self.pc_length = pc_length 50 self.has_opcodes = has_opcodes 51 self.extra_length = extra_length 52 self.uses_thumb_flag = uses_thumb_flag 53 self.triple_and_model = triple_and_model 54 55 def __str__(self): 56 return "Header: pc_length: {}, has_opcodes: {}, extra_length: {}, uses_thumb_flag: {}, triple_and_model: {}".format( 57 self.pc_length, self.has_opcodes, self.extra_length, self.uses_thumb_flag, self.triple_and_model) 58 59 60def read_header(file): 61 if file.read(len(FILE_SIGNATURE)) != FILE_SIGNATURE: 62 raise InvalidFileFormatException("File signature isn't detected.") 63 64 version = file.read(1) 65 if version != FILE_VERSION: 66 raise InvalidFileFormatException(f"Unsuported file format version {version}, expected {FILE_VERSION}") 67 68 pc_length_raw = file.read(1) 69 opcodes_raw = file.read(1) 70 if len(pc_length_raw) != 1 or len(opcodes_raw) != 1: 71 raise InvalidFileFormatException("Invalid file header") 72 73 if opcodes_raw[0] == 0: 74 return Header(pc_length_raw[0], False, 0, False, None) 75 elif opcodes_raw[0] == 1: 76 uses_thumb_flag_raw = file.read(1) 77 identifier_length_raw = file.read(1) 78 if len(uses_thumb_flag_raw) != 1 or len(identifier_length_raw) != 1: 79 raise InvalidFileFormatException("Invalid file header") 80 81 uses_thumb_flag = uses_thumb_flag_raw[0] == 1 82 identifier_length = identifier_length_raw[0] 83 triple_and_model_raw = file.read(identifier_length) 84 if len(triple_and_model_raw) != identifier_length: 85 raise InvalidFileFormatException("Invalid file header") 86 87 triple_and_model = triple_and_model_raw.decode("utf-8") 88 extra_length = 2 + identifier_length 89 90 return Header(pc_length_raw[0], True, extra_length, uses_thumb_flag, triple_and_model) 91 else: 92 raise InvalidFileFormatException("Invalid opcodes field at file header") 93 94 95def read_file(file, disassemble, llvm_disas_path): 96 header = read_header(file) 97 return TraceData(file, header, disassemble, llvm_disas_path) 98 99 100def bytes_to_hex(bytes, zero_padded=True): 101 integer = int.from_bytes(bytes, byteorder="little", signed=False) 102 format_string = "0{}X".format(len(bytes)*2) if zero_padded else "X" 103 return "0x{0:{fmt}}".format(integer, fmt=format_string) 104 105 106class TraceData: 107 disassembler = None 108 disassembler_thumb = None 109 thumb_mode = False 110 instructions_left_in_block = 0 111 112 def __init__(self, file: typing.IO, header: Header, disassemble: bool, llvm_disas_path: str): 113 self.file = file 114 self.pc_length = int(header.pc_length) 115 self.has_pc = (self.pc_length != 0) 116 self.has_opcodes = bool(header.has_opcodes) 117 self.extra_length = header.extra_length 118 self.uses_thumb_flag = header.uses_thumb_flag 119 self.triple_and_model = header.triple_and_model 120 self.disassemble = disassemble 121 if self.disassemble: 122 triple, model = header.triple_and_model.split(" ") 123 self.disassembler = LLVMDisassembler(triple, model, llvm_disas_path) 124 if self.uses_thumb_flag: 125 self.disassembler_thumb = LLVMDisassembler("thumb", model, llvm_disas_path) 126 127 def __iter__(self): 128 self.file.seek(HEADER_LENGTH + self.extra_length, 0) 129 return self 130 131 def __next__(self): 132 additional_data = [] 133 134 if self.uses_thumb_flag and self.instructions_left_in_block == 0: 135 thumb_flag_raw = self.file.read(1) 136 if len(thumb_flag_raw) != 1: 137 # No more data frames to read 138 raise StopIteration 139 140 self.thumb_mode = thumb_flag_raw[0] == 1 141 142 block_length_raw = self.file.read(8) 143 if len(block_length_raw) != 8: 144 raise InvalidFileFormatException("Unexpected end of file") 145 146 # The `instructions_left_in_block` counter is kept only for traces produced by cores that can switch between ARM and Thumb mode. 147 self.instructions_left_in_block = int.from_bytes(block_length_raw, byteorder="little", signed=False) 148 149 if self.uses_thumb_flag: 150 self.instructions_left_in_block -= 1 151 152 pc = self.file.read(self.pc_length) 153 opcode_length = self.file.read(int(self.has_opcodes)) 154 155 if self.pc_length != len(pc): 156 # No more data frames to read 157 raise StopIteration 158 if self.has_opcodes and len(opcode_length) == 0: 159 if self.has_pc: 160 raise InvalidFileFormatException("Unexpected end of file") 161 else: 162 # No more data frames to read 163 raise StopIteration 164 165 if self.has_opcodes: 166 opcode_length = opcode_length[0] 167 opcode = self.file.read(opcode_length) 168 if len(opcode) != opcode_length: 169 raise InvalidFileFormatException("Unexpected end of file") 170 else: 171 opcode = b"" 172 173 additional_data_type = AdditionalDataType(self.file.read(1)[0]) 174 while (additional_data_type is not AdditionalDataType.Empty): 175 if additional_data_type is AdditionalDataType.MemoryAccess: 176 additional_data.append(self.parse_memory_access_data()) 177 elif additional_data_type is AdditionalDataType.RiscVVectorConfiguration: 178 additional_data.append(self.parse_riscv_vector_configuration_data()) 179 180 try: 181 additional_data_type = AdditionalDataType(self.file.read(1)[0]) 182 except IndexError: 183 break 184 return (pc, opcode, additional_data, self.thumb_mode) 185 186 def parse_memory_access_data(self): 187 data = self.file.read(MEMORY_ACCESS_LENGTH) 188 if len(data) != MEMORY_ACCESS_LENGTH: 189 raise InvalidFileFormatException("Unexpected end of file") 190 type = MemoryAccessType(data[0]) 191 address = bytes_to_hex(data[1:9], zero_padded=False) 192 value = bytes_to_hex(data[9:17], zero_padded=False) 193 address_physical = bytes_to_hex(data[17:], zero_padded=False) 194 195 if address == address_physical: 196 return f"{type.name} with address {address}, value {value}" 197 else: 198 return f"{type.name} with address {address} => {address_physical}, value {value}" 199 200 201 def parse_riscv_vector_configuration_data(self): 202 data = self.file.read(RISCV_VECTOR_CONFIGURATION_LENGTH) 203 if len(data) != RISCV_VECTOR_CONFIGURATION_LENGTH: 204 raise InvalidFileFormatException("Unexpected end of file") 205 vl = bytes_to_hex(data[0:8], zero_padded=False) 206 vtype = bytes_to_hex(data[8:16], zero_padded=False) 207 return f"Vector configured to VL: {vl}, VTYPE: {vtype}" 208 209 def format_entry(self, entry): 210 (pc, opcode, additional_data, thumb_mode) = entry 211 if self.pc_length: 212 pc_str = bytes_to_hex(pc) 213 if self.has_opcodes: 214 opcode_str = bytes_to_hex(opcode) 215 output = "" 216 if self.pc_length and self.has_opcodes: 217 output = f"{pc_str}: {opcode_str}" 218 elif self.pc_length: 219 output = pc_str 220 elif self.has_opcodes: 221 output = opcode_str 222 else: 223 output = "" 224 225 if self.has_opcodes and self.disassemble: 226 disas = self.disassembler_thumb if thumb_mode else self.disassembler 227 _, instruction = disas.get_instruction(opcode) 228 output += " " + instruction.decode("utf-8") 229 230 if len(additional_data) > 0: 231 output += "\n" + "\n".join(additional_data) 232 233 return output 234 235 236class InvalidFileFormatException(Exception): 237 pass 238 239 240class LLVMDisassembler(): 241 def __init__(self, triple, cpu, llvm_disas_path): 242 try: 243 self.lib = cdll.LoadLibrary(llvm_disas_path) 244 except OSError: 245 raise FileNotFoundError('Could not find valid `libllvm-disas` library. Please specify the correct path with the --llvm-disas-path argument.') 246 247 self.__init_library() 248 249 self._context = self.lib.llvm_create_disasm_cpu(c_char_p(triple.encode('utf-8')), c_char_p(cpu.encode('utf-8'))) 250 if not self._context: 251 raise RuntimeError('CPU or triple name not detected by LLVM. Disassembling will not be possible.') 252 253 def __del__(self): 254 if hasattr(self, '_context'): 255 self.lib.llvm_disasm_dispose(self._context) 256 257 def __init_library(self): 258 self.lib.llvm_create_disasm_cpu.argtypes = [c_char_p, c_char_p] 259 self.lib.llvm_create_disasm_cpu.restype = POINTER(c_void_p) 260 261 self.lib.llvm_disasm_dispose.argtypes = [POINTER(c_void_p)] 262 263 self.lib.llvm_disasm_instruction.argtypes = [POINTER(c_void_p), POINTER(c_ubyte), c_uint64, c_char_p, c_size_t] 264 self.lib.llvm_disasm_instruction.restype = c_size_t 265 266 def get_instruction(self, opcode): 267 opcode_buf = cast(c_char_p(opcode), POINTER(c_ubyte)) 268 disas_str = cast((c_byte * 1024)(), c_char_p) 269 270 bytes_read = self.lib.llvm_disasm_instruction(self._context, opcode_buf, c_uint64(len(opcode)), disas_str, 1024) 271 272 return (bytes_read, disas_str.value) 273 274 275def print_coverage_report(report): 276 for line in report: 277 yield f"{line.most_executions():5d}:\t {line.content.rstrip()}" 278 279 280def handle_coverage(args, trace_data): 281 coverage_config = dwarf.Coverage( 282 elf_file_handler=args.coverage_binary, 283 code_filenames=args.coverage_code, 284 substitute_paths=args.sub_source_path, 285 debug=args.debug, 286 print_unmatched_address=args.print_unmatched_address, 287 lazy_line_cache=args.lazy_line_cache, 288 ) 289 290 report = coverage_config.report_coverage(trace_data) 291 if args.legacy: 292 printed_report = print_coverage_report(report) 293 else: 294 printed_report = coverage_config.convert_to_lcov(report) 295 296 if args.coverage_output != None: 297 if args.export_for_coverview: 298 if not coverview_integration.create_coverview_archive( 299 args.coverage_output, 300 printed_report, 301 coverage_config._code_files, 302 args.coverview_config 303 ): 304 sys.exit(1) 305 else: 306 for line in printed_report: 307 args.coverage_output.write(f"{line}\n") 308 else: 309 for line in printed_report: 310 print(line) 311 312 313def main(): 314 parser = argparse.ArgumentParser(description="Renode's ExecutionTracer binary format reader") 315 parser.add_argument("--debug", default=False, action="store_true", help="enable additional debug logs to stdout") 316 parser.add_argument("--decompress", action="store_true", default=False, 317 help="decompress trace file, without the flag decompression is enabled based on a file extension") 318 parser.add_argument("--force-disable-decompression", action="store_true", default=False, help="never attempt to decompress the trace file") 319 320 subparsers = parser.add_subparsers(title='subcommands', dest='subcommands', required=True) 321 trace_parser = subparsers.add_parser('inspect', help='Inspect the binary trace format') 322 trace_parser.add_argument("file", help="binary trace file") 323 324 trace_parser.add_argument("--disassemble", action="store_true", default=False) 325 trace_parser.add_argument("--llvm-disas-path", default=None, help="path to libllvm-disas library") 326 327 cov_parser = subparsers.add_parser('coverage', help='Generate coverage reports') 328 cov_parser.add_argument("file", help="binary trace file") 329 330 cov_parser.add_argument("--binary", dest='coverage_binary', required=True, default=None, type=argparse.FileType('rb'), help="path to an ELF file with DWARF data") 331 cov_parser.add_argument("--sources", dest='coverage_code', default=None, nargs='+', type=str, help="path to a (list of) source file(s)") 332 cov_parser.add_argument("--output", dest='coverage_output', default=None, type=argparse.FileType('w'), help="path to the output coverage file") 333 cov_parser.add_argument("--legacy", default=False, action="store_true", help="Output data in a legacy text-based format") 334 cov_parser.add_argument("--export-for-coverview", default=False, action="store_true", help="Pack data to a format compatible with the Coverview project (https://github.com/antmicro/coverview)") 335 cov_parser.add_argument("--coverview-config", default=None, type=str, help="Provide parameters for Coverview integration configuration JSON") 336 cov_parser.add_argument("--print-unmatched-address", default=False, action="store_true", help="Print addresses not matched to any source lines") 337 cov_parser.add_argument("--sub-source-path", default=[], nargs='*', action='extend', type=dwarf.PathSubstitution.from_arg, help="Substitute a part of sources' path. Format is: old_path:new_path") 338 cov_parser.add_argument("--lazy-line-cache", default=False, action="store_true", help="Disable line to address eager cache generation. For big programs, reduce memory usage, but process traces much slower") 339 args = parser.parse_args() 340 341 # Look for the libllvm-disas library in default location 342 if args.subcommands == 'inspect' and args.disassemble and args.llvm_disas_path is None: 343 p = platform.system() 344 if p == 'Darwin': 345 ext = '.dylib' 346 elif p == 'Windows': 347 ext = '.dll' 348 else: 349 ext = '.so' 350 351 lib_name = 'libllvm-disas' + ext 352 353 lib_search_paths = [ 354 os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir, "lib", "resources", "llvm"), 355 os.path.dirname(os.path.realpath(__file__)), 356 os.getcwd() 357 ] 358 359 for search_path in lib_search_paths: 360 lib_path = os.path.join(search_path, lib_name) 361 if os.path.isfile(lib_path): 362 args.llvm_disas_path = lib_path 363 break 364 365 if args.llvm_disas_path is None: 366 raise FileNotFoundError('Could not find ' + lib_name + ' in any of the following locations: ' + ', '.join([os.path.abspath(path) for path in lib_search_paths])) 367 368 try: 369 filename, file_extension = os.path.splitext(args.file) 370 if (args.decompress or file_extension == ".gz") and not args.force_disable_decompression: 371 file_open = gzip.open 372 else: 373 file_open = open 374 375 with file_open(args.file, "rb") as file: 376 if args.subcommands == 'coverage': 377 trace_data = read_file(file, False, None) 378 else: 379 trace_data = read_file(file, args.disassemble, args.llvm_disas_path) 380 if args.subcommands == 'coverage': 381 if args.export_for_coverview: 382 if args.legacy: 383 print("'--export-for-coverview' implies LCOV-compatible format") 384 args.legacy = False 385 if not args.coverage_output: 386 raise ValueError("Specify a file with '--output' when packing an archive for Coverview") 387 388 handle_coverage(args, trace_data) 389 else: 390 for entry in trace_data: 391 print(trace_data.format_entry(entry)) 392 except BrokenPipeError: 393 # Avoid crashing when piping the results e.g. to less 394 sys.exit(0) 395 except (ValueError, RuntimeError) as err: 396 sys.exit(f"Error during execution: {err}") 397 except (FileNotFoundError, InvalidFileFormatException) as err: 398 sys.exit(f"Error while loading file: {err}") 399 except KeyboardInterrupt: 400 sys.exit(1) 401 402if __name__ == "__main__": 403 main() 404