# pylint:disable=superfluous-parens,too-many-boolean-expressions,line-too-long
from __future__ import annotations
from typing import Any, TYPE_CHECKING
import itertools
import logging
import math
import re
import string
from collections import defaultdict, OrderedDict
from enum import Enum, unique

import networkx
from sortedcontainers import SortedDict
import capstone

import claripy
import cle
import pyvex
from cle.address_translator import AT
from archinfo import Endness
from archinfo.arch_soot import SootAddressDescriptor
from archinfo.arch_arm import is_arm_arch, get_real_address_if_arm

from angr.analyses import AnalysesHub
from angr.misc.ux import once
from angr.knowledge_plugins.cfg import CFGNode, MemoryDataSort, MemoryData, IndirectJump, IndirectJumpType
from angr.knowledge_plugins.xrefs import XRef, XRefType
from angr.knowledge_plugins.functions import Function
from angr.codenode import HookNode
from angr import sim_options as o
from angr.errors import (
    AngrCFGError,
    AngrSkipJobNotice,
    SimEngineError,
    SimMemoryError,
    SimTranslationError,
    SimValueError,
    SimIRSBNoDecodeError,
)
from angr.utils.constants import DEFAULT_STATEMENT
from angr.utils.funcid import (
    is_function_security_check_cookie,
    is_function_security_check_cookie_strict,
    is_function_security_init_cookie,
    is_function_security_init_cookie_win8,
    is_function_likely_security_init_cookie,
)
from angr.analyses import ForwardAnalysis
from angr.rustylib import SegmentList
from .cfg_arch_options import CFGArchOptions
from .cfg_base import CFGBase
from .indirect_jump_resolvers.jumptable import JumpTableResolver

if TYPE_CHECKING:
    from angr.block import Block
    from angr.engines.pcode.lifter import IRSB as PcodeIRSB


VEX_IRSB_MAX_SIZE = 400


l = logging.getLogger(name=__name__)


class ContinueScanningNotification(RuntimeError):
    """
    A notification raised by _next_code_addr_core() to indicate no code address is found and _next_code_addr_core()
    should be invoked again.
    """


class ARMDecodingMode:
    """
    Enums indicating decoding mode for ARM code.
    """

    ARM = 0
    THUMB = 1


class DecodingAssumption:
    """
    Describes the decoding mode (ARM/THUMB) for a given basic block identified by its address.
    """

    def __init__(self, addr: int, size: int, mode: int):
        self.addr = addr
        self.size = size
        self.mode = mode
        self.attempted_arm = mode == ARMDecodingMode.ARM
        self.attempted_thumb = mode == ARMDecodingMode.THUMB
        self.data_segs = None

    def add_data_seg(self, addr: int, size: int) -> None:
        if self.data_segs is None:
            self.data_segs = set()
        self.data_segs.add((addr, size))


class FunctionReturn:
    """
    FunctionReturn describes a function call in a specific location and its return location. Hashable and equatable
    """

    __slots__ = (
        "call_site_addr",
        "callee_func_addr",
        "caller_func_addr",
        "return_to",
    )

    def __init__(self, callee_func_addr, caller_func_addr, call_site_addr, return_to):
        self.callee_func_addr = callee_func_addr
        self.caller_func_addr = caller_func_addr
        self.call_site_addr = call_site_addr
        self.return_to = return_to

    def __eq__(self, other):
        """
        Comparison

        :param FunctionReturn other: The other object
        :return: True if equal, False otherwise
        """
        return (
            self.callee_func_addr == other.callee_func_addr
            and self.caller_func_addr == other.caller_func_addr
            and self.call_site_addr == other.call_site_addr
            and self.return_to == other.return_to
        )

    def __hash__(self):
        return hash((self.callee_func_addr, self.caller_func_addr, self.call_site_addr, self.return_to))


