1#!/usr/bin/env python3 2# 3# Copyright (c) 2010-2023 Antmicro 4# 5# This file is licensed under the MIT License. 6# Full license text is available in 'licenses/MIT.txt'. 7# 8 9import asyncio 10import argparse 11import pexpect 12import psutil 13import re 14import telnetlib 15import difflib 16from time import time 17from os import path 18from typing import Any, Optional, Callable, Awaitable 19 20RENODE_GDB_PORT = 2222 21RENODE_TELNET_PORT = 12348 22RE_HEX = re.compile(r"0x[0-9A-Fa-f]+") 23RE_VEC_REGNAME = re.compile(r"v\d+") 24RE_FLOAT_REGNAME = re.compile(r"f[tsa]\d+") 25RE_GDB_ERRORS = ( 26 re.compile(r"\bUndefined .*?\.", re.MULTILINE), 27 re.compile(r"\bThe \"remote\" target does not support \".*?\"\.", re.MULTILINE), 28 re.compile(r"\bNo symbol \".*?\".*?\.", re.MULTILINE), 29 re.compile(r"\bCannot .*$", re.MULTILINE), 30 re.compile(r"\bRemote communication error\..*$", re.MULTILINE), 31 re.compile(r"\bRemote connection closed", re.MULTILINE), 32 re.compile(r"\bThe program has no registers.*?\.", re.MULTILINE), 33 re.compile(r"\bThe program is not being run.*?\.", re.MULTILINE), 34 re.compile(r"\b.*: cannot resolve name.*$", re.MULTILINE), 35 re.compile(r"\b.*: no such file or directory\.", re.MULTILINE), 36 re.compile(r"\bArgument required.*?\.", re.MULTILINE), 37 re.compile(r'\b.*: .*(not in executable format|file format not recognized)', re.MULTILINE), 38 re.compile(r"\bNo symbol table is loaded\.", re.MULTILINE), 39) 40 41parser = argparse.ArgumentParser( 42 description="Compare Renode execution with hardware/other simulator state using GDB") 43 44cmp_parser = parser.add_mutually_exclusive_group(required=True) 45cmp_parser.add_argument("-c", "--gdb-command", 46 dest="command", 47 default=None, 48 help="GDB command to run on both instances after each instruction. Outputs of these commands are compared against each other.") 49cmp_parser.add_argument("-R", "--register-list", 50 dest="registers", 51 action="store", 52 default=None, 53 help="Sequence of register names to compare. Formated as ';' separated list of register names, e.g. 'pc;ra'") 54 55parser.add_argument("-r", "--reference-command", 56 dest="reference_command", 57 action="store", 58 required=True, 59 help="Command used to run the GDB server provider used as a reference") 60parser.add_argument("-s", "--renode-script", 61 dest="renode_script", 62 action="store", 63 required=True, 64 help="Path to the '.resc' script") 65parser.add_argument("-p", "--reference-gdb-port", 66 type=int, 67 dest="reference_gdb_port", 68 action="store", 69 required=True, 70 help="Port on which the reference GDB server can be reached") 71parser.add_argument("--renode-gdb-port", 72 type=int, 73 dest="renode_gdb_port", 74 action="store", 75 default=RENODE_GDB_PORT, 76 help="Port on which Renode will comunicate with GDB server") 77parser.add_argument("-P", "--renode-telnet-port", 78 type=int, 79 dest="renode_telnet_port", 80 action="store", 81 default=RENODE_TELNET_PORT, 82 help="Port on which Renode will comunicate with telnet") 83parser.add_argument("-b", "--binary", 84 dest="debug_binary", 85 action="store", 86 required=True, 87 help="Path to ELF file with symbols") 88parser.add_argument("-x", "--renode-path", 89 dest="renode_path", 90 action="store", 91 default="renode", 92 help="Path to the Renode runscript") 93parser.add_argument("-g", "--gdb-path", 94 dest="gdb_path", 95 action="store", 96 default="/usr/bin/gdb", 97 help="Path to the GDB binary to be run") 98parser.add_argument("-f", "--start-frame", 99 dest="start_frame", 100 action="store", 101 default=None, 102 help="Sequence of jumps to reach target frame. Formated as 'addr, occurrence', separated with ';', e.g. '_start,1;printf,7'") 103parser.add_argument("-i", "--interest-points", 104 dest="ips", 105 action="store", 106 default=None, 107 help="Sequence of address, interest points, after which state will be compared. Formatted as ';' spearated list of hexadecimal addresses, e.g. '0x8000;0x340eba3c'") 108parser.add_argument("-S", "--stop-address", 109 dest="stop_address", 110 action="store", 111 default=None, 112 help="Stop condition, if reached script will stop") 113 114SECTION_SEPARATOR = "==================================================" 115 116# A stack is a list of (address, nth_occurrence) tuples. 117# `address` is a PC value, as a hex string, e.g. "0x00f24710". 118# `nth_occurrence` is the number of times `address` was reached since the start of execution. 119# Therefore, assuming deterministic runtime, an arbitrary program state can be reached 120# by setting a breakpoint at `address` and continuing `nth_occurrence` times. 121Stack = list[tuple[str, int]] 122 123 124class Renode: 125 """A class for communicating with a remote instance of Renode.""" 126 def __init__(self, binary: str, port: int): 127 """Spawns a new instance of Renode and connects to it through Telnet.""" 128 print(f"* Starting Renode instance on telnet port {port}") 129 # making sure there is only one instance of Renode on this port 130 for p in psutil.process_iter(): 131 process_name = p.name().casefold() 132 if "renode" in process_name and str(port) in process_name: 133 print("!!! Found another instance of Renode running on the same port. Killing it before proceeding") 134 p.kill() 135 try: 136 self.proc = pexpect.spawn(f"{binary} --disable-gui --plain --port {port}", timeout=20) 137 self.proc.stripcr = True 138 self.proc.expect("Monitor available in telnet mode on port") 139 except pexpect.exceptions.EOF as err: 140 print("!!! Renode failed to start telnet server! (is --renode-path correct? is --renode-telnet-port available?)") 141 raise err 142 self.connection = telnetlib.Telnet("localhost", port) 143 # Sometimes first command does not work, hence we send this dummy one to make sure we got functional connection right after initialization 144 self.command("echo 'Connected to GDB comparator'") 145 146 def close(self) -> None: 147 """Closes the underlying Renode instance.""" 148 self.command("quit", expected_log="Disposed") 149 150 def command(self, input: str, expected_log: str = "") -> None: 151 """Sends an arbitrary command to the underlying Renode instance.""" 152 if not self.proc.isalive(): 153 print("!!! Renode has died!") 154 print("Process:") 155 print(str(self.proc)) 156 raise RuntimeError 157 158 input = input + "\n" 159 self.connection.write(input.encode()) 160 if expected_log != "": 161 try: 162 self.proc.expect([expected_log.encode()]) 163 except pexpect.TIMEOUT as err: 164 print(SECTION_SEPARATOR) 165 print(f"Renode command '{input.strip()}' failed!") 166 print(f"Expected regex '{expected_log}' was not found") 167 print("Process:") 168 print(str(self.proc)) 169 print(SECTION_SEPARATOR) 170 print(f"{err=} ; {type(err)=}") 171 exit(1) 172 173 def get_output(self) -> bytes: 174 """Reads all output from the Telnet connection.""" 175 return self.connection.read_all() 176 177 178class GDBInstance: 179 """A class for controlling a remote GDB instance.""" 180 def __init__(self, gdb_binary: str, port: int, debug_binary: str, name: str, target_process: pexpect.spawn): 181 """Spawns a new GDB instance and connects to it.""" 182 self.dimensions = (0, 4096) 183 self.name = name 184 self.last_cmd = "" 185 self.last_output = "" 186 self.task: Awaitable[Any] 187 self.target_process = target_process 188 print(f"* Connecting {self.name} GDB instance to target on port {port}") 189 self.process = pexpect.spawn(f"{gdb_binary} --silent --nx --nh", timeout=10, dimensions=self.dimensions) 190 self.process.timeout = 120 191 self.run_command("clear", async_=False) 192 self.run_command("set pagination off", async_=False) 193 self.run_command(f"file {debug_binary}", async_=False) 194 self.run_command(f"target remote :{port}", async_=False) 195 196 def close(self) -> None: 197 """Closes the underlying GDB instance.""" 198 self.run_command("quit", dont_wait_for_output=True, async_=False) 199 200 def progress_by(self, delta: int, type: str = "stepi") -> None: 201 """Steps `delta` times.""" 202 adjusted_timeout = max(120, int(delta) / 5) 203 self.run_command(type + (f" {delta}" if int(delta) > 1 else ""), timeout=adjusted_timeout) 204 205 def get_symbol_at(self, addr: str) -> str: 206 """Returns the name of the symbol which is stored at `addr` (`info symbol`).""" 207 self.run_command(f"info symbol {addr}", async_=False) 208 return self.last_output.splitlines()[-1] 209 210 def delete_breakpoints(self) -> None: 211 """Deletes all breakpoints.""" 212 self.run_command("clear", async_=False) 213 214 def run_command(self, command: str, timeout: float = 10, confirm: bool = False, dont_wait_for_output: bool = False, async_: bool = True) -> None: 215 """Send an arbitrary command to the underlying GDB instance.""" 216 if not self.process.isalive(): 217 print(f"!!! The {self.name} GDB process has died!") 218 print("Process:") 219 print(str(self.process)) 220 self.last_output = "" 221 raise RuntimeError 222 if not self.target_process.isalive(): 223 print(f"!!! {self.name} GDB's target has died!") 224 print("Target process:") 225 print(str(self.target_process)) 226 self.last_output = "" 227 raise RuntimeError 228 229 self.last_cmd = command 230 self.process.write(command + "\n") 231 if dont_wait_for_output: 232 return 233 try: 234 if not confirm: 235 result = self.process.expect(re.escape(command) + r".+\n", timeout, async_=async_) 236 self.task = result if async_ else None 237 if not async_: 238 self.last_output = "" 239 line = self.process.match[0].decode().strip("\r") 240 while "(gdb)" not in line: 241 self.last_output += line 242 self.process.expect([r".+\n", r"\(gdb\)"], timeout) 243 line = self.process.match[0].decode().strip("\r") 244 self.validate_response(self.last_output) 245 else: 246 self.process.expect("[(]y or n[)]") 247 self.process.writelines("y") 248 result = self.process.expect("[(]gdb[)]", async_=async_) 249 self.task = result if async_ else None 250 self.last_output = self.process.match[0].decode().strip("\r") 251 252 except pexpect.TIMEOUT as err: 253 print(f"!!! {self.name} GDB: Command '{command}' timed out!") 254 print("Process:") 255 print(str(self.process)) 256 self.last_output = "" 257 raise err 258 except pexpect.exceptions.EOF as err: 259 print(f"!!! {self.name} GDB: pexpect encountered an unexpected EOF (is --gdb-path correct?)") 260 print("Process:") 261 print(str(self.process)) 262 self.last_output = "" 263 raise err 264 265 def print_stack(self, stack: Stack) -> None: 266 """Prints a stack.""" 267 print("Address\t\tOccurrence\t\tSymbol") 268 for address, occurrence in stack: 269 print(f"{address}\t{occurrence}\t{self.get_symbol_at(address)}") 270 271 def validate_response(self, response: str) -> None: 272 """Scans a GDB response for common error messages.""" 273 for regex in RE_GDB_ERRORS: 274 err_match = regex.search(response) 275 if err_match is not None: 276 print(f"!!! {self.name} GDB: {err_match[0].strip()} (last command: \"{self.last_cmd}\")") 277 # Assuming we correctly identified a GDB error, this would be 278 # the right place to terminate execution. However, there is 279 # a risk of a false positive, so it's safer not to (if it is 280 # a critical error, it will most likely cause a timeout anyway). 281 282 async def get_pc(self) -> str: 283 """Returns the value of the PC register, as a hex string.""" 284 self.run_command("i r pc") 285 await self.expect() 286 pc_match = RE_HEX.search(self.last_output) 287 if pc_match is not None: 288 return pc_match[0] 289 else: 290 raise TypeError 291 292 async def expect(self, timeout: float = 10) -> None: 293 """Await execution of the last command to finish and update `self.last_output`.""" 294 try: 295 await self.task 296 line = self.process.match[0].decode().strip("\r") 297 self.last_output = "" 298 while "(gdb)" not in line: 299 self.last_output += line 300 self.task = self.process.expect([r".+\n", r"\(gdb\)"], timeout, async_=True) 301 await self.task 302 line = self.process.match[0].decode().strip("\r") 303 self.validate_response(self.last_output) 304 305 except pexpect.TIMEOUT as err: 306 print(f"!!! {self.name} GDB: Command '{self.last_cmd}' timed out!") 307 print("Process:") 308 print(str(self.process)) 309 self.last_output = "" 310 raise err 311 312 313class GDBComparator: 314 """A helper class to aggregate control over 2 `GDBInstance` objects.""" 315 316 COMMAND_NAME = "gdb_compare__print_registers" 317 COMMANDS = None 318 319 # REGISTER_CASES is an ordered list of (condition_func, cmd_builder_func) tuples. 320 # It is used to assign registers to groups based on their type, and for each such group 321 # have a dedicated function that constructs a gdb command to pretty-print those registers. 322 # Each tuple in REGISTER_CASES represents a group of registers. condition_func is used to 323 # determine whether a register belongs to the group. cmd_builder_func intakes a list of 324 # registers belonging to the group and returns a GDB command to print all their values. 325 # The order of tuples matters - only the first match is used. 326 RegNameTester = Callable[[str], bool] # condition_func type 327 CommandsBuilder = Callable[[list[str]], list[str]] # cmd_builder_func type 328 REGISTER_CASES: list[tuple[RegNameTester, CommandsBuilder]] = [ 329 (lambda reg: RE_VEC_REGNAME.fullmatch(reg) is not None, lambda regs: [f"p/x (char[])${reg}.b" for reg in regs]), 330 (lambda reg: RE_FLOAT_REGNAME.fullmatch(reg) is not None, lambda regs: [f"p/x (char[])${reg}" for reg in regs]), 331 (lambda _: True, lambda regs: ["printf \"" + ": 0x%x\\n".join(regs) + ": 0x%x\\n\",$" + ",$".join(regs)]), 332 ] 333 334 def __init__(self, args: argparse.Namespace, renode_proc: pexpect.spawn, ref_proc: pexpect.spawn): 335 """Creates 2 `GDBInstance` objects, one expecting to connect on port `args.renode_gdb_port` and the other on `args.reference_gdb_port`.""" 336 self.instances = [ 337 GDBInstance(args.gdb_path, args.renode_gdb_port, args.debug_binary, "Renode", renode_proc), 338 GDBInstance(args.gdb_path, args.reference_gdb_port, args.debug_binary, "Reference", ref_proc), 339 ] 340 self.cmd = args.command if args.command else self.build_command_from_register_list(args.registers.split(";")) 341 342 def close(self) -> None: 343 """Closes all owned instances.""" 344 for i in self.instances: 345 i.close() 346 347 def build_command_from_register_list(self, regs: list[str]) -> str: 348 """Defines a custom gdb command for pretty-printing all registers and returns its name.""" 349 if GDBComparator.COMMANDS is None: 350 # Assign registers to groups based on the RegNameTester functions 351 reg_groups: dict[GDBComparator.CommandsBuilder, list[str]] = {} 352 for reg in regs: 353 for test, cmds_builder in GDBComparator.REGISTER_CASES: 354 if test(reg): 355 reg_groups.setdefault(cmds_builder, []).append(reg) 356 break 357 358 # Compose a gdb script that defines a custom command for printing all groups of registers 359 GDBComparator.COMMANDS = [ 360 f"define {GDBComparator.COMMAND_NAME}", 361 *[cmd for cmds_builder, reg_group in reg_groups.items() for cmd in cmds_builder(reg_group)], 362 "end" 363 ] 364 365 # Warn if for any GDBInstance there is a register that was requested by the user 366 # but does not appear in the output of "info registers all" 367 for i in self.instances: 368 i.run_command("i r all", async_=False) 369 reported_regs = list(map(lambda x: x.split()[0], i.last_output.split("\n")[1:-1])) 370 not_found = list(filter(lambda reg: reg not in reported_regs, regs)) 371 if not_found: 372 print("WARNING: " + ", ".join(not_found) + " register[s] not found when executing 'info registers all' for " + i.name) 373 374 # Define the custom command 375 commands = GDBComparator.COMMANDS 376 for i in self.instances: 377 for cmd in commands[:-1]: 378 i.run_command(cmd, dont_wait_for_output=True, async_=False) 379 i.run_command(commands[-1], async_=False) 380 381 return GDBComparator.COMMAND_NAME 382 383 def delete_breakpoints(self) -> None: 384 """Deletes all breakpoints in all owned instances.""" 385 for i in self.instances: 386 i.delete_breakpoints() 387 388 def get_symbol_at(self, addr: str) -> str: 389 """Returns the name of the symbol which is stored at `addr` (`info symbol`).""" 390 return self.instances[0].get_symbol_at(addr) 391 392 def print_stack(self, stack: Stack) -> None: 393 """Prints a stack.""" 394 return self.instances[0].print_stack(stack) 395 396 async def run_command(self, cmd: Optional[str] = None, **kwargs: Any) -> list[str]: 397 """Sends an arbitrary command to all owned instances and returns a list of outputs.""" 398 cmd = cmd if cmd else self.cmd 399 for i in self.instances: 400 i.run_command(cmd, **kwargs) 401 await asyncio.gather(*[i.expect(**kwargs) for i in self.instances]) 402 return [i.last_output for i in self.instances] 403 404 async def get_pcs(self) -> list[str]: 405 """Returns a list containing the values of PC registers of all owned instances, as hex strings.""" 406 return await asyncio.gather(*[i.get_pc() for i in self.instances]) 407 408 async def progress_by(self, delta: int, type: str = "stepi") -> None: 409 """Steps `delta` times in all owned instances.""" 410 adjusted_timeout = max(120, int(delta) / 5) 411 await self.run_command(type + (f" {delta}" if int(delta) > 1 else ""), timeout=adjusted_timeout) 412 413 async def compare_instances(self, previous_pc: str) -> None: 414 """Compares the execution states of all owned instances. `previous_pc` must refer to the previous value of PC; it does not offer a choice.""" 415 for name, command in [("Opcode at previous pc", f"x/i {previous_pc}"), ("Frame", "frame"), ("Registers", "info registers all")]: 416 print("*** " + name + ":") 417 GDBComparator.compare_outputs(await self.run_command(command)) 418 419 @staticmethod 420 def compare_outputs(outputs: list[str]) -> None: 421 """Prints a comparison of two output strings (same & different values).""" 422 assert len(outputs) == 2 423 output1_dict: dict[str, str] = {} 424 output2_dict: dict[str, str] = {} 425 426 # Truncate 1st elements in outputs, because it's the repl 427 for output, output_dict in zip([x.split("\n")[1:] for x in outputs], [output1_dict, output2_dict]): 428 for x in output: 429 end_of_name = x.strip().find(" ") 430 name = x[:end_of_name].strip() 431 rest = x[end_of_name:].strip() 432 output_dict[name] = rest 433 434 output_same = "" 435 output_different = "" 436 437 for name in output1_dict.keys(): 438 if name in output2_dict: 439 if name == "": 440 continue 441 if output1_dict[name] != output2_dict[name]: 442 output_different += f">> {name}:\n" 443 output_different += string_compare(output1_dict[name], output2_dict[name]) + "\n" 444 else: 445 output_same += f">> {name}:\t{output1_dict[name]}\n" 446 447 if len(output_different) == 0: 448 print("Same:") 449 print(output_same) 450 else: 451 print("Same values:") 452 print(output_same) 453 print("Different values:") 454 print(output_different) 455 456 457def setup_processes(args: argparse.Namespace) -> tuple[Renode, pexpect.spawn, GDBComparator]: 458 """Spawns Renode, the reference process, `GDBComparator` and returns their handles (in that order).""" 459 reference = pexpect.spawn(args.reference_command, timeout=10) 460 renode = Renode(args.renode_path, args.renode_telnet_port) 461 renode.command("include @" + path.abspath(args.renode_script), expected_log="System bus created") 462 renode.command(f"machine StartGdbServer {args.renode_gdb_port}", expected_log=f"started on port :{args.renode_gdb_port}") 463 gdb_comparator = GDBComparator(args, renode.proc, reference) 464 renode.command("start") 465 return renode, reference, gdb_comparator 466 467def string_compare(renode_string: str, reference_string: str) -> str: 468 """Returns a pretty diff of two single-line strings.""" 469 BOLD = "\033[1m" 470 END = "\033[0m" 471 RED = "\033[91m" 472 GREEN = "\033[92m" 473 474 renode_string = re.sub(r"\x1b\[[0-9]*m", "", renode_string) 475 reference_string = re.sub(r"\x1b\[[0-9]*m", "", reference_string) 476 477 assert len(RED) == len(GREEN) 478 formatting_length = len(BOLD + RED + END) 479 480 s1_insertions = 0 481 s2_insertions = 0 482 diff = difflib.SequenceMatcher(None, renode_string, reference_string) 483 484 for type, s1_start, s1_end, s2_start, s2_end in diff.get_opcodes(): 485 if type == "equal": 486 continue 487 elif type == "replace": 488 s1_start += s1_insertions * formatting_length 489 s1_end += s1_insertions * formatting_length 490 s2_end += s2_insertions * formatting_length 491 s2_start += s2_insertions * formatting_length 492 renode_string = renode_string[:s1_start] + GREEN + BOLD + renode_string[s1_start:s1_end] + END + renode_string[s1_end:] 493 reference_string = reference_string[:s2_start] + RED + BOLD + reference_string[s2_start:s2_end] + END + reference_string[s2_end:] 494 s1_insertions += 1 495 s2_insertions += 1 496 elif type == "insert": 497 s2_end += s2_insertions * (len(BOLD) + len(RED) + len(END)) 498 s2_start += s2_insertions * (len(BOLD) + len(RED) + len(END)) 499 reference_string = reference_string[:s2_start] + RED + BOLD + \ 500 reference_string[s2_start:s2_end] + END + reference_string[s2_end:] 501 s2_insertions += 1 502 elif type == "delete": 503 s1_end += s1_insertions * (len(BOLD) + len(GREEN) + len(END)) 504 s1_start += s1_insertions * (len(BOLD) + len(GREEN) + len(END)) 505 renode_string = renode_string[:s1_start] + GREEN + BOLD + \ 506 renode_string[s1_start:s1_end] + END + renode_string[s1_end:] 507 s1_insertions += 1 508 return f"Renode: {renode_string}\nReference: {reference_string}" 509 510 511class CheckStatus: 512 """This class serves as an enum for possible outcomes of the `check` function.""" 513 STOP = 1 514 CONTINUE = 2 515 FOUND = 3 516 MISMATCH = 4 517 518 519async def check(stack: Stack, gdb_comparator: GDBComparator, previous_pc: str, previous_output: str, steps_count: int, exec_count: dict[str, int], time_of_start: float, args: argparse.Namespace) -> tuple[str, str, int]: 520 """Executes the next `gdb_comparator` instruction, compares the outputs and returns the new PC value, output and `CheckStatus`.""" 521 ren_pc, pc = await gdb_comparator.get_pcs() 522 pc_mismatch = False 523 if pc != ren_pc: 524 print("Renode and reference PC differs!") 525 print(string_compare(ren_pc, pc)) 526 print(f"\tPrevious PC: {previous_pc}") 527 pc_mismatch = True 528 if pc not in exec_count: 529 exec_count[pc] = 0 530 exec_count[pc] += 1 531 532 if args.stop_address and int(ren_pc, 16) == args.stop_address: 533 print("stop address reached") 534 return previous_pc, previous_output, CheckStatus.STOP 535 536 if not pc_mismatch: 537 output_ren, output_reference = map(lambda s: s.splitlines(), await gdb_comparator.run_command()) 538 539 for line in range(len(output_ren)): 540 if output_ren[line] != output_reference[line]: 541 print(SECTION_SEPARATOR) 542 print(f"!!! Difference in line {line + 1} of output:") 543 print(string_compare(output_ren[line], output_reference[line])) 544 print(f"Previous: {previous_output}") 545 break 546 else: 547 if steps_count % 10 == 0: 548 print(f"{steps_count} steps; current pc = {pc} {gdb_comparator.get_symbol_at(pc)}") 549 previous_pc = pc 550 previous_output = "\n".join(output_ren[1:]) 551 return previous_pc, previous_output, CheckStatus.CONTINUE 552 553 if pc_mismatch or (len(stack) > 0 and previous_pc == stack[-1][0]): 554 print(SECTION_SEPARATOR) 555 print("Found faulting insn at " + previous_pc + " " + gdb_comparator.get_symbol_at(previous_pc)) 556 elapsed_time = time() - time_of_start 557 print(f"Took {elapsed_time:.2f} seconds [~ {elapsed_time/steps_count:.2f} steps/sec]") 558 print(SECTION_SEPARATOR) 559 print("*** Stack:") 560 gdb_comparator.print_stack(stack) 561 print("*** Gdb command:") 562 print(args.command) 563 print(SECTION_SEPARATOR) 564 print("Gdb instances comparision:") 565 await gdb_comparator.compare_instances(previous_pc) 566 567 return previous_pc, previous_output, CheckStatus.FOUND 568 569 if previous_pc not in exec_count: 570 previous_pc = pc 571 print("Found point after which state is different. Adding to `stack` for later iterations") 572 occurrence = exec_count[previous_pc] 573 print(f"\tAddress: {previous_pc}\n\tOccurrence: {occurrence}") 574 stack.append((previous_pc, occurrence)) 575 exec_count = {} 576 print(SECTION_SEPARATOR) 577 578 return previous_pc, previous_output, CheckStatus.MISMATCH 579 580async def main() -> None: 581 """Script entry point.""" 582 args = parser.parse_args() 583 assert 0 <= args.reference_gdb_port <= 65535, "Illegal reference GDB port" 584 assert 0 <= args.renode_gdb_port <= 65535, "Illegal Renode GDB port" 585 assert 0 <= args.renode_telnet_port <= 65535, "Illegal Renode Telnet port" 586 assert args.reference_gdb_port != args.renode_gdb_port != args.renode_telnet_port, "Overlapping port numbers" 587 if args.stop_address: 588 args.stop_address = int(args.stop_address, 16) 589 590 pcs = [args.stop_address] if args.stop_address else [] 591 if args.ips: 592 pcs += [pc for pc in args.ips.split(";")] 593 594 execution_cmd = "continue" if args.ips else "nexti" 595 print(SECTION_SEPARATOR) 596 time_of_start = time() 597 previous_pc = "Unknown" 598 previous_output = "Unknown" 599 steps_count = 0 600 iterations_count = 0 601 stack = [] 602 603 if args.start_frame is not None: 604 jumps = args.start_frame.split(";") 605 for jump in jumps: 606 addr, occur = jump.split(",") 607 address = addr.strip() 608 occurrence = int(occur.strip()) 609 stack.append((address, occurrence)) 610 611 insn_found = False 612 613 while not insn_found: 614 iterations_count += 1 615 print("Preparing processes for iteration number " + str(iterations_count)) 616 renode, reference, gdb_comparator = setup_processes(args) 617 if len(stack) != 0: 618 print("Recreating stack; jumping to breakpoint at:") 619 for address, count in stack: 620 print("\t" + address + ", " + str(count) + " occurrence") 621 await gdb_comparator.run_command(f"break *{address}") 622 623 for _ in range(count): 624 await gdb_comparator.run_command("continue", timeout=120) 625 626 gdb_comparator.delete_breakpoints() 627 print("Stepping single instruction") 628 await gdb_comparator.progress_by(1) 629 630 for pc in pcs: 631 await gdb_comparator.run_command(f"br *{pc}") 632 633 exec_count: dict[str, int] = {} 634 print("Starting execution") 635 while True: 636 await gdb_comparator.run_command(execution_cmd) 637 steps_count += 1 638 639 previous_pc, previous_output, status = await check(stack, gdb_comparator, previous_pc, previous_output, steps_count, exec_count, time_of_start, args) 640 641 if status == CheckStatus.CONTINUE and execution_cmd == "continue": 642 await gdb_comparator.run_command("stepi") 643 steps_count += 1 644 645 previous_pc, previous_output, status = await check(stack, gdb_comparator, previous_pc, previous_output, steps_count, exec_count, time_of_start, args) 646 647 if status == CheckStatus.STOP: 648 return 649 elif status == CheckStatus.CONTINUE: 650 continue 651 elif status == CheckStatus.FOUND: 652 insn_found = True 653 break 654 elif status == CheckStatus.MISMATCH: 655 execution_cmd = "nexti" 656 gdb_comparator.close() 657 renode.close() 658 reference.close(force=True) 659 break 660 else: 661 exit(1) 662 663if __name__ == "__main__": 664 asyncio.run(main()) 665 exit(0) 666