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