class PendingJobs:
    """
    A collection of pending jobs during CFG recovery.
    """

    def __init__(self, kb, deregister_job_callback):
        self._jobs = OrderedDict()  # A mapping between function addresses and lists of pending jobs
        self._kb = kb
        self._deregister_job_callback = deregister_job_callback

        self._returning_functions = set()
        self._updated_functions = set()  # Addresses of functions whose returning status have changed between two
        # consecutive calls to cleanup().
        self._job_count = 0

    @property
    def _functions(self):
        return self._kb.functions

    def __len__(self):
        return self._job_count

    def __bool__(self):
        return self._job_count > 0

    __nonzero__ = __bool__

    def _pop_job(self, func_addr: int | None):
        jobs = self._jobs[func_addr]
        j = jobs.pop(-1)
        if not jobs:
            del self._jobs[func_addr]
        self._job_count -= 1
        return j

    def add_job(self, job):
        func_addr = job.returning_source
        if func_addr not in self._jobs:
            self._jobs[func_addr] = []
        self._jobs[func_addr].append(job)
        self._job_count += 1

    def pop_job(self, returning=True):
        """
        Pop a job from the pending jobs list.

        When returning == True, we prioritize the jobs whose functions are known to be returning (function.returning is
        True). As an optimization, we are sorting the pending jobs list according to job.function.returning.

        :param bool returning: Only pop a pending job if the corresponding function returns.
        :return: A pending job if we can find one, or None if we cannot find any that satisfies the requirement.
        :rtype: angr.analyses.cfg.cfg_fast.CFGJob
        """

        if not self:
            return None

        if not returning:
            if None in self._jobs:
                # return sites for indirect calls are all put under the None key. in the majority of cases, indirect
                # calls are returning.
                return self._pop_job(None)
            return self._pop_job(next(reversed(self._jobs.keys())))

        # Prioritize returning functions
        for func_addr in reversed(self._jobs):
            if func_addr not in self._returning_functions:
                continue
            return self._pop_job(func_addr)

        return None

    def cleanup(self):
        """
        Remove those pending exits if:
        a) they are the return exits of non-returning SimProcedures
        b) they are the return exits of non-returning syscalls
        b) they are the return exits of non-returning functions

        :return: None
        """

        pending_exits_to_remove = defaultdict(list)

        for func_addr in self._updated_functions:
            if func_addr not in self._jobs:
                continue
            jobs = self._jobs[func_addr]
            for i, pe in enumerate(jobs):
                if pe.returning_source is None:
                    # The original call failed. This pending exit must be followed.
                    continue

                func = self._functions.function(pe.returning_source)
                if func is None:
                    # Why does it happen?
                    l.warning(
                        "An expected function at %s is not found. Please report it to Fish.",
                        pe.returning_source if pe.returning_source is not None else "None",
                    )
                    continue

                if func.returning is False:
                    # Oops, it's not returning
                    # Remove this pending exit
                    pending_exits_to_remove[pe.returning_source].append(i)

        for func_addr, indices in pending_exits_to_remove.items():
            jobs = self._jobs[func_addr]
            for index in reversed(indices):
                job = jobs[index]
                self._deregister_job_callback(job.func_addr, job)
                del jobs[index]
                self._job_count -= 1
            if not jobs:
                del self._jobs[func_addr]

        self.clear_updated_functions()

    def add_returning_function(self, func_addr):
        """
        Mark a function as returning.

        :param int func_addr: Address of the function that returns.
        :return:              None
        """

        self._returning_functions.add(func_addr)
        self._updated_functions.add(func_addr)

    def add_nonreturning_function(self, func_addr):
        """
        Mark a function as not returning.

        :param int func_addr:   Address of the function that does not return.
        :return:                None
        """

        self._updated_functions.add(func_addr)

    def clear_updated_functions(self):
        """
        Clear the updated_functions set.

        :return:    None
        """

        self._updated_functions.clear()


#
# Descriptors of edges in individual function graphs
#


class FunctionEdge:
    """
    Describes an edge in functions' transition graphs. Base class for all types of edges.
    """

    __slots__ = (
        "ins_addr",
        "src_func_addr",
        "stmt_idx",
    )

    def apply(self, cfg):
        raise NotImplementedError


