# pylint:disable=line-too-long,multiple-statements
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import logging
from collections import defaultdict

import networkx
from sortedcontainers import SortedDict

import pyvex
from cle import (
    ELF,
    PE,
    Blob,
    PEStubs,
    TLSObject,
    MachO,
    ExternObject,
    KernelObject,
    FunctionHintSource,
    Hex,
    Coff,
    SRec,
    XBE,
)
from cle.backends import NamedRegion
import archinfo
from archinfo.arch_soot import SootAddressDescriptor, SootMethodDescriptor
from archinfo.arch_arm import is_arm_arch, get_real_address_if_arm

from angr.knowledge_plugins.functions.function_manager import FunctionManager
from angr.knowledge_plugins.functions.function import Function
from angr.knowledge_plugins.cfg import IndirectJump, CFGNode, CFGENode, CFGModel  # pylint:disable=unused-import
from angr.procedures.stubs.UnresolvableJumpTarget import UnresolvableJumpTarget
from angr.utils.constants import DEFAULT_STATEMENT
from angr.procedures.procedure_dict import SIM_PROCEDURES
from angr.errors import (
    AngrCFGError,
    SimTranslationError,
    SimMemoryError,
    SimIRSBError,
    SimEngineError,
    AngrUnsupportedSyscallError,
    SimError,
)
from angr.codenode import HookNode, BlockNode
from angr.engines.vex.lifter import VEX_IRSB_MAX_SIZE, VEX_IRSB_MAX_INST
from angr.analyses import Analysis
from angr.analyses.stack_pointer_tracker import StackPointerTracker
from angr.utils.orderedset import OrderedSet
from .indirect_jump_resolvers.default_resolvers import default_indirect_jump_resolvers

if TYPE_CHECKING:
    from angr.sim_state import SimState


l = logging.getLogger(name=__name__)