class FunctionTransitionEdge(FunctionEdge):
    """
    Describes a transition edge in functions' transition graphs.
    """

    __slots__ = (
        "dst_addr",
        "dst_func_addr",
        "is_exception",
        "src_node",
        "to_outside",
    )

    def __init__(
        self,
        src_node,
        dst_addr,
        src_func_addr,
        to_outside=False,
        dst_func_addr=None,
        stmt_idx=None,
        ins_addr=None,
        is_exception=False,
    ):
        self.src_node = src_node
        self.dst_addr = dst_addr
        self.src_func_addr = src_func_addr
        self.to_outside = to_outside
        self.dst_func_addr = dst_func_addr
        self.stmt_idx = stmt_idx
        self.ins_addr = ins_addr
        self.is_exception = is_exception

    def apply(self, cfg):
        to_outside = self.to_outside
        if not to_outside:
            # is it jumping to outside? Maybe we are seeing more functions now.
            dst_node = cfg.model.get_any_node(self.dst_addr, force_fastpath=True)
            if dst_node is not None and dst_node.function_address != self.src_func_addr:
                to_outside = True
        return cfg._function_add_transition_edge(
            self.dst_addr,
            self.src_node,
            self.src_func_addr,
            to_outside=to_outside,
            dst_func_addr=self.dst_func_addr,
            stmt_idx=self.stmt_idx,
            ins_addr=self.ins_addr,
            is_exception=self.is_exception,
        )


class FunctionCallEdge(FunctionEdge):
    """
    Describes a call edge in functions' transition graphs.
    """

    __slots__ = ("dst_addr", "ret_addr", "src_node", "syscall")

    def __init__(self, src_node, dst_addr, ret_addr, src_func_addr, syscall=False, stmt_idx=None, ins_addr=None):
        self.src_node = src_node
        self.dst_addr = dst_addr
        self.ret_addr = ret_addr
        self.src_func_addr = src_func_addr
        self.syscall = syscall
        self.stmt_idx = stmt_idx
        self.ins_addr = ins_addr

    def apply(self, cfg):
        return cfg._function_add_call_edge(
            self.dst_addr,
            self.src_node,
            self.src_func_addr,
            syscall=self.syscall,
            stmt_idx=self.stmt_idx,
            ins_addr=self.ins_addr,
        )


class FunctionFakeRetEdge(FunctionEdge):
    """
    Describes a FakeReturn (also called fall-through) edge in functions' transition graphs.
    """

    __slots__ = ("confirmed", "dst_addr", "src_node")

    def __init__(self, src_node, dst_addr, src_func_addr, confirmed=None):
        self.src_node = src_node
        self.dst_addr = dst_addr
        self.src_func_addr = src_func_addr
        self.confirmed = confirmed

    def apply(self, cfg):
        return cfg._function_add_fakeret_edge(
            self.dst_addr,
            self.src_node,
            self.src_func_addr,
            confirmed=self.confirmed,
        )


class FunctionReturnEdge(FunctionEdge):
    """
    Describes a return (from a function call or a syscall) edge in functions' transition graphs.
    """

    __slots__ = ("dst_func_addr", "ret_from_addr", "ret_to_addr")

    def __init__(self, ret_from_addr, ret_to_addr, dst_func_addr):
        self.ret_from_addr = ret_from_addr
        self.ret_to_addr = ret_to_addr
        self.dst_func_addr = dst_func_addr

    def apply(self, cfg):
        return cfg._function_add_return_edge(self.ret_from_addr, self.ret_to_addr, self.dst_func_addr)


#
# CFGJob
#


@unique
class CFGJobType(Enum):
    """
    Defines the type of work of a CFGJob
    """

    NORMAL = 0
    FUNCTION_PROLOGUE = 1
    COMPLETE_SCANNING = 2
    IFUNC_HINTS = 3
    DATAREF_HINTS = 4
    EH_FRAME_HINTS = 5


class CFGJob:
    """
    Defines a job to work on during the CFG recovery
    """

    __slots__ = (
        "_func_edges",
        "addr",
        "func_addr",
        "gp",
        "job_type",
        "jumpkind",
        "last_addr",
        "ret_target",
        "returning_source",
        "src_ins_addr",
        "src_node",
        "src_stmt_idx",
        "syscall",
    )

    def __init__(
        self,
        addr: int,
        func_addr: int,
        jumpkind: str,
        ret_target: int | None = None,
        last_addr: int | None = None,
        src_node: CFGNode | None = None,
        src_ins_addr: int | None = None,
        src_stmt_idx: int | None = None,
        returning_source=None,
        syscall: bool = False,
        func_edges: list | None = None,
        job_type: CFGJobType = CFGJobType.NORMAL,
        gp: int | None = None,
    ):
        self.addr = addr
        self.func_addr = func_addr
        self.jumpkind = jumpkind
        self.ret_target = ret_target
        self.last_addr = last_addr
        self.src_node = src_node
        self.src_ins_addr = src_ins_addr
        self.src_stmt_idx = src_stmt_idx
        self.returning_source = returning_source
        self.syscall = syscall
        self.job_type = job_type
        self.gp = gp  # Used in MIPS32/MIPS64. Value of the gp register in the caller function. Only set at call sites.

        self._func_edges = func_edges

    def add_function_edge(self, edge):
        if self._func_edges is None:
            self._func_edges = []
        self._func_edges.append(edge)

    def apply_function_edges(self, cfg, clear=False):
        if not self._func_edges:
            return
        for edge in self._func_edges:
            edge.apply(cfg)
        if clear:
            self._func_edges = None

    def __repr__(self):
        if isinstance(self.addr, SootAddressDescriptor):
            return f"<CFGJob {self.addr}>"
        return "<CFGJob{} {:#08x} @ func {:#08x}>".format(" syscall" if self.syscall else "", self.addr, self.func_addr)

    def __eq__(self, other):
        return (
            self.addr == other.addr
            and self.func_addr == other.func_addr
            and self.jumpkind == other.jumpkind
            and self.ret_target == other.ret_target
            and self.last_addr == other.last_addr
            and self.src_node == other.src_node
            and self.src_stmt_idx == other.src_stmt_idx
            and self.src_ins_addr == other.src_ins_addr
            and self.returning_source == other.returning_source
            and self.syscall == other.syscall
            and self.job_type == other.job_type
            and self.gp == other.gp
        )

    def __hash__(self):
        return hash(
            (
                self.addr,
                self.func_addr,
                self.jumpkind,
                self.ret_target,
                self.last_addr,
                self.src_node,
                self.src_stmt_idx,
                self.src_ins_addr,
                self.returning_source,
                self.syscall,
                self.job_type,
                self.gp,
            )
        )


class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):  # pylint: disable=abstract-method
    """
    We find functions inside the given binary, and build a control-flow graph in very fast manners: instead of
    simulating program executions, keeping track of states, and performing expensive data-flow analysis, CFGFast will
    only perform light-weight analyses combined with some heuristics, and with some strong assumptions.

    In order to identify as many functions as possible, and as accurate as possible, the following operation sequence
    is followed:

    # Active scanning

        - If the binary has "function symbols" (TODO: this term is not accurate enough), they are starting points of
          the code scanning
        - If the binary does not have any "function symbol", we will first perform a function prologue scanning on the
          entire binary, and start from those places that look like function beginnings
        - Otherwise, the binary's entry point will be the starting point for scanning

    # Passive scanning

        - After all active scans are done, we will go through the whole image and scan all code pieces

    Due to the nature of those techniques that are used here, a base address is often not required to use this analysis
    routine. However, with a correct base address, CFG recovery will almost always yield a much better result. A custom
    analysis, called GirlScout, is specifically made to recover the base address of a binary blob. After the base
    address is determined, you may want to reload the binary with the new base address by creating a new Project object,
    and then re-recover the CFG.
    """

    # TODO: Move arch_options to CFGBase, and add those logic to CFGEmulated as well.

    PRINTABLES = string.printable.replace("\x0b", "").replace("\x0c", "").encode()
    SPECIAL_THUNKS = {
        "AMD64": {
            bytes.fromhex("E807000000F3900FAEE8EBF9488D642408C3"): ("ret",),
            bytes.fromhex("E807000000F3900FAEE8EBF948890424C3"): ("jmp", "rax"),
        }
    }

    tag = "CFGFast"

    def __init__(
        self,
        binary=None,
        objects=None,
        regions=None,
        pickle_intermediate_results=False,
        symbols=True,
        function_prologues: bool | None = None,
        resolve_indirect_jumps=True,
        force_segment=False,
        force_smart_scan: bool | None = None,
        force_complete_scan=False,
        indirect_jump_target_limit=100000,
        data_references=True,
        cross_references=False,
        normalize=False,
        start_at_entry=True,
        function_starts=None,
        extra_memory_regions=None,
        data_type_guessing_handlers=None,
        arch_options=None,
        indirect_jump_resolvers=None,
        base_state=None,
        exclude_sparse_regions=True,
        skip_specific_regions=True,
        heuristic_plt_resolving=None,
        detect_tail_calls=False,
        low_priority=False,
        cfb=None,
        model=None,
        eh_frame=True,
        exceptions=True,
        skip_unmapped_addrs=True,
        nodecode_window_size=512,
        nodecode_threshold=0.3,
        nodecode_step=16483,
        check_funcret_max_job=500,
        indirect_calls_always_return: bool | None = None,
        jumptable_resolver_resolves_calls: bool | None = None,
        start=None,  # deprecated
        end=None,  # deprecated
        collect_data_references=None,  # deprecated
        extra_cross_references=None,  # deprecated
        elf_eh_frame=None,  # deprecated
        **extra_arch_options,
    ):
        """
        :param binary:                  The binary to recover CFG on. By default the main binary is used.
        :param objects:                 A list of objects to recover the CFG on. By default it will recover the CFG of
                                        all loaded objects.
        :param iterable regions:        A list of tuples in the form of (start address, end address) describing memory
                                        regions that the CFG should cover.
        :param bool pickle_intermediate_results: If we want to store the intermediate results or not.
        :param bool symbols:            Get function beginnings from symbols in the binary.
        :param bool function_prologues: Scan the binary for function prologues, and use those positions as function
                                        beginnings
        :param bool resolve_indirect_jumps: Try to resolve indirect jumps. This is necessary to resolve jump targets
                                            from jump tables, etc.
        :param bool force_segment:      Force CFGFast to rely on binary segments instead of sections.
        :param bool force_complete_scan:    Perform a complete scan on the binary and maximize the number of identified
                                            code blocks.
        :param bool data_references:    Enables the collection of references to data used by individual instructions.
                                        This does not collect 'cross-references', particularly those that involve
                                        multiple instructions.  For that, see `cross_references`
        :param bool cross_references:   Whether CFGFast should collect "cross-references" from the entire program or
                                        not. This will populate the knowledge base with references to and from each
                                        recognizable address constant found in the code. Note that, because this
                                        performs constant propagation on the entire program, it may be much slower and
                                        consume more memory.
                                        This option implies `data_references=True`.
        :param bool normalize:          Normalize the CFG as well as all function graphs after CFG recovery.
        :param bool start_at_entry:     Begin CFG recovery at the entry point of this project. Setting it to False
                                        prevents CFGFast from viewing the entry point as one of the starting points of
                                        code scanning.
        :param list function_starts:    A list of extra function starting points. CFGFast will try to resume scanning
                                        from each address in the list.
        :param list extra_memory_regions: A list of 2-tuple (start-address, end-address) that shows extra memory
                                          regions. Integers falling inside will be considered as pointers.
        :param list indirect_jump_resolvers: A custom list of indirect jump resolvers. If this list is None or empty,
                                             default indirect jump resolvers specific to this architecture and binary
                                             types will be loaded.
        :param base_state:              A state to use as a backer for all memory loads
        :param bool detect_tail_calls:  Enable aggressive tail-call optimization detection.
        :param bool eh_frame:           Retrieve function starts (and maybe sizes later) from the .eh_frame of ELF
                                        binaries or exception records of PE binaries.
        :param skip_unmapped_addrs:     Ignore all branches into unmapped regions. True by default. You may want to set
                                        it to False if you are analyzing manually patched binaries or malware samples.
        :param indirect_calls_always_return:    Should CFG assume indirect calls must return or not. Assuming indirect
                                        calls must return will significantly reduce the number of constant propagation
                                        runs, but may reduce the overall CFG recovery precision when facing
                                        non-returning indirect calls. By default, we only assume indirect calls always
                                        return for large binaries (region > 50KB).
        :param jumptable_resolver_resolves_calls: Whether JumpTableResolver should resolve indirect calls or not. Most
                                        indirect calls in C++ binaries or UEFI binaries cannot be resolved using jump
                                        table resolver and must be resolved using their specific resolvers. By default,
                                        we will only disable JumpTableResolver from resolving indirect calls for large
                                        binaries (region > 50 KB).
        :param check_funcret_max_job    When popping return-site jobs out of the job queue, angr will prioritize jobs
                                        for which the callee is known to return. This check may be slow when there are
                                        a large amount of jobs in different caller functions, and this situation often
                                        occurs in obfuscated binaries where many functions never return. This parameter
                                        acts as a threshold to disable this check when the number of jobs in the queue
                                        exceeds this threshold.
        :param int start:               (Deprecated) The beginning address of CFG recovery.
        :param int end:                 (Deprecated) The end address of CFG recovery.
        :param CFGArchOptions arch_options: Architecture-specific options.
        :param dict extra_arch_options: Any key-value pair in kwargs will be seen as an arch-specific option and will
                                        be used to set the option value in self._arch_options.

        Extra parameters that angr.Analysis takes:

        :param progress_callback:       Specify a callback function to get the progress during CFG recovery.
        :param bool show_progressbar:   Should CFGFast show a progressbar during CFG recovery or not.
        :return: None
        """

        ForwardAnalysis.__init__(self, allow_merging=False)

        if start is not None or end is not None:
            l.warning(
                '"start" and "end" are deprecated and will be removed soon. Please use "regions" to specify one '
                "or more memory regions instead."
            )
            if regions is None:
                regions = [(start, end)]
            else:
                l.warning('"regions", "start", and "end" are all specified. Ignoring "start" and "end".')

        if binary is not None and not objects:
            objects = [binary]

        is_dotnet = (
            isinstance(self.project.loader.main_object, cle.backends.pe.PE)
            and self.project.loader.main_object.is_dotnet
        )

        if function_prologues is None:
            function_prologues = not is_dotnet
            if is_dotnet and once("dotnet_native"):
                l.warning("You're trying to analyze a .NET binary as native code. Are you sure?")
        if force_smart_scan is None:
            force_smart_scan = not is_dotnet
            if is_dotnet and once("dotnet_native"):
                l.warning("You're trying to analyze a .NET binary as native code. Are you sure?")

        CFGBase.__init__(
            self,
            "fast",
            0,
            normalize=normalize,
            binary=binary,
            objects=objects,
            regions=regions,
            exclude_sparse_regions=exclude_sparse_regions,
            skip_specific_regions=skip_specific_regions,
            force_segment=force_segment,
            base_state=base_state,
            resolve_indirect_jumps=resolve_indirect_jumps,
            indirect_jump_resolvers=indirect_jump_resolvers,
            indirect_jump_target_limit=indirect_jump_target_limit,
            detect_tail_calls=detect_tail_calls,
            skip_unmapped_addrs=skip_unmapped_addrs,
            low_priority=low_priority,
            model=model,
        )

        # necessary warnings
        if collect_data_references is not None:
            l.warning(
                '"collect_data_references" is deprecated and will be removed soon. Please use '
                '"data_references" instead'
            )
            data_references = collect_data_references
        if extra_cross_references is not None:
            l.warning(
                '"extra_cross_references" is deprecated and will be removed soon. Please use '
                '"cross_references" instead'
            )
            cross_references = extra_cross_references

        # data references collection and force smart scan must be enabled at the same time. otherwise decoding errors
        # caused by decoding data will lead to incorrect cascading re-lifting, which is suboptimal
        if force_smart_scan and not data_references:
            l.warning(
                'It is recommended to enable "data_references" if "force_smart_scan" is enabled for best '
                'result. Otherwise you may want to disable "force_smart_scan" or enable '
                '"force_complete_scan".'
            )

        # smart complete scanning and naive complete scanning cannot be enabled at the same time
        if force_smart_scan and force_complete_scan:
            l.warning(
                'You cannot enable "force_smart_scan" and "force_complete_scan" at the same time. I am disabling '
                '"force_complete_scan".'
            )
            force_complete_scan = False

        if elf_eh_frame is not None:
            l.warning('"elf_eh_frame" is deprecated and will be removed soon. Please use "eh_frame" instead.')
            eh_frame = eh_frame or elf_eh_frame

        self._pickle_intermediate_results = pickle_intermediate_results

        self._use_symbols = symbols
        self._use_function_prologues = function_prologues
        self._force_smart_scan = force_smart_scan
        self._force_complete_scan = force_complete_scan
        self._use_eh_frame = eh_frame
        self._use_exceptions = exceptions
        self._check_funcret_max_job = check_funcret_max_job

        self._nodecode_window_size = nodecode_window_size
        self._nodecode_threshold = nodecode_threshold
        self._nodecode_step = nodecode_step
        self._indirect_calls_always_return = indirect_calls_always_return
        self._jumptable_resolver_resolve_calls = jumptable_resolver_resolves_calls

        if self._indirect_calls_always_return is None:
            # heuristics
            self._indirect_calls_always_return = self._regions_size >= 50_000

        if self._jumptable_resolver_resolve_calls is None:
            # heuristics
            self._jumptable_resolver_resolve_calls = self._regions_size < 50_000
        for ijr in self.indirect_jump_resolvers:
            if isinstance(ijr, JumpTableResolver):
                ijr.resolve_calls = self._jumptable_resolver_resolve_calls

        if heuristic_plt_resolving is None:
            # If unspecified, we only enable heuristic PLT resolving when there is at least one binary loaded with the
            # ELF backend
            self._heuristic_plt_resolving = len(self.project.loader.all_elf_objects) > 0
        else:
            self._heuristic_plt_resolving = heuristic_plt_resolving

        self._start_at_entry = start_at_entry
        self._extra_function_starts = function_starts

        self._extra_memory_regions = extra_memory_regions

        self._cross_references = cross_references
        # You need data refs to get cross refs
        self._collect_data_ref = data_references or self._cross_references

        self._arch_options = (
            arch_options if arch_options is not None else CFGArchOptions(self.project.arch, **extra_arch_options)
        )

        self._data_type_guessing_handlers = [] if data_type_guessing_handlers is None else data_type_guessing_handlers

        self._cfb = cfb

        # mapping to all known thunks
        self._known_thunks = {}

        self._initial_state = None
        self._next_addr: int | None = None

        # Create the segment list
        self._seg_list = SegmentList()

        self._read_addr_to_run = defaultdict(list)
        self._write_addr_to_run = defaultdict(list)

        self._remaining_eh_frame_addrs: list[int] | None = None
        self._remaining_function_prologue_addrs: list[int] | None = None

        # exception handling
        self._exception_handling_by_endaddr = SortedDict()

        self.stage: str = ""

        #
        # Variables used during analysis
        #