class CFGBase(Analysis):
    """
    The base class for control flow graphs.
    """

    tag: str | None = None
    _cle_pseudo_objects = (ExternObject, KernelObject, TLSObject)

    def __init__(
        self,
        sort,
        context_sensitivity_level,
        normalize=False,
        binary=None,
        objects=None,
        regions=None,
        exclude_sparse_regions=True,
        skip_specific_regions=True,
        force_segment=False,
        base_state=None,
        resolve_indirect_jumps=True,
        indirect_jump_resolvers=None,
        indirect_jump_target_limit=100000,
        detect_tail_calls=False,
        low_priority=False,
        skip_unmapped_addrs=True,
        sp_tracking_track_memory=True,
        model=None,
    ):
        """
        :param str sort:                            'fast' or 'emulated'.
        :param int context_sensitivity_level:       The level of context-sensitivity of this CFG (see documentation for
                                                    further details). It ranges from 0 to infinity.
        :param bool normalize:                      Whether the CFG as well as all Function graphs should be normalized.
        :param cle.backends.Backend 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 force_segment:                  Force CFGFast to rely on binary segments instead of sections.
        :param angr.SimState base_state:            A state to use as a backer for all memory loads.
        :param bool resolve_indirect_jumps:         Whether to try to resolve indirect jumps.
                                                    This is necessary to resolve jump targets from jump tables, etc.
        :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 int indirect_jump_target_limit:      Maximum indirect jump targets to be recovered.
        :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 bool detect_tail_calls:              Aggressive tail-call optimization detection. This option is only
                                                    respected in make_functions().
        :param bool sp_tracking_track_memory:       Whether or not to track memory writes if tracking the stack pointer.
                                                    This increases the accuracy of stack pointer tracking,
                                                    especially for architectures without a base pointer.
                                                    Only used if detect_tail_calls is enabled.
        :param None or CFGModel model:              The CFGModel instance to write to. A new CFGModel instance will be
                                                    created and registered with the knowledge base if `model` is None.

        :return: None
        """

        self.sort = sort
        self._context_sensitivity_level = context_sensitivity_level

        # Sanity checks
        if context_sensitivity_level < 0:
            raise ValueError(f"Unsupported context sensitivity level {context_sensitivity_level}")

        self._binary = binary if binary is not None else self.project.loader.main_object
        self._force_segment = force_segment
        self._base_state = base_state
        self._detect_tail_calls = detect_tail_calls
        self._low_priority = low_priority
        self._skip_unmapped_addrs = skip_unmapped_addrs

        # Initialization
        self._edge_map = None
        self._loop_back_edges = None
        self._overlapped_loop_headers = None
        self._thumb_addrs = set()
        self._tail_calls = set()

        # Store all the functions analyzed before the set is cleared
        # Used for performance optimization
        self._updated_nonreturning_functions: set[int | SootMethodDescriptor] | None = None

        self._normalize = normalize

        # Flag, whether to track memory writes in stack pointer tracking
        self._sp_tracking_track_memory = sp_tracking_track_memory

        # IndirectJump object that describe all indirect exits found in the binary
        # stores as a map between addresses and IndirectJump objects
        self.indirect_jumps: dict[int, IndirectJump] = {}
        self._indirect_jumps_to_resolve = set()

        # Indirect jump resolvers
        self._indirect_jump_target_limit = indirect_jump_target_limit
        self._resolve_indirect_jumps = resolve_indirect_jumps
        self.timeless_indirect_jump_resolvers = []
        self.indirect_jump_resolvers = []
        if not indirect_jump_resolvers:
            indirect_jump_resolvers = default_indirect_jump_resolvers(self._binary, self.project)
        if self._resolve_indirect_jumps and indirect_jump_resolvers:
            # split them into different groups for the sake of speed
            for ijr in indirect_jump_resolvers:
                if ijr.timeless:
                    self.timeless_indirect_jump_resolvers.append(ijr)
                else:
                    self.indirect_jump_resolvers.append(ijr)

        l.info(
            "Loaded %d indirect jump resolvers (%d timeless, %d generic).",
            len(self.timeless_indirect_jump_resolvers) + len(self.indirect_jump_resolvers),
            len(self.timeless_indirect_jump_resolvers),
            len(self.indirect_jump_resolvers),
        )

        # Get all executable memory regions
        self._exec_mem_regions = self._executable_memory_regions(None, self._force_segment)
        self._exec_mem_region_size = sum((end - start) for start, end in self._exec_mem_regions)

        # initialize UnresolvableJumpTarget and UnresolvableCallTarget SimProcedure
        # but we do not want to hook the same symbol multiple times
        ut_jump_addr = self.project.loader.extern_object.get_pseudo_addr("UnresolvableJumpTarget")
        if not self.project.is_hooked(ut_jump_addr):
            self.project.hook(ut_jump_addr, SIM_PROCEDURES["stubs"]["UnresolvableJumpTarget"]())
        self._unresolvable_jump_target_addr = ut_jump_addr
        ut_call_addr = self.project.loader.extern_object.get_pseudo_addr("UnresolvableCallTarget")
        if not self.project.is_hooked(ut_call_addr):
            self.project.hook(ut_call_addr, SIM_PROCEDURES["stubs"]["UnresolvableCallTarget"]())
        self._unresolvable_call_target_addr = ut_call_addr

        # partially and fully analyzed functions
        # this is implemented as a state machine: jobs (CFGJob instances) of each function are put into
        # _jobs_to_analyze_per_function, which essentially makes the keys of this dict being all partially analyzed
        # functions so far. And then when a function does not have any more job to analyze in the future, it will be
        # put in to _completed_functions.

        # a dict of mapping between function addresses and sets of jobs (include both future jobs and pending jobs)
        # a set is used to speed up the job removal procedure
        self._jobs_to_analyze_per_function = defaultdict(set)
        # addresses of functions that have been completely recovered (i.e. all of its blocks are identified) so far
        self._completed_functions = set()

        # TODO: A segment tree to speed up CFG node lookups
        self._node_lookup_index = None
        self._node_lookup_index_warned = False

        self._function_addresses_from_symbols = self._load_func_addrs_from_symbols()
        self._function_addresses_from_eh_frame = self._load_func_addrs_from_eh_frame()

        # Cache if an object has executable sections or not
        self._object_to_executable_sections = {}
        # Cache if an object has executable segments or not
        self._object_to_executable_segments = {}

        if model is not None:
            self._model = model
        else:
            self._model: CFGModel = self.kb.cfgs.new_model(self.tag)

        # necessary warnings
        regions_not_specified = regions is None and binary is None and not objects
        if regions_not_specified and self.project.loader._auto_load_libs and len(self.project.loader.all_objects) > 3:
            l.warning(
                '"auto_load_libs" is enabled. With libraries loaded in project, CFG will cover libraries, '
                "which may take significantly more time than expected. You may reload the binary with "
                '"auto_load_libs" disabled, or specify "regions" to limit the scope of CFG recovery.'
            )

        if regions is None:
            if self._skip_unmapped_addrs:
                regions = self._executable_memory_regions(objects=objects, force_segment=force_segment)
            else:
                if not objects:
                    objects = self.project.loader.all_objects
                regions = self._executable_memory_regions(objects=objects, force_segment=force_segment)
                if not regions:
                    # fall back to using min and max addresses of all objects
                    regions = [(obj.min_addr, obj.max_addr) for obj in objects]

        for start, end in regions:
            if end < start:
                raise AngrCFGError("Invalid region bounds (end precedes start)")

        # Block factory returns patched state by default, so ensure we are also analyzing the patched state
        if self._base_state is None and self.project.kb.patches.values():
            self._base_state = self.project.kb.patches.patched_entry_state

        if exclude_sparse_regions:
            regions = [r for r in regions if not self._is_region_extremely_sparse(*r, base_state=self._base_state)]

        if skip_specific_regions:
            if base_state is not None:
                l.warning("You specified both base_state and skip_specific_regions. They may conflict with each other.")
            regions = [r for r in regions if not self._should_skip_region(r[0])]

        if not regions and self.project.arch.name != "Soot":
            raise AngrCFGError(
                "Regions are empty, or all regions are skipped. You may want to manually specify regions."
            )

        self._regions_size = sum((end - start) for start, end in regions)
        self._regions: SortedDict = SortedDict(regions)

        l.debug("CFG recovery covers %d regions:", len(self._regions))
        for start, end in self._regions.items():
            l.debug("... %#x - %#x", start, end)

    def __contains__(self, cfg_node):
        return cfg_node in self.graph

    #
    # Properties
    #

    @property
    def _nodes(self):
        return self._model._nodes

    @property
    def _nodes_by_addr(self):
        return self._model._nodes_by_addr

    @property
    def model(self) -> CFGModel:
        """
        Get the CFGModel instance.
        :return:    The CFGModel instance that this analysis currently uses.
        """
        return self._model

    @property
    def normalized(self):
        return self._model.normalized

    @normalized.setter
    def normalized(self, v):
        self._model.normalized = v

    @property
    def context_sensitivity_level(self):
        return self._context_sensitivity_level

    @property
    def functions(self):
        """
        A reference to the FunctionManager in the current knowledge base.

        :return: FunctionManager with all functions
        :rtype: angr.knowledge_plugins.FunctionManager
        """
        return self.kb.functions

    #
    # Methods
    #

    def _initialize_cfg(self):
        """
        Re-create the DiGraph
        """

        self._jobs_to_analyze_per_function = defaultdict(set)
        self._completed_functions = set()

    def _function_completed(self, func_addr: int):
        pass

    def _post_analysis(self):
        if self._normalize:
            if not self.normalized:
                self.normalize()

            # Call normalize() on each function
            for f in self.kb.functions.values():
                if not self.project.is_hooked(f.addr):
                    f.normalize()

        # drop all propagation results that start with "cfg_intermediate"
        self.kb.propagations.discard_by_prefix("cfg_intermediate")

    def make_copy(self, copy_to):
        """
        Copy self attributes to the new object.

        :param CFGBase copy_to: The target to copy to.
        :return: None
        """

        for attr, value in self.__dict__.items():
            if attr.startswith("__") and attr.endswith("__"):
                continue
            setattr(copy_to, attr, value)

    # pylint: disable=no-self-use
    def copy(self):
        raise NotImplementedError

    def output(self):
        raise NotImplementedError

    def generate_index(self):
        """
        Generate an index of all nodes in the graph in order to speed up get_any_node() with anyaddr=True.

        :return: None
        """

        raise NotImplementedError("I'm too lazy to implement it right now")

    def get_loop_back_edges(self):
        return self._loop_back_edges

    @property
    def graph(self) -> networkx.DiGraph[CFGNode]:
        raise NotImplementedError

    def remove_edge(self, block_from, block_to):
        if self.graph is None:
            raise TypeError("self.graph does not exist.")

        if block_from not in self.graph:
            raise ValueError(f"{block_from!r} is not in CFG.")

        if block_to not in self.graph:
            raise ValueError(f"{block_to!r} is not in CFG.")

        if block_to not in self.graph[block_from]:
            raise ValueError(f"Edge {block_from!r}->{block_to!r} does not exist.")

        self.graph.remove_edge(block_from, block_to)

    def _merge_cfgnodes(self, cfgnode_0, cfgnode_1):
        """
        Merge two adjacent CFGNodes into one.

        :param CFGNode cfgnode_0:   The first CFGNode.
        :param CFGNode cfgnode_1:   The second CFGNode.
        :return:                    None
        """

        assert cfgnode_0.addr + cfgnode_0.size == cfgnode_1.addr
        new_node = cfgnode_0.merge(cfgnode_1)

        # Update the graph and the nodes dict accordingly
        self._model.remove_node(cfgnode_1.block_id, cfgnode_1)
        self._model.remove_node(cfgnode_0.block_id, cfgnode_0)

        in_edges = list(self.graph.in_edges(cfgnode_0, data=True))
        out_edges = list(self.graph.out_edges(cfgnode_1, data=True))

        self.graph.remove_node(cfgnode_0)
        self.graph.remove_node(cfgnode_1)

        self.graph.add_node(new_node)
        for src, _, data in in_edges:
            self.graph.add_edge(src, new_node, **data)
        for _, dst, data in out_edges:
            self.graph.add_edge(new_node, dst, **data)

        # Put the new node into node dicts
        self._model.add_node(new_node.block_id, new_node)

    def _to_snippet(self, cfg_node=None, addr=None, size=None, thumb=False, jumpkind=None, base_state=None):
        """
        Convert a CFGNode instance to a CodeNode object.

        :param angr.analyses.CFGNode cfg_node: The CFGNode instance.
        :param int addr: Address of the node. Only used when `cfg_node` is None.
        :param bool thumb: Whether this is in THUMB mode or not. Only used for ARM code and when `cfg_node` is None.
        :param str or None jumpkind: Jumpkind of this node.
        :param SimState or None base_state: The state where BlockNode should be created from.
        :return: A converted CodeNode instance.
        :rtype: CodeNode
        """

        if cfg_node is not None:
            addr = cfg_node.addr
            size = cfg_node.size
            thumb = cfg_node.thumb

        if addr is None:
            raise ValueError("_to_snippet(): Either cfg_node or addr must be provided.")

        if self.project.is_hooked(addr) and jumpkind != "Ijk_NoHook":
            hooker = self.project._sim_procedures[addr]
            size = hooker.kwargs.get("length", 0)
            return HookNode(addr, size, type(hooker))

        if cfg_node is not None:
            return BlockNode(addr, size, thumb=thumb, bytestr=cfg_node.byte_string)  # pylint: disable=no-member
        return self.project.factory.snippet(addr, size=size, jumpkind=jumpkind, thumb=thumb, backup_state=base_state)

    def is_thumb_addr(self, addr):
        return addr in self._thumb_addrs

    def _arm_thumb_filter_jump_successors(self, irsb, successors, get_ins_addr, get_exit_stmt_idx, get_jumpkind):
        """
        Filter successors for THUMB mode basic blocks, and remove those successors that won't be taken normally.

        :param irsb:            The IRSB object.
        :param list successors: A list of successors.
        :param func get_ins_addr: A callable that returns the source instruction address for a successor.
        :param func get_exit_stmt_idx: A callable that returns the source statement ID for a successor.
        :param func get_jumpkind:      A callable that returns the jumpkind of a successor.
        :return: A new list of successors after filtering.
        :rtype: list
        """

        if not successors:
            return []

        if len(successors) == 1 and get_exit_stmt_idx(successors[0]) == DEFAULT_STATEMENT:
            # only have a default exit. no need to filter
            return successors

        if irsb.instruction_addresses and all(
            get_ins_addr(suc) == irsb.instruction_addresses[-1] for suc in successors
        ):
            # check if all exits are produced by the last instruction
            # only takes the following jump kinds: Boring, FakeRet, Call, Syscall, Ret
            allowed_jumpkinds = {"Ijk_Boring", "Ijk_FakeRet", "Ijk_Call", "Ijk_Ret"}
            successors = [
                suc
                for suc in successors
                if get_jumpkind(suc) in allowed_jumpkinds or get_jumpkind(suc).startswith("Ijk_Sys")
            ]
            if len(successors) == 1:
                return successors

        can_produce_exits = set()  # addresses of instructions that can produce exits
        bb = self._lift(irsb.addr, size=irsb.size, thumb=True)

        # step A: filter exits using capstone (since it's faster than re-lifting the entire block to VEX)
        THUMB_BRANCH_INSTRUCTIONS = {
            "beq",
            "bne",
            "bcs",
            "bhs",
            "bcc",
            "blo",
            "bmi",
            "bpl",
            "bvs",
            "bvc",
            "bhi",
            "bls",
            "bge",
            "blt",
            "bgt",
            "ble",
            "cbz",
            "cbnz",
        }
        for cs_insn in bb.capstone.insns:
            if cs_insn.mnemonic.split(".")[0] in THUMB_BRANCH_INSTRUCTIONS:
                can_produce_exits.add(cs_insn.address)

        if all(
            get_ins_addr(suc) in can_produce_exits or get_exit_stmt_idx(suc) == DEFAULT_STATEMENT for suc in successors
        ):
            # nothing will be filtered.
            return successors

        # step B: consider VEX statements
        it_counter = 0
        conc_temps = {}

        for stmt in bb.vex.statements:
            if stmt.tag == "Ist_IMark":
                if it_counter > 0:
                    it_counter -= 1
                    can_produce_exits.add(stmt.addr + stmt.delta)
            elif stmt.tag == "Ist_WrTmp":
                val = stmt.data
                if val.tag == "Iex_Const":
                    conc_temps[stmt.tmp] = val.con.value
            elif stmt.tag == "Ist_Put" and stmt.offset == self.project.arch.registers["itstate"][0]:
                val = stmt.data
                if val.tag == "Iex_RdTmp":
                    if val.tmp in conc_temps:
                        # We found an IT instruction!!
                        # Determine how many instructions are conditional
                        it_counter = 0
                        itstate = conc_temps[val.tmp]
                        while itstate != 0:
                            it_counter += 1
                            itstate >>= 8
                elif val.tag == "Iex_Const":
                    it_counter = 0
                    itstate = val.con.value
                    while itstate != 0:
                        it_counter += 1
                        itstate >>= 8

        if it_counter != 0:
            l.debug("Basic block ends before calculated IT block (%#x)", irsb.addr)

        return [
            suc
            for suc in successors
            if get_ins_addr(suc) in can_produce_exits or get_exit_stmt_idx(suc) == DEFAULT_STATEMENT
        ]

    # Methods for determining scanning scope

    def _inside_regions(self, address: int | None) -> bool:
        """
        Check if the address is inside any existing region.

        :param int address: Address to check.
        :return:            True if the address is within one of the memory regions, False otherwise.
        """

        try:
            start_addr = next(self._regions.irange(maximum=address, reverse=True))
        except StopIteration:
            return False
        else:
            return address < self._regions[start_addr]

    def _get_min_addr(self) -> int | None:
        """
        Get the minimum address out of all regions. We assume self._regions is sorted.

        :return: The minimum address, or None if there is no such address.
        """

        if not self._regions:
            if self.project.arch.name != "Soot":
                l.error("self._regions is empty or not properly set.")
            return None

        return next(self._regions.irange())

    def _next_address_in_regions(self, address: int | None) -> int | None:
        """
        Return the next immediate address that is inside any of the regions.

        :param address: The address to start scanning.
        :return:        The next address that is inside one of the memory regions, or None if there is no such address.
        """

        if self._inside_regions(address):
            return address

        try:
            return next(self._regions.irange(minimum=address, reverse=False))
        except StopIteration:
            return None

    def _is_region_extremely_sparse(self, start: int, end: int, base_state: SimState | None = None) -> bool:
        """
        Check whether the given memory region is extremely sparse, i.e., all bytes are the same value.

        :param start:      The beginning of the region.
        :param end:        The end of the region (exclusive).
        :param base_state: The base state (optional).
        :return:           True if the region is extremely sparse, False otherwise.
        """

        all_bytes = None

        if base_state is not None:
            all_bytes = base_state.memory.load(start, end - start)
            try:
                all_bytes = base_state.solver.eval(all_bytes, cast_to=bytes)
            except SimError:
                all_bytes = None

        size = end - start

        if all_bytes is None:
            # load from the binary
            all_bytes = self._fast_memory_load_bytes(start, size)

        if all_bytes is None:
            # failed to load bytes in this region. it might be because the region is not fully mapped (i.e., there are
            # holes). we assume this region is good for analysis.
            return False

        if len(all_bytes) < size:
            l.warning(
                "_is_region_extremely_sparse: The given region %#x-%#x is not a continuous memory region in the "
                "memory space. Only the first %d bytes (%#x-%#x) are processed.",
                start,
                end,
                len(all_bytes),
                start,
                start + len(all_bytes),
            )

        the_byte_value = None
        for b in all_bytes:
            if the_byte_value is None:
                the_byte_value = b
            else:
                if the_byte_value != b:
                    return False

        return True

    def _should_skip_region(self, region_start):
        """
        Some regions usually do not contain any executable code, but are still marked as executable. We should skip
        those regions by default.

        :param int region_start: Address of the beginning of the region.
        :return:                 True/False
        :rtype:                  bool
        """

        obj = self.project.loader.find_object_containing(region_start, membership_check=False)
        if obj is None:
            return False
        if isinstance(obj, PE):
            section = obj.find_section_containing(region_start)
            if section is None:
                return False
            if section.name in {".textbss"}:
                return True

        return False

    def _executable_memory_regions(self, objects=None, force_segment=False):
        """
        Get all executable memory regions from the binaries

        :param objects: A collection of binary objects to collect regions from. If None, regions from all project
                        binary objects are used.
        :param bool force_segment: Rely on binary segments instead of sections.
        :return: A sorted list of tuples (beginning_address, end_address)
        """

        binaries = self.project.loader.all_objects if objects is None else objects

        memory_regions = []

        for b in binaries:
            if not b.has_memory:
                continue

            if isinstance(b, ELF):
                # If we have sections, we get result from sections
                sections = []
                if not force_segment and b.sections:
                    # Get all executable sections
                    for section in b.sections:
                        if section.is_executable:
                            max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
                            tpl = (section.min_addr, max_mapped_addr)
                            sections.append(tpl)
                    memory_regions += sections

                segments = []
                # Get all executable segments
                for segment in b.segments:
                    if segment.is_executable:
                        max_mapped_addr = segment.min_addr + min(segment.memsize, segment.filesize)
                        tpl = (segment.min_addr, max_mapped_addr)
                        segments.append(tpl)
                if sections and segments:
                    # are there executable segments with no sections inside?
                    for segment in segments:
                        for section in sections:
                            if segment[0] <= section[0] < segment[1]:
                                break
                        else:
                            memory_regions.append(segment)

            elif isinstance(b, (Coff, PE)):
                for section in b.sections:
                    if section.is_executable:
                        max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
                        tpl = (section.min_addr, max_mapped_addr)
                        memory_regions.append(tpl)

            elif isinstance(b, XBE):
                # some XBE files will mark the data sections as executable
                for section in b.sections:
                    if (
                        section.is_executable
                        and not section.is_writable
                        and section.name not in {".data", ".rdata", ".rodata"}
                    ):
                        tpl = (section.min_addr, section.max_addr)
                        memory_regions.append(tpl)

            elif isinstance(b, MachO):
                if b.segments:
                    # Get all executable segments
                    for seg in b.segments:
                        if seg.is_executable:
                            # Take all sections from this segment (MachO style)
                            for section in seg.sections:
                                max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
                                tpl = (section.min_addr, max_mapped_addr)
                                memory_regions.append(tpl)

            elif isinstance(b, (Hex, SRec)):
                if b.regions:
                    for region_addr, region_size in b.regions:
                        memory_regions.append((region_addr, region_addr + region_size))

            elif isinstance(b, Blob):
                # a blob is entirely executable
                tpl = (b.min_addr, b.max_addr + 1)
                memory_regions.append(tpl)

            elif isinstance(b, NamedRegion):
                # NamedRegions have no content! Ignore
                pass

            elif isinstance(b, PEStubs):
                # no code here
                pass

            elif isinstance(b, self._cle_pseudo_objects):
                pass

            elif hasattr(b, "sections") and b.sections:
                sections = []
                # Get all executable sections
                for section in b.sections:
                    if section.is_executable:
                        max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
                        tpl = (section.min_addr, max_mapped_addr)
                        sections.append(tpl)
                memory_regions += sections

            elif hasattr(b, "segments") and b.segments:
                segments = []
                for segment in b.segments:
                    if segment.is_executable:
                        max_mapped_addr = segment.min_addr + min(segment.memsize, segment.filesize)
                        tpl = (segment.min_addr, max_mapped_addr)
                        segments.append(tpl)
                memory_regions += segments

            else:
                l.warning('Unsupported object format "%s". Treat it as an executable.', b.__class__.__name__)

                tpl = (b.min_addr, b.max_addr + 1)
                memory_regions.append(tpl)

        if not memory_regions:
