#######################################################################################
# GEF - Multi-Architecture GDB Enhanced Features for Exploiters & Reverse-Engineers
#
# by  @_hugsy_
#######################################################################################
#
# GEF is a kick-ass set of commands for X86, ARM, MIPS, PowerPC and SPARC to
# make GDB cool again for exploit dev. It is aimed to be used mostly by exploit
# devs and reversers, to provides additional features to GDB using the Python
# API to assist during the process of dynamic analysis.
#
# GEF fully relies on GDB API and other Linux-specific sources of information
# (such as /proc/<pid>). As a consequence, some of the features might not work
# on custom or hardened systems such as GrSec.
#
# Since January 2020, GEF solely support GDB compiled with Python3 and was tested on
#   * x86-32 & x86-64
#   * arm v5,v6,v7
#   * aarch64 (armv8)
#   * mips & mips64
#   * powerpc & powerpc64
#   * sparc & sparc64(v9)
#
# For GEF with Python2 (only) support was moved to the GEF-Legacy
# (https://github.com/hugsy/gef-legacy)
#
# To start: in gdb, type `source /path/to/gef.py`
#
#######################################################################################
#
# gef is distributed under the MIT License (MIT)
# Copyright (c) 2013-2024 crazy rabbidz
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import abc
import argparse
import ast
import atexit
import binascii
import codecs
import collections
import configparser
import ctypes
import enum
import functools
import hashlib
import importlib
import importlib.util
import inspect
import itertools
import os
import pathlib
import platform
import re
import shutil
import socket
import string
import struct
import subprocess
import sys
import tempfile
import time
import traceback
import warnings
from functools import lru_cache
from io import StringIO, TextIOWrapper
from types import ModuleType
from typing import (Any, ByteString, Callable, Generator, Iterable, Iterator,
                    NoReturn, Sequence, Type, TypeVar, cast)
from urllib.request import urlopen


GEF_DEFAULT_BRANCH                     = "main"
GEF_EXTRAS_DEFAULT_BRANCH              = "main"

def http_get(url: str) -> bytes | None:
    """Basic HTTP wrapper for GET request. Return the body of the page if HTTP code is OK,
    otherwise return None."""
    try:
        http = urlopen(url)
        return http.read() if http.getcode() == 200 else None
    except Exception:
        return None


def update_gef(argv: list[str]) -> int:
    """Obsolete. Use `gef.sh`."""
    return -1


try:
    import gdb  # type:ignore
except ImportError:
    if len(sys.argv) >= 2 and sys.argv[1].lower() in ("--update", "--upgrade"):
        print("[-] `update_gef` is obsolete. Use the `gef.sh` script to update gef from the command line.")
    print("[-] gef cannot run as standalone")
    sys.exit(1)


GDB_MIN_VERSION: tuple[int, int]       = (10, 0)
PYTHON_MIN_VERSION: tuple[int, int]    = (3, 10)
PYTHON_VERSION: tuple[int, int]        = sys.version_info[0:2]
GDB_VERSION: tuple[int, int]           = tuple(map(int, re.search(r"(\d+)[^\d]+(\d+)", gdb.VERSION).groups())) # type:ignore

DEFAULT_PAGE_ALIGN_SHIFT               = 12
DEFAULT_PAGE_SIZE                      = 1 << DEFAULT_PAGE_ALIGN_SHIFT

GEF_RC                                 = (pathlib.Path(os.getenv("GEF_RC", "")).absolute()
                                          if os.getenv("GEF_RC")
                                          else pathlib.Path().home() / ".gef.rc")
GEF_TEMP_DIR                           = pathlib.Path(tempfile.gettempdir())/ "gef"
GEF_MAX_STRING_LENGTH                  = 50

LIBC_HEAP_MAIN_ARENA_DEFAULT_NAME      = "main_arena"
ANSI_SPLIT_RE                          = r"(\033\[[\d;]*m)"

LEFT_ARROW                             = " ← "
RIGHT_ARROW                            = " → "
DOWN_ARROW                             = "↳"
HORIZONTAL_LINE                        = "─"
VERTICAL_LINE                          = "│"
CROSS                                  = "✘ "
TICK                                   = "✓ "
BP_GLYPH                               = "●"
GEF_PROMPT                             = "gef➤  "
GEF_PROMPT_ON                          = f"\001\033[1;32m\002{GEF_PROMPT}\001\033[0m\002"
GEF_PROMPT_OFF                         = f"\001\033[1;31m\002{GEF_PROMPT}\001\033[0m\002"

__registered_commands__ : set[Type["GenericCommand"]]                               = set()
__registered_functions__ : set[Type["GenericFunction"]]                             = set()
__registered_architectures__ : dict["Elf.Abi | str", Type["Architecture"]]          = {}
__registered_file_formats__ : set[ Type["FileFormat"] ]                             = set()

GefMemoryMapProvider = Callable[[], Generator["Section", None, None]]


def reset_all_caches() -> None:
    """Free all caches. If an object is cached, it will have a callable attribute `cache_clear`
    which will be invoked to purge the function cache."""

    for mod in dir(sys.modules["__main__"]):
        obj = getattr(sys.modules["__main__"], mod)
        if hasattr(obj, "cache_clear"):
            obj.cache_clear()

    gef.reset_caches()
    return


def reset() -> None:
    global gef

    arch = None
    if "gef" in locals().keys():
        reset_all_caches()
        arch = gef.arch
        del gef

    gef = Gef()
    gef.setup()

    if arch:
        gef.arch = arch
    return


def highlight_text(text: str) -> str:
    """
    Highlight text using `gef.ui.highlight_table` { match -> color } settings.

    If RegEx is enabled it will create a match group around all items in the
    `gef.ui.highlight_table` and wrap the specified color in the `gef.ui.highlight_table`
    around those matches.

    If RegEx is disabled, split by ANSI codes and 'colorify' each match found
    within the specified string.
    """
    global gef

    if not gef.ui.highlight_table:
        return text

    if gef.config["highlight.regex"]:
        for match, color in gef.ui.highlight_table.items():
            text = re.sub("(" + match + ")", Color.colorify("\\1", color), text)
        return text

    ansiSplit = re.split(ANSI_SPLIT_RE, text)

    for match, color in gef.ui.highlight_table.items():
        for index, val in enumerate(ansiSplit):
            found = val.find(match)
            if found > -1:
                ansiSplit[index] = val.replace(match, Color.colorify(match, color))
                break
        text = "".join(ansiSplit)
        ansiSplit = re.split(ANSI_SPLIT_RE, text)

    return "".join(ansiSplit)


def gef_print(*args: str, end="\n", sep=" ", **kwargs: Any) -> None:
    """Wrapper around print(), using string buffering feature."""
    parts = [highlight_text(a) for a in args]
    if buffer_output() and gef.ui.stream_buffer and not is_debug():
        gef.ui.stream_buffer.write(sep.join(parts) + end)
        return

    print(*parts, sep=sep, end=end, **kwargs)
    return


def bufferize(f: Callable) -> Callable:
    """Store the content to be printed for a function in memory, and flush it on function exit."""

    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        global gef

        if gef.ui.stream_buffer:
            return f(*args, **kwargs)

        gef.ui.stream_buffer = StringIO()
        try:
            rv = f(*args, **kwargs)
        finally:
            redirect = gef.config["context.redirect"]
            if redirect.startswith("/dev/pts/"):
                if not gef.ui.redirect_fd:
                    # if the FD has never been open, open it
                    fd = open(redirect, "wt")
                    gef.ui.redirect_fd = fd
                elif redirect != gef.ui.redirect_fd.name:
                    # if the user has changed the redirect setting during runtime, update the state
                    gef.ui.redirect_fd.close()
                    fd = open(redirect, "wt")
                    gef.ui.redirect_fd = fd
                else:
                    # otherwise, keep using it
                    fd = gef.ui.redirect_fd
            else:
                fd = sys.stdout
                gef.ui.redirect_fd = None

            if gef.ui.redirect_fd and fd.closed:
                # if the tty was closed, revert back to stdout
                fd = sys.stdout
                gef.ui.redirect_fd = None
                gef.config["context.redirect"] = ""

            fd.write(gef.ui.stream_buffer.getvalue())
            fd.flush()
            gef.ui.stream_buffer = None
        return rv

    return wrapper


class ValidationError(Exception): pass

#
# Helpers
#

class ObsoleteException(Exception): pass

class AlreadyRegisteredException(Exception): pass

def p8(x: int, s: bool = False, e: "Endianness | None" = None) -> bytes:
    """Pack one byte respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.pack(f"{endian}B", x) if not s else struct.pack(f"{endian:s}b", x)


def p16(x: int, s: bool = False, e: "Endianness | None" = None) -> bytes:
    """Pack one word respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.pack(f"{endian}H", x) if not s else struct.pack(f"{endian:s}h", x)


def p32(x: int, s: bool = False, e: "Endianness | None" = None) -> bytes:
    """Pack one dword respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.pack(f"{endian}I", x) if not s else struct.pack(f"{endian:s}i", x)


def p64(x: int, s: bool = False, e: "Endianness | None" = None) -> bytes:
    """Pack one qword respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.pack(f"{endian}Q", x) if not s else struct.pack(f"{endian:s}q", x)


def u8(x: bytes, s: bool = False, e: "Endianness | None" = None) -> int:
    """Unpack one byte respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.unpack(f"{endian}B", x)[0] if not s else struct.unpack(f"{endian:s}b", x)[0]


def u16(x: bytes, s: bool = False, e: "Endianness | None" = None) -> int:
    """Unpack one word respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.unpack(f"{endian}H", x)[0] if not s else struct.unpack(f"{endian:s}h", x)[0]


def u32(x: bytes, s: bool = False, e: "Endianness | None" = None) -> int:
    """Unpack one dword respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.unpack(f"{endian}I", x)[0] if not s else struct.unpack(f"{endian:s}i", x)[0]


def u64(x: bytes, s: bool = False, e: "Endianness | None" = None) -> int:
    """Unpack one qword respecting the current architecture endianness."""
    endian = e or gef.arch.endianness
    return struct.unpack(f"{endian}Q", x)[0] if not s else struct.unpack(f"{endian:s}q", x)[0]


def is_ascii_string(address: int) -> bool:
    """Helper function to determine if the buffer pointed by `address` is an ASCII string (in GDB)"""
    try:
        return gef.memory.read_ascii_string(address) is not None
    except Exception:
        return False


def is_alive() -> bool:
    """Check if GDB is running."""
    try:
        return gdb.selected_inferior().pid > 0
    except Exception:
        return False


def calling_function() -> str | None:
    """Return the name of the calling function"""
    try:
        stack_info = traceback.extract_stack()[-3]
        return stack_info.name
    except Exception as e:
        dbg(f"traceback failed with {str(e)}")
        return None


#
# Decorators
#
def only_if_gdb_running(f: Callable) -> Callable:
    """Decorator wrapper to check if GDB is running."""
    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if is_alive():
            return f(*args, **kwargs)
        else:
            warn("No debugging session active")

    return wrapper


def only_if_gdb_target_local(f: Callable) -> Callable:
    """Decorator wrapper to check if GDB is running locally (target not remote)."""

    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if not is_remote_debug():
            return f(*args, **kwargs)
        else:
            warn("This command cannot work for remote sessions.")

    return wrapper


def deprecated(solution: str = "") -> Callable:
    """Decorator to add a warning when a command is obsolete and will be removed."""
    def decorator(f: Callable) -> Callable:
        @functools.wraps(f)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            caller = inspect.stack()[1]
            caller_file = pathlib.Path(caller.filename)
            caller_loc = caller.lineno
            msg = f"{caller_file.name}:L{caller_loc} '{f.__name__}' is deprecated and will be removed in a feature release. "
            if not gef:
                print(msg)
            elif gef.config["gef.show_deprecation_warnings"] is True:
                if solution:
                    msg += solution
                warn(msg)
            return f(*args, **kwargs)

        if not wrapper.__doc__:
            wrapper.__doc__ = ""
        wrapper.__doc__ += f"\r\n`{f.__name__}` is **DEPRECATED** and will be removed in the future.\r\n{solution}"
        return wrapper
    return decorator


def experimental_feature(f: Callable) -> Callable:
    """Decorator to add a warning when a feature is experimental."""

    @functools.wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        warn("This feature is under development, expect bugs and unstability...")
        return f(*args, **kwargs)

    return wrapper


def only_if_events_supported(event_type: str) -> Callable:
    """Checks if GDB supports events without crashing."""
    def wrap(f: Callable) -> Callable:
        def wrapped_f(*args: Any, **kwargs: Any) -> Any:
            if getattr(gdb, "events") and getattr(gdb.events, event_type):
                return f(*args, **kwargs)
            warn("GDB events cannot be set")
        return wrapped_f
    return wrap


class classproperty(property):
    """Make the attribute a `classproperty`."""
    def __get__(self, cls, owner):
        assert self.fget
        return classmethod(self.fget).__get__(None, owner)()


def FakeExit(*args: Any, **kwargs: Any) -> NoReturn:
    raise RuntimeWarning


sys.exit = FakeExit


def parse_arguments(required_arguments: dict[str | tuple[str, str], Any],
                    optional_arguments: dict[str | tuple[str, str], Any]) -> Callable:
    """Argument parsing decorator."""

    def int_wrapper(x: str) -> int: return int(x, 0)

    def decorator(f: Callable) -> Callable | None:
        def wrapper(*args: Any, **kwargs: Any) -> Callable:
            parser = argparse.ArgumentParser(prog=args[0]._cmdline_, add_help=True)
            for argname in required_arguments:
                argvalue = required_arguments[argname]
                argtype = type(argvalue)
                if argtype is int:
                    argtype = int_wrapper

                argname_is_list = not isinstance(argname, str)
                assert not argname_is_list and isinstance(argname, str)
                if not argname_is_list and argname.startswith("-"):
                    # optional args
                    if argtype is bool:
                        parser.add_argument(argname, action="store_true" if argvalue else "store_false")
                    else:
                        parser.add_argument(argname, type=argtype, required=True, default=argvalue)
                else:
                    if argtype in (list, tuple):
                        nargs = "*"
                        argtype = type(argvalue[0])
                    else:
                        nargs = "?"
                    # positional args
                    parser.add_argument(argname, type=argtype, default=argvalue, nargs=nargs)

            for argname in optional_arguments:
                if isinstance(argname, str) and not argname.startswith("-"):
                    # refuse positional arguments
                    continue
                argvalue = optional_arguments[argname]
                argtype = type(argvalue)
                if isinstance(argname, str):
                    argname = [argname,]
                if argtype is int:
                    argtype = int_wrapper
                elif argtype is bool:
                    parser.add_argument(*argname, action="store_false" if argvalue else "store_true")
                    continue
                elif argtype in (list, tuple):
                    parser.add_argument(*argname, type=type(argvalue[0]),
                                        default=[], action="append")
                    continue
                parser.add_argument(*argname, type=argtype, default=argvalue)

            parsed_args = parser.parse_args(*(args[1:]))
            kwargs["arguments"] = parsed_args
            return f(*args, **kwargs)
        return wrapper
    return decorator


class Color:
    """Used to colorify terminal output."""

    ### Special chars:
    # \001      ->  Tell the readline library that we start a special sequence
    #               which won't be displayed (takes no column in the output)
    # \002      ->  Tell the readline library that we end a special sequence
    #               started with \001
    # \033      ->  Start an ANSI escape code for displaying colors
    colors = {
        "normal"         : "\001\033[0m\002",
        "gray"           : "\001\033[1;38;5;240m\002",
        "light_gray"     : "\001\033[0;37m\002",
        "red"            : "\001\033[31m\002",
        "green"          : "\001\033[32m\002",
        "yellow"         : "\001\033[33m\002",
        "blue"           : "\001\033[34m\002",
        "pink"           : "\001\033[35m\002",
        "cyan"           : "\001\033[36m\002",
        "bold"           : "\001\033[1m\002",
        "underline"      : "\001\033[4m\002",
        "underline_off"  : "\001\033[24m\002",
        "highlight"      : "\001\033[3m\002",
        "highlight_off"  : "\001\033[23m\002",
        "blink"          : "\001\033[5m\002",
        "blink_off"      : "\001\033[25m\002",
    }

    @staticmethod
    def redify(msg: str) -> str:        return Color.colorify(msg, "red")
    @staticmethod
    def greenify(msg: str) -> str:      return Color.colorify(msg, "green")
    @staticmethod
    def blueify(msg: str) -> str:       return Color.colorify(msg, "blue")
    @staticmethod
    def yellowify(msg: str) -> str:     return Color.colorify(msg, "yellow")
    @staticmethod
    def grayify(msg: str) -> str:       return Color.colorify(msg, "gray")
    @staticmethod
    def light_grayify(msg: str) -> str: return Color.colorify(msg, "light_gray")
    @staticmethod
    def pinkify(msg: str) -> str:       return Color.colorify(msg, "pink")
    @staticmethod
    def cyanify(msg: str) -> str:       return Color.colorify(msg, "cyan")
    @staticmethod
    def boldify(msg: str) -> str:       return Color.colorify(msg, "bold")
    @staticmethod
    def underlinify(msg: str) -> str:   return Color.colorify(msg, "underline")
    @staticmethod
    def highlightify(msg: str) -> str:  return Color.colorify(msg, "highlight")
    @staticmethod
    def blinkify(msg: str) -> str:      return Color.colorify(msg, "blink")

    @staticmethod
    def colorify(text: str, attrs: str) -> str:
        """Color text according to the given attributes."""
        if gef.config["gef.disable_color"] is True: return text

        colors = Color.colors
        msg = [colors[attr] for attr in attrs.split() if attr in colors]
        msg.append(str(text))
        if colors["highlight"] in msg:   msg.append(colors["highlight_off"])
        if colors["underline"] in msg:   msg.append(colors["underline_off"])
        if colors["blink"] in msg:       msg.append(colors["blink_off"])
        msg.append(colors["normal"])
        return "".join(msg)


class Address:
    """GEF representation of memory addresses."""
    def __init__(self, **kwargs: Any) -> None:
        self.value: int = kwargs.get("value", 0)
        self.section: "Section" = kwargs.get("section", None)
        self.info: "Zone" = kwargs.get("info", None)
        return

    def __str__(self) -> str:
        value = format_address(self.value)
        code_color = gef.config["theme.address_code"]
        stack_color = gef.config["theme.address_stack"]
        heap_color = gef.config["theme.address_heap"]
        if self.is_in_text_segment():
            return Color.colorify(value, code_color)
        if self.is_in_heap_segment():
            return Color.colorify(value, heap_color)
        if self.is_in_stack_segment():
            return Color.colorify(value, stack_color)
        return value

    def __int__(self) -> int:
        return self.value

    def is_in_text_segment(self) -> bool:
        return (hasattr(self.info, "name") and ".text" in self.info.name) or \
            (hasattr(self.section, "path") and get_filepath() == self.section.path and self.section.is_executable())

    def is_in_stack_segment(self) -> bool:
        return hasattr(self.section, "path") and "[stack]" == self.section.path

    def is_in_heap_segment(self) -> bool:
        return hasattr(self.section, "path") and "[heap]" == self.section.path

    def dereference(self) -> int | None:
        addr = align_address(int(self.value))
        derefed = dereference(addr)
        return None if derefed is None else int(derefed)

    @property
    def valid(self) -> bool:
        return any(map(lambda x: x.page_start <= self.value < x.page_end, gef.memory.maps))


class Permission(enum.Flag):
    """GEF representation of Linux permission."""
    NONE      = 0
    EXECUTE   = 1
    WRITE     = 2
    READ      = 4
    ALL       = 7

    def __str__(self) -> str:
        perm_str = ""
        perm_str += "r" if self & Permission.READ else "-"
        perm_str += "w" if self & Permission.WRITE else "-"
        perm_str += "x" if self & Permission.EXECUTE else "-"
        return perm_str

    @classmethod
    def from_info_sections(cls, *args: str) -> "Permission":
        perm = cls(0)
        for arg in args:
            if "READONLY" in arg: perm |= Permission.READ
            if "DATA" in arg: perm |= Permission.WRITE
            if "CODE" in arg: perm |= Permission.EXECUTE
        return perm

    @classmethod
    def from_process_maps(cls, perm_str: str) -> "Permission":
        perm = cls(0)
        if perm_str[0] == "r": perm |= Permission.READ
        if perm_str[1] == "w": perm |= Permission.WRITE
        if perm_str[2] == "x": perm |= Permission.EXECUTE
        return perm

    @classmethod
    def from_monitor_info_mem(cls, perm_str: str) -> "Permission":
        perm = cls(0)
        # perm_str[0] shows if this is a user page, which
        # we don't track
        if perm_str[1] == "r": perm |= Permission.READ
        if perm_str[2] == "w": perm |= Permission.WRITE
        return perm

    @classmethod
    def from_info_mem(cls, perm_str: str) -> "Permission":
        perm = cls(0)
        if "r" in perm_str: perm |= Permission.READ
        if "w" in perm_str: perm |= Permission.WRITE
        if "x" in perm_str: perm |= Permission.EXECUTE
        return perm


class Section:
    """GEF representation of process memory sections."""

    def __init__(self, **kwargs: Any) -> None:
        self.page_start: int = kwargs.get("page_start", 0)
        self.page_end: int = kwargs.get("page_end", 0)
        self.offset: int = kwargs.get("offset", 0)
        self.permission: Permission = kwargs.get("permission", Permission(0))
        self.inode: int = kwargs.get("inode", 0)
        self.path: str = kwargs.get("path", "")
        return

    def is_readable(self) -> bool:
        return (self.permission & Permission.READ) != 0

    def is_writable(self) -> bool:
        return (self.permission & Permission.WRITE) != 0

    def is_executable(self) -> bool:
        return (self.permission & Permission.EXECUTE) != 0

    @property
    def size(self) -> int:
        if self.page_end is None or self.page_start is None:
            raise AttributeError
        return self.page_end - self.page_start

    def _search_for_realpath_without_versions(self, path: pathlib.Path) -> str | None:
        """Given a path, search for a file that exists without numeric suffixes."""

        # Match the path string against a regex that will remove a suffix
        # consisting of a dot followed by numbers.
        candidate = re.match(r"^(.*)\.(\d*)$", str(path))
        while candidate:
            candidate = candidate.group(1)
            # If the prefix from the regex match is a file, return that path.
            if pathlib.Path(candidate).is_file():
                return candidate
            # Otherwise, try to match again.
            candidate = re.match(r"^(.*)\.(\d*)$", candidate)
        return None

    def _search_for_realpath(self) -> str | None:
        """This function is a workaround for gdb bug #23764

        path might be wrong for remote sessions, so try a simple search for files
        that aren't found at the path indicated, which should be canonical.
        """

        assert gef.session.remote
        remote_path = pathlib.Path(self.path)
        # First, try the canonical path in the remote session root.
        candidate1 = gef.session.remote.root / remote_path.relative_to(remote_path.anchor)
        if candidate1.is_file():
            return str(candidate1)
        # Also try that path without version suffixes.
        candidate = self._search_for_realpath_without_versions(candidate1)
        if candidate:
            return candidate

        # On some systems, /lib(64) might be a symlink to /usr/lib(64), so try removing
        # the /usr prefix.
        if self.path.startswith("/usr"):
            candidate = gef.session.remote.root / remote_path.relative_to("/usr")
            if candidate.is_file():
                return str(candidate)
            # Also try that path without version suffixes.
            candidate = self._search_for_realpath_without_versions(candidate)
            if candidate:
                return candidate

        # Base case, return the original realpath
        return str(candidate1)

    @property
    def realpath(self) -> str:
        # when in a `gef-remote` session, realpath returns the path to the binary on the local disk, not remote
        if gef.session.remote is None:
            return self.path
        default = self._search_for_realpath()
        if default:
            return default
        raise FileNotFoundError

    def __str__(self) -> str:
        return (f"Section(start={self.page_start:#x}, end={self.page_end:#x}, "
                f"perm={self.permission!s})")

    def __repr__(self) -> str:
        return str(self)

    def __eq__(self, other: "Section") -> bool:
        return other and \
            self.page_start == other.page_start and \
            self.size == other.size and \
            self.permission == other.permission and \
            self.path == other.path

    def overlaps(self, other: "Section") -> bool:
        return max(self.page_start, other.page_start) <= min(self.page_end, other.page_end)

    def contains(self, addr: int) -> bool:
        return addr in range(self.page_start, self.page_end)


Zone = collections.namedtuple("Zone", ["name", "zone_start", "zone_end", "filename"])


class Endianness(enum.Enum):
    LITTLE_ENDIAN     = 1
    BIG_ENDIAN        = 2

    def __str__(self) -> str:
        return "<" if self == Endianness.LITTLE_ENDIAN else ">"

    def __repr__(self) -> str:
        return self.name

    def __int__(self) -> int:
        return self.value


class FileFormatSection:
    misc: Any


class FileFormat:
    name: str
    path: pathlib.Path
    entry_point: int
    checksec: dict[str, bool]
    sections: list[FileFormatSection]

    def __init__(self, path: str | pathlib.Path) -> None:
        raise NotImplementedError

    def __init_subclass__(cls: Type["FileFormat"], **kwargs):
        global __registered_file_formats__
        super().__init_subclass__(**kwargs)
        required_attributes = ("name", "entry_point", "is_valid", "checksec",)
        for attr in required_attributes:
            if not hasattr(cls, attr):
                raise NotImplementedError(f"File format '{cls.__name__}' is invalid: missing attribute '{attr}'")
        __registered_file_formats__.add(cls)
        return

    @classmethod
    def is_valid(cls, _: pathlib.Path) -> bool:
        raise NotImplementedError

    def __str__(self) -> str:
        return f"{self.name}('{self.path.absolute()}', entry @ {self.entry_point:#x})"


class Elf(FileFormat):
    """Basic ELF parsing.
    Ref:
    - http://www.skyfree.org/linux/references/ELF_Format.pdf
    - https://refspecs.linuxfoundation.org/elf/elfspec_ppc.pdf
    - https://refspecs.linuxfoundation.org/ELF/ppc64/PPC-elf64abi.html
    """
    class Class(enum.Enum):
        ELF_32_BITS       = 0x01
        ELF_64_BITS       = 0x02

    ELF_MAGIC         = 0x7f454c46

    class Abi(enum.Enum):
        X86_64            = 0x3e
        X86_32            = 0x03
        ARM               = 0x28
        MIPS              = 0x08
        POWERPC           = 0x14
        POWERPC64         = 0x15
        SPARC             = 0x02
        SPARC64           = 0x2b
        AARCH64           = 0xb7
        RISCV             = 0xf3
        IA64              = 0x32
        M68K              = 0x04

    class Type(enum.Enum):
        ET_RELOC          = 1
        ET_EXEC           = 2
        ET_DYN            = 3
        ET_CORE           = 4

    class OsAbi(enum.Enum):
        SYSTEMV     = 0x00
        HPUX        = 0x01
        NETBSD      = 0x02
        LINUX       = 0x03
        SOLARIS     = 0x06
        AIX         = 0x07
        IRIX        = 0x08
        FREEBSD     = 0x09
        OPENBSD     = 0x0C

    e_magic: int                = ELF_MAGIC
    e_class: "Elf.Class"        = Class.ELF_32_BITS
    e_endianness: Endianness    = Endianness.LITTLE_ENDIAN
    e_eiversion: int
    e_osabi: "Elf.OsAbi"
    e_abiversion: int
    e_pad: bytes
    e_type: "Elf.Type"          = Type.ET_EXEC
    e_machine: Abi              = Abi.X86_32
    e_version: int
    e_entry: int
    e_phoff: int
    e_shoff: int
    e_flags: int
    e_ehsize: int
    e_phentsize: int
    e_phnum: int
    e_shentsize: int
    e_shnum: int
    e_shstrndx: int

    path: pathlib.Path
    phdrs : list["Phdr"]
    shdrs : list["Shdr"]
    name: str = "ELF"

    __checksec : dict[str, bool]

    def __init__(self, path: str | pathlib.Path) -> None:
        """Instantiate an ELF object. A valid ELF must be provided, or an exception will be thrown."""

        if isinstance(path, str):
            self.path = pathlib.Path(path).expanduser()
        elif isinstance(path, pathlib.Path):
            self.path = path
        else:
            raise TypeError

        if not self.path.exists():
            raise FileNotFoundError(f"'{self.path}' not found/readable, most gef features will not work")

        self.__checksec = {}

        with self.path.open("rb") as self.fd:
            # off 0x0
            self.e_magic, e_class, e_endianness, self.e_eiversion = self.read_and_unpack(">IBBB")
            if self.e_magic != Elf.ELF_MAGIC:
                # The ELF is corrupted, GDB won't handle it, no point going further
                raise RuntimeError("Not a valid ELF file (magic)")

            self.e_class, self.e_endianness = Elf.Class(e_class), Endianness(e_endianness)

            if self.e_endianness != gef.arch.endianness:
                warn("Unexpected endianness for architecture")

            endian = self.e_endianness

            # off 0x7
            e_osabi, self.e_abiversion = self.read_and_unpack(f"{endian}BB")
            self.e_osabi = Elf.OsAbi(e_osabi)

            # off 0x9
            self.e_pad = self.read(7)

            # off 0x10
            e_type, e_machine, self.e_version = self.read_and_unpack(f"{endian}HHI")
            self.e_type, self.e_machine = Elf.Type(e_type), Elf.Abi(e_machine)

            # off 0x18
            if self.e_class == Elf.Class.ELF_64_BITS:
                self.e_entry, self.e_phoff, self.e_shoff = self.read_and_unpack(f"{endian}QQQ")
            else:
                self.e_entry, self.e_phoff, self.e_shoff = self.read_and_unpack(f"{endian}III")

            self.e_flags, self.e_ehsize, self.e_phentsize, self.e_phnum = self.read_and_unpack(f"{endian}IHHH")
            self.e_shentsize, self.e_shnum, self.e_shstrndx = self.read_and_unpack(f"{endian}HHH")

            self.phdrs = []
            for i in range(self.e_phnum):
                self.phdrs.append(Phdr(self, self.e_phoff + self.e_phentsize * i))

            self.shdrs = []
            for i in range(self.e_shnum):
                self.shdrs.append(Shdr(self, self.e_shoff + self.e_shentsize * i))
        return

    def read(self, size: int) -> bytes:
        return self.fd.read(size)

    def read_and_unpack(self, fmt: str) -> tuple[Any, ...]:
        size = struct.calcsize(fmt)
        data = self.fd.read(size)
        return struct.unpack(fmt, data)

    def seek(self, off: int) -> None:
        self.fd.seek(off, 0)

    def __str__(self) -> str:
        return f"ELF('{self.path.absolute()}', {self.e_class.name}, {self.e_machine.name})"

    def __repr__(self) -> str:
        return f"ELF('{self.path.absolute()}', {self.e_class.name}, {self.e_machine.name})"

    @property
    def entry_point(self) -> int:
        return self.e_entry

    @classmethod
    def is_valid(cls, path: pathlib.Path) -> bool:
        return u32(path.open("rb").read(4), e = Endianness.BIG_ENDIAN) == Elf.ELF_MAGIC

    @property
    def checksec(self) -> dict[str, bool]:
        """Check the security property of the ELF binary. The following properties are:
        - Canary
        - NX
        - PIE
        - Fortify
        - Partial/Full RelRO.
        Return a dict() with the different keys mentioned above, and the boolean
        associated whether the protection was found."""
        if not self.__checksec:
            def __check_security_property(opt: str, filename: str, pattern: str) -> bool:
                cmd   = [readelf,]
                cmd  += opt.split()
                cmd  += [filename,]
                lines = gef_execute_external(cmd, as_list=True)
                for line in lines:
                    if re.search(pattern, line):
                        return True
                return False

            abspath = str(self.path.absolute())
            readelf = gef.session.constants["readelf"]
            self.__checksec["Canary"] = __check_security_property("-rs", abspath, r"__stack_chk_fail") is True
            has_gnu_stack = __check_security_property("-W -l", abspath, r"GNU_STACK") is True
            if has_gnu_stack:
                self.__checksec["NX"] = __check_security_property("-W -l", abspath, r"GNU_STACK.*RWE") is False
            else:
                self.__checksec["NX"] = False
            self.__checksec["PIE"] = __check_security_property("-h", abspath, r":.*EXEC") is False
            self.__checksec["Fortify"] = __check_security_property("-s", abspath, r"_chk@GLIBC") is True
            self.__checksec["Partial RelRO"] = __check_security_property("-l", abspath, r"GNU_RELRO") is True
            self.__checksec["Full RelRO"] = self.__checksec["Partial RelRO"] and __check_security_property("-d", abspath, r"BIND_NOW") is True
        return self.__checksec

    @classproperty
    @deprecated("use `Elf.Abi.X86_64`")
    def X86_64(cls) -> int: return Elf.Abi.X86_64.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.X86_32`")
    def X86_32(cls) -> int : return Elf.Abi.X86_32.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.ARM`")
    def ARM(cls) -> int : return Elf.Abi.ARM.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.MIPS`")
    def MIPS(cls) -> int : return Elf.Abi.MIPS.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.POWERPC`")
    def POWERPC(cls) -> int : return Elf.Abi.POWERPC.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.POWERPC64`")
    def POWERPC64(cls) -> int : return Elf.Abi.POWERPC64.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.SPARC`")
    def SPARC(cls) -> int : return Elf.Abi.SPARC.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.SPARC64`")
    def SPARC64(cls) -> int : return Elf.Abi.SPARC64.value # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.AARCH64`")
    def AARCH64(cls) -> int : return Elf.Abi.AARCH64.value  # pylint: disable=no-self-argument

    @classproperty
    @deprecated("use `Elf.Abi.RISCV`")
    def RISCV(cls) -> int : return Elf.Abi.RISCV.value # pylint: disable=no-self-argument


class Phdr:
    class Type(enum.IntEnum):
        PT_NULL         = 0
        PT_LOAD         = 1
        PT_DYNAMIC      = 2
        PT_INTERP       = 3
        PT_NOTE         = 4
        PT_SHLIB        = 5
        PT_PHDR         = 6
        PT_TLS          = 7
        PT_LOOS         = 0x60000000
        PT_GNU_EH_FRAME = 0x6474e550
        PT_GNU_STACK    = 0x6474e551
        PT_GNU_RELRO    = 0x6474e552
        PT_GNU_PROPERTY = 0x6474e553
        PT_LOSUNW       = 0x6ffffffa
        PT_SUNWBSS      = 0x6ffffffa
        PT_SUNWSTACK    = 0x6ffffffb
        PT_HISUNW       = PT_HIOS         = 0x6fffffff
        PT_LOPROC       = 0x70000000
        PT_ARM_EIDX     = 0x70000001
        PT_MIPS_ABIFLAGS= 0x70000003
        PT_HIPROC       = 0x7fffffff
        UNKNOWN_PHDR    = 0xffffffff

        @classmethod
        def _missing_(cls, _:int) -> "Phdr.Type":
            return cls.UNKNOWN_PHDR

    class Flags(enum.IntFlag):
        PF_X            = 1
        PF_W            = 2
        PF_R            = 4

    p_type: "Phdr.Type"
    p_flags: "Phdr.Flags"
    p_offset: int
    p_vaddr: int
    p_paddr: int
    p_filesz: int
    p_memsz: int
    p_align: int

    def __init__(self, elf: Elf, off: int) -> None:
        if not elf:
            return
        elf.seek(off)
        self.offset = off
        endian = elf.e_endianness
        if elf.e_class == Elf.Class.ELF_64_BITS:
            p_type, p_flags, self.p_offset = elf.read_and_unpack(f"{endian}IIQ")
            self.p_vaddr, self.p_paddr = elf.read_and_unpack(f"{endian}QQ")
            self.p_filesz, self.p_memsz, self.p_align = elf.read_and_unpack(f"{endian}QQQ")
        else:
            p_type, self.p_offset = elf.read_and_unpack(f"{endian}II")
            self.p_vaddr, self.p_paddr = elf.read_and_unpack(f"{endian}II")
            self.p_filesz, self.p_memsz, p_flags, self.p_align = elf.read_and_unpack(f"{endian}IIII")

        self.p_type, self.p_flags = Phdr.Type(p_type), Phdr.Flags(p_flags)
        return

    def __str__(self) -> str:
        return (f"Phdr(offset={self.offset}, type={self.p_type.name}, flags={self.p_flags.name}, "
	            f"vaddr={self.p_vaddr}, paddr={self.p_paddr}, filesz={self.p_filesz}, "
	            f"memsz={self.p_memsz}, align={self.p_align})")


class Shdr:
    class Type(enum.IntEnum):
        SHT_NULL             = 0
        SHT_PROGBITS         = 1
        SHT_SYMTAB           = 2
        SHT_STRTAB           = 3
        SHT_RELA             = 4
        SHT_HASH             = 5
        SHT_DYNAMIC          = 6
        SHT_NOTE             = 7
        SHT_NOBITS           = 8
        SHT_REL              = 9
        SHT_SHLIB            = 10
        SHT_DYNSYM           = 11
        SHT_NUM	             = 12
        SHT_INIT_ARRAY       = 14
        SHT_FINI_ARRAY       = 15
        SHT_PREINIT_ARRAY    = 16
        SHT_GROUP            = 17
        SHT_SYMTAB_SHNDX     = 18
        SHT_LOOS             = 0x60000000
        SHT_GNU_ATTRIBUTES   = 0x6ffffff5
        SHT_GNU_HASH         = 0x6ffffff6
        SHT_GNU_LIBLIST      = 0x6ffffff7
        SHT_CHECKSUM         = 0x6ffffff8
        SHT_LOSUNW           = 0x6ffffffa
        SHT_SUNW_move        = 0x6ffffffa
        SHT_SUNW_COMDAT      = 0x6ffffffb
        SHT_SUNW_syminfo     = 0x6ffffffc
        SHT_GNU_verdef       = 0x6ffffffd
        SHT_GNU_verneed      = 0x6ffffffe
        SHT_GNU_versym       = 0x6fffffff
        SHT_LOPROC           = 0x70000000
        SHT_ARM_EXIDX        = 0x70000001
        SHT_X86_64_UNWIND    = 0x70000001
        SHT_ARM_ATTRIBUTES   = 0x70000003
        SHT_MIPS_OPTIONS     = 0x7000000d
        DT_MIPS_INTERFACE    = 0x7000002a
        SHT_HIPROC           = 0x7fffffff
        SHT_LOUSER           = 0x80000000
        SHT_HIUSER           = 0x8fffffff
        UNKNOWN_SHDR         = 0xffffffff

        @classmethod
        def _missing_(cls, _:int) -> "Shdr.Type":
            return cls.UNKNOWN_SHDR

    class Flags(enum.IntFlag):
        WRITE            = 1
        ALLOC            = 2
        EXECINSTR        = 4
        MERGE            = 0x10
        STRINGS          = 0x20
        INFO_LINK        = 0x40
        LINK_ORDER       = 0x80
        OS_NONCONFORMING = 0x100
        GROUP            = 0x200
        TLS              = 0x400
        COMPRESSED       = 0x800
        RELA_LIVEPATCH   = 0x00100000
        RO_AFTER_INIT    = 0x00200000
        ORDERED          = 0x40000000
        EXCLUDE          = 0x80000000
        UNKNOWN_FLAG     = 0xffffffff

        @classmethod
        def _missing_(cls, _:int):
            return cls.UNKNOWN_FLAG

    sh_name: int
    sh_type: "Shdr.Type"
    sh_flags: "Shdr.Flags"
    sh_addr: int
    sh_offset: int
    sh_size: int
    sh_link: int
    sh_info: int
    sh_addralign: int
    sh_entsize: int
    name: str

    def __init__(self, elf: Elf | None, off: int) -> None:
        if elf is None:
            return
        elf.seek(off)
        endian = elf.e_endianness
        if elf.e_class == Elf.Class.ELF_64_BITS:
            self.sh_name, sh_type, sh_flags = elf.read_and_unpack(f"{endian}IIQ")
            self.sh_addr, self.sh_offset = elf.read_and_unpack(f"{endian}QQ")
            self.sh_size, self.sh_link, self.sh_info = elf.read_and_unpack(f"{endian}QII")
            self.sh_addralign, self.sh_entsize = elf.read_and_unpack(f"{endian}QQ")
        else:
            self.sh_name, sh_type, sh_flags = elf.read_and_unpack(f"{endian}III")
            self.sh_addr, self.sh_offset = elf.read_and_unpack(f"{endian}II")
            self.sh_size, self.sh_link, self.sh_info = elf.read_and_unpack(f"{endian}III")
            self.sh_addralign, self.sh_entsize = elf.read_and_unpack(f"{endian}II")

        self.sh_type = Shdr.Type(sh_type)
        self.sh_flags = Shdr.Flags(sh_flags)
        stroff = elf.e_shoff + elf.e_shentsize * elf.e_shstrndx

        if elf.e_class == Elf.Class.ELF_64_BITS:
            elf.seek(stroff + 16 + 8)
            offset = u64(elf.read(8))
        else:
            elf.seek(stroff + 12 + 4)
            offset = u32(elf.read(4))
        elf.seek(offset + self.sh_name)
        self.name = ""
        while True:
            c = u8(elf.read(1))
            if c == 0:
                break
            self.name += chr(c)
        return

    def __str__(self) -> str:
        return (f"Shdr(name={self.name}, type={self.sh_type.name}, flags={self.sh_flags.name}, "
	            f"addr={self.sh_addr:#x}, offset={self.sh_offset}, size={self.sh_size}, link={self.sh_link}, "
	            f"info={self.sh_info}, addralign={self.sh_addralign}, entsize={self.sh_entsize})")


class Instruction:
    """GEF representation of a CPU instruction."""

    def __init__(self, address: int, location: str, mnemo: str, operands: list[str], opcodes: bytes) -> None:
        self.address, self.location, self.mnemonic, self.operands, self.opcodes = \
            address, location, mnemo, operands, opcodes
        return

    # Allow formatting an instruction with {:o} to show opcodes.
    # The number of bytes to display can be configured, e.g. {:4o} to only show 4 bytes of the opcodes
    def __format__(self, format_spec: str) -> str:
        if len(format_spec) == 0 or format_spec[-1] != "o":
            return str(self)

        if format_spec == "o":
            opcodes_len = len(self.opcodes)
        else:
            opcodes_len = int(format_spec[:-1])

        opcodes_text = "".join(f"{b:02x}" for b in self.opcodes[:opcodes_len])
        if opcodes_len < len(self.opcodes):
            opcodes_text += "..."
        return (f"{self.address:#10x} {opcodes_text:{opcodes_len * 2 + 3:d}s} {self.location:16} "
                f"{self.mnemonic:6} {', '.join(self.operands)}")

    def __str__(self) -> str:
        return f"{self.address:#10x} {self.location:16} {self.mnemonic:6} {', '.join(self.operands)}"

    def is_valid(self) -> bool:
        return "(bad)" not in self.mnemonic

    def size(self) -> int:
        return len(self.opcodes)

    def next(self) -> "Instruction":
        address = self.address + self.size()
        return gef_get_instruction_at(address)


@deprecated("Use GefHeapManager.find_main_arena_addr()")
def search_for_main_arena() -> int:
    return GefHeapManager.find_main_arena_addr()

class GlibcHeapInfo:
    """Glibc heap_info struct"""

    @staticmethod
    def heap_info_t() -> Type[ctypes.Structure]:
        assert gef.libc.version
        class heap_info_cls(ctypes.Structure):
            pass
        pointer = ctypes.c_uint64 if gef.arch.ptrsize == 8 else ctypes.c_uint32
        pad_size = -5 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1)
        fields = [
            ("ar_ptr", ctypes.POINTER(GlibcArena.malloc_state_t())),
            ("prev", ctypes.POINTER(heap_info_cls)),
            ("size", pointer)
        ]
        if gef.libc.version >= (2, 5):
            fields += [
                ("mprotect_size", pointer)
            ]
            pad_size = -6 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1)
        if gef.libc.version >= (2, 34):
            fields += [
                ("pagesize", pointer)
            ]
            pad_size = -3 * gef.arch.ptrsize & (gef.heap.malloc_alignment - 1)
        fields += [
            ("pad", ctypes.c_uint8*pad_size)
        ]
        heap_info_cls._fields_ = fields
        return heap_info_cls

    def __init__(self, addr: str |  int) -> None:
        self.__address : int = parse_address(f"&{addr}") if isinstance(addr, str) else addr
        self.reset()
        return

    def reset(self):
        self._sizeof = ctypes.sizeof(GlibcHeapInfo.heap_info_t())
        self._data = gef.memory.read(self.__address, ctypes.sizeof(GlibcHeapInfo.heap_info_t()))
        self._heap_info = GlibcHeapInfo.heap_info_t().from_buffer_copy(self._data)
        return

    def __getattr__(self, item: Any) -> Any:
        if item in dir(self._heap_info):
            return ctypes.cast(getattr(self._heap_info, item), ctypes.c_void_p).value
        return getattr(self, item)

    def __abs__(self) -> int:
        return self.__address

    def __int__(self) -> int:
        return self.__address

    @property
    def address(self) -> int:
        return self.__address

    @property
    def sizeof(self) -> int:
        return self._sizeof

    @property
    def addr(self) -> int:
        return int(self)

    @property
    def heap_start(self) -> int:
        # check special case: first heap of non-main-arena
        if self.ar_ptr - self.address < 0x60:
            # the first heap of a non-main-arena starts with a `heap_info`
            # struct, which should fit easily into 0x60 bytes throughout
            # all architectures and glibc versions. If this check succeeds
            # then we are currently looking at such a "first heap"
            arena = GlibcArena(f"*{self.ar_ptr:#x}")
            heap_addr = arena.heap_addr()
            if heap_addr:
                return heap_addr
            else:
                err(f"Cannot find heap address for arena {self.ar_ptr:#x}")
                return 0
        return self.address + self.sizeof

    @property
    def heap_end(self) -> int:
        return self.address + self.size


class GlibcArena:
    """Glibc arena class"""

    NFASTBINS = 10
    NBINS = 128
    NSMALLBINS = 64
    BINMAPSHIFT = 5
    BITSPERMAP = 1 << BINMAPSHIFT
    BINMAPSIZE = NBINS // BITSPERMAP

    @staticmethod
    def malloc_state_t() -> Type[ctypes.Structure]:
        pointer = ctypes.c_uint64 if gef and gef.arch.ptrsize == 8 else ctypes.c_uint32
        fields = [
            ("mutex", ctypes.c_uint32),
            ("flags", ctypes.c_uint32),
        ]
        if gef and gef.libc.version and gef.libc.version >= (2, 27):
            # https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L1684
            fields += [
                ("have_fastchunks", ctypes.c_uint32),
                ("UNUSED_c", ctypes.c_uint32), # padding to align to 0x10
            ]
        fields += [
            ("fastbinsY", GlibcArena.NFASTBINS * pointer),
            ("top", pointer),
            ("last_remainder", pointer),
            ("bins", (GlibcArena.NBINS * 2 - 2) * pointer),
            ("binmap", GlibcArena.BINMAPSIZE * ctypes.c_uint32),
            ("next", pointer),
            ("next_free", pointer)
        ]
        if gef and gef.libc.version and gef.libc.version >= (2, 23):
            # https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L1719
            fields += [
                ("attached_threads", pointer)
            ]
        fields += [
            ("system_mem", pointer),
            ("max_system_mem", pointer),
        ]
        class malloc_state_cls(ctypes.Structure):
            _fields_ = fields
        return malloc_state_cls

    def __init__(self, addr: str) -> None:
        try:
            self.__address : int = parse_address(f"&{addr}")
        except gdb.error:
            self.__address : int = GefHeapManager.find_main_arena_addr()
            # if `find_main_arena_addr` throws `gdb.error` on symbol lookup:
            # it means the session is not started, so just propagate the exception
        self.reset()
        return

    def reset(self):
        self._sizeof = ctypes.sizeof(GlibcArena.malloc_state_t())
        self._data = gef.memory.read(self.__address, ctypes.sizeof(GlibcArena.malloc_state_t()))
        self.__arena = GlibcArena.malloc_state_t().from_buffer_copy(self._data)
        return

    def __abs__(self) -> int:
        return self.__address

    def __int__(self) -> int:
        return self.__address

    def __iter__(self) -> Generator["GlibcArena", None, None]:
        assert gef.heap.main_arena
        main_arena = int(gef.heap.main_arena)

        current_arena = self
        yield current_arena

        while True:
            if current_arena.next == 0 or current_arena.next == main_arena:
                break

            current_arena = GlibcArena(f"*{current_arena.next:#x} ")
            yield current_arena
        return

    def __eq__(self, other: "GlibcArena") -> bool:
        return self.__address == int(other)

    def __str__(self) -> str:
        properties = f"base={self.__address:#x}, top={self.top:#x}, " \
                f"last_remainder={self.last_remainder:#x}, next={self.next:#x}, " \
                f"mem={self.system_mem}, mempeak={self.max_system_mem}"
        return (f"{Color.colorify('Arena', 'blue bold underline')}({properties})")

    def __repr__(self) -> str:
        return f"GlibcArena(address={self.__address:#x}, size={self._sizeof})"

    @property
    def address(self) -> int:
        return self.__address

    @property
    def sizeof(self) -> int:
        return self._sizeof

    @property
    def addr(self) -> int:
        return int(self)

    @property
    def top(self) -> int:
        return self.__arena.top

    @property
    def last_remainder(self) -> int:
        return self.__arena.last_remainder

    @property
    def fastbinsY(self) -> ctypes.Array:
        return self.__arena.fastbinsY

    @property
    def bins(self) -> ctypes.Array:
        return self.__arena.bins

    @property
    def binmap(self) -> ctypes.Array:
        return self.__arena.binmap

    @property
    def next(self) -> int:
        return self.__arena.next

    @property
    def next_free(self) -> int:
        return self.__arena.next_free

    @property
    def attached_threads(self) -> int:
        return self.__arena.attached_threads

    @property
    def system_mem(self) -> int:
        return self.__arena.system_mem

    @property
    def max_system_mem(self) -> int:
        return self.__arena.max_system_mem

    def fastbin(self, i: int) -> "GlibcFastChunk | None":
        """Return head chunk in fastbinsY[i]."""
        addr = int(self.fastbinsY[i])
        if addr == 0:
            return None
        return GlibcFastChunk(addr + 2 * gef.arch.ptrsize)

    def bin(self, i: int) -> tuple[int, int]:
        idx = i * 2
        fd = int(self.bins[idx])
        bk = int(self.bins[idx + 1])
        return fd, bk

    def bin_at(self, i) -> int:
        header_sz = 2 * gef.arch.ptrsize
        offset = ctypes.addressof(self.__arena.bins) - ctypes.addressof(self.__arena)
        return self.__address + offset + (i-1) * 2 * gef.arch.ptrsize + header_sz

    def is_main_arena(self) -> bool:
        return gef.heap.main_arena is not None and int(self) == int(gef.heap.main_arena)

    def heap_addr(self, allow_unaligned: bool = False) -> int | None:
        if self.is_main_arena():
            heap_section = gef.heap.base_address
            if not heap_section:
                return None
            return heap_section
        _addr = int(self) + self.sizeof
        if allow_unaligned:
            return _addr
        return gef.heap.malloc_align_address(_addr)

    def get_heap_info_list(self) -> list[GlibcHeapInfo] | None:
        if self.is_main_arena():
            return None
        heap_addr = self.get_heap_for_ptr(self.top)
        heap_infos = [GlibcHeapInfo(heap_addr)]
        while heap_infos[-1].prev is not None:
            prev = int(heap_infos[-1].prev)
            heap_info = GlibcHeapInfo(prev)
            heap_infos.append(heap_info)
        return heap_infos[::-1]

    @staticmethod
    def get_heap_for_ptr(ptr: int) -> int:
        """Find the corresponding heap for a given pointer (int).
        See https://github.com/bminor/glibc/blob/glibc-2.34/malloc/arena.c#L129"""
        if is_32bit():
            default_mmap_threshold_max = 512 * 1024
        else:  # 64bit
            val = cached_lookup_type("long")
            sz = val.sizeof if val else gef.arch.ptrsize
            default_mmap_threshold_max = 4 * 1024 * 1024 * sz
        heap_max_size = 2 * default_mmap_threshold_max
        return ptr & ~(heap_max_size - 1)

    @staticmethod
    def verify(addr: int) -> bool:
        """Verify that the address matches a possible valid GlibcArena"""
        try:
            test_arena = GlibcArena(f"*{addr:#x}")
            cur_arena = GlibcArena(f"*{test_arena.next:#x}")
            while cur_arena != test_arena:
                if cur_arena == 0:
                    return False
                cur_arena = GlibcArena(f"*{cur_arena.next:#x}")
        except Exception as e:
            dbg(f"GlibcArena.verify({addr:#x}) failed: {str(e)}")
            return False
        return True


class GlibcChunk:
    """Glibc chunk class. The default behavior (from_base=False) is to interpret the data starting at the memory
    address pointed to as the chunk data. Setting from_base to True instead treats that data as the chunk header.
    Ref:  https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/."""

    class ChunkFlags(enum.IntFlag):
        PREV_INUSE = 1
        IS_MMAPPED = 2
        NON_MAIN_ARENA = 4

        def __str__(self) -> str:
            return " | ".join([
                Color.greenify("PREV_INUSE") if self.value & self.PREV_INUSE else Color.redify("PREV_INUSE"),
                Color.greenify("IS_MMAPPED") if self.value & self.IS_MMAPPED else Color.redify("IS_MMAPPED"),
                Color.greenify("NON_MAIN_ARENA") if self.value & self.NON_MAIN_ARENA else Color.redify("NON_MAIN_ARENA")
            ])

    @staticmethod
    def malloc_chunk_t() -> Type[ctypes.Structure]:
        pointer = ctypes.c_uint64 if gef and gef.arch.ptrsize == 8 else ctypes.c_uint32
        class malloc_chunk_cls(ctypes.Structure):
            pass

        malloc_chunk_cls._fields_ = [
            ("prev_size", pointer),
            ("size", pointer),
            ("fd", pointer),
            ("bk", pointer),
            ("fd_nextsize", ctypes.POINTER(malloc_chunk_cls)),
            ("bk_nextsize", ctypes.POINTER(malloc_chunk_cls)),
        ]
        return malloc_chunk_cls

    def __init__(self, addr: int, from_base: bool = False, allow_unaligned: bool = True) -> None:
        ptrsize = gef.arch.ptrsize
        self.data_address = addr + 2 * ptrsize if from_base else addr
        self.base_address = addr if from_base else addr - 2 * ptrsize
        if not allow_unaligned:
            self.data_address = gef.heap.malloc_align_address(self.data_address)
        self.size_addr = int(self.data_address - ptrsize)
        self.prev_size_addr = self.base_address
        self.reset()
        return

    def reset(self):
        self._sizeof = ctypes.sizeof(GlibcChunk.malloc_chunk_t())
        self._data = gef.memory.read(
            self.base_address, ctypes.sizeof(GlibcChunk.malloc_chunk_t()))
        self._chunk = GlibcChunk.malloc_chunk_t().from_buffer_copy(self._data)
        return

    @property
    def prev_size(self) -> int:
        return self._chunk.prev_size

    @property
    def size(self) -> int:
        return self._chunk.size & (~0x07)

    @property
    def flags(self) -> ChunkFlags:
        return GlibcChunk.ChunkFlags(self._chunk.size & 0x07)

    @property
    def fd(self) -> int:
        return self._chunk.fd

    @property
    def bk(self) -> int:
        return self._chunk.bk

    @property
    def fd_nextsize(self) -> int:
        return self._chunk.fd_nextsize

    @property
    def bk_nextsize(self) -> int:
        return self._chunk.bk_nextsize

    def get_usable_size(self) -> int:
        # https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L4537
        ptrsz = gef.arch.ptrsize
        cursz = self.size
        if cursz == 0: return cursz
        if self.has_m_bit(): return cursz - 2 * ptrsz
        return cursz - ptrsz

    @property
    def usable_size(self) -> int:
        return self.get_usable_size()

    def get_prev_chunk_size(self) -> int:
        return gef.memory.read_integer(self.prev_size_addr)

    def __iter__(self) -> Generator["GlibcChunk", None, None]:
        assert gef.heap.main_arena
        current_chunk = self
        top = gef.heap.main_arena.top

        while True:
            yield current_chunk

            if current_chunk.base_address == top:
                break

            if current_chunk.size == 0:
                break

            next_chunk_addr = current_chunk.get_next_chunk_addr()

            if not Address(value=next_chunk_addr).valid:
                break

            next_chunk = current_chunk.get_next_chunk()
            if next_chunk is None:
                break

            current_chunk = next_chunk
        return

    def get_next_chunk(self, allow_unaligned: bool = False) -> "GlibcChunk":
        addr = self.get_next_chunk_addr()
        return GlibcChunk(addr, allow_unaligned=allow_unaligned)

    def get_next_chunk_addr(self) -> int:
        return self.data_address + self.size

    def has_p_bit(self) -> bool:
        return bool(self.flags & GlibcChunk.ChunkFlags.PREV_INUSE)

    def has_m_bit(self) -> bool:
        return bool(self.flags & GlibcChunk.ChunkFlags.IS_MMAPPED)

    def has_n_bit(self) -> bool:
        return bool(self.flags & GlibcChunk.ChunkFlags.NON_MAIN_ARENA)

    def is_used(self) -> bool:
        """Check if the current block is used by:
        - checking the M bit is true
        - or checking that next chunk PREV_INUSE flag is true"""
        if self.has_m_bit():
            return True

        next_chunk = self.get_next_chunk()
        return True if next_chunk.has_p_bit() else False

    def __str_sizes(self) -> str:
        msg = []
        failed = False

        try:
            msg.append(f"Chunk size: {self.size:d} ({self.size:#x})")
            msg.append(f"Usable size: {self.usable_size:d} ({self.usable_size:#x})")
            failed = True
        except gdb.MemoryError:
            msg.append(f"Chunk size: Cannot read at {self.size_addr:#x} (corrupted?)")

        try:
            prev_chunk_sz = self.get_prev_chunk_size()
            msg.append(f"Previous chunk size: {prev_chunk_sz:d} ({prev_chunk_sz:#x})")
            failed = True
        except gdb.MemoryError:
            msg.append(f"Previous chunk size: Cannot read at {self.base_address:#x} (corrupted?)")

        if failed:
            msg.append(str(self.flags))

        return "\n".join(msg)

    def _str_pointers(self) -> str:
        fwd = self.data_address
        bkw = self.data_address + gef.arch.ptrsize

        msg = []
        try:
            msg.append(f"Forward pointer: {self.fd:#x}")
        except gdb.MemoryError:
            msg.append(f"Forward pointer: {fwd:#x} (corrupted?)")

        try:
            msg.append(f"Backward pointer: {self.bk:#x}")
        except gdb.MemoryError:
            msg.append(f"Backward pointer: {bkw:#x} (corrupted?)")

        return "\n".join(msg)

    def __str__(self) -> str:
        return (f"{Color.colorify('Chunk', 'yellow bold underline')}(addr={self.data_address:#x}, "
                f"size={self.size:#x}, flags={self.flags!s})")

    def psprint(self) -> str:
        msg = [
            str(self),
            self.__str_sizes(),
        ]
        if not self.is_used():
            msg.append(f"\n\n{self._str_pointers()}")
        return "\n".join(msg) + "\n"

    def resolve_type(self) -> str:
        ptr_data = gef.memory.read_integer(self.data_address)
        if ptr_data != 0:
            sym = gdb_get_location_from_symbol(ptr_data)
            if sym is not None and "vtable for" in sym[0]:
                return sym[0].replace("vtable for ", "")

        return ""


class GlibcFastChunk(GlibcChunk):

    @property
    def fd(self) -> int:
        assert(gef and gef.libc.version)
        if gef.libc.version < (2, 32):
            return self._chunk.fd
        return self.reveal_ptr(self.data_address)

    def protect_ptr(self, pos: int, pointer: int) -> int:
        """https://elixir.bootlin.com/glibc/glibc-2.32/source/malloc/malloc.c#L339"""
        assert(gef and gef.libc.version)
        if gef.libc.version < (2, 32):
            return pointer
        return (pos >> 12) ^ pointer

    def reveal_ptr(self, pointer: int) -> int:
        """https://elixir.bootlin.com/glibc/glibc-2.32/source/malloc/malloc.c#L341"""
        assert(gef and gef.libc.version)
        if gef.libc.version < (2, 32):
            return pointer
        return gef.memory.read_integer(pointer) ^ (pointer >> 12)

class GlibcTcacheChunk(GlibcFastChunk):
    pass

@deprecated("Use GefLibcManager.find_libc_version()")
def get_libc_version() -> tuple[int, ...]:
    return GefLibcManager.find_libc_version()

def titlify(text: str, color: str | None = None, msg_color: str | None = None) -> str:
    """Print a centered title."""
    _, cols = get_terminal_size()
    nb = (cols - len(text) - 2) // 2
    line_color = color or gef.config["theme.default_title_line"]
    text_color = msg_color or gef.config["theme.default_title_message"]

    msg = [Color.colorify(f"{HORIZONTAL_LINE * nb} ", line_color),
           Color.colorify(text, text_color),
           Color.colorify(f" {HORIZONTAL_LINE * nb}", line_color)]
    return "".join(msg)


def dbg(msg: str) -> None:
    if gef.config["gef.debug"] is True:
        gef_print(f"{Color.colorify('[=]', 'bold cyan')} {msg}")
    return


def err(msg: str) -> None:
    gef_print(f"{Color.colorify('[!]', 'bold red')} {msg}")
    return


def warn(msg: str) -> None:
    gef_print(f"{Color.colorify('[*]', 'bold yellow')} {msg}")
    return


def ok(msg: str) -> None:
    gef_print(f"{Color.colorify('[+]', 'bold green')} {msg}")
    return


def info(msg: str) -> None:
    gef_print(f"{Color.colorify('[+]', 'bold blue')} {msg}")
    return


def push_context_message(level: str, message: str) -> None:
    """Push the message to be displayed the next time the context is invoked."""
    if level not in ("error", "warn", "ok", "info"):
        err(f"Invalid level '{level}', discarding message")
        return
    gef.ui.context_messages.append((level, message))
    return


def show_last_exception() -> None:
    """Display the last Python exception."""

    def _show_code_line(fname: str, idx: int) -> str:
        fpath = pathlib.Path(os.path.expanduser(os.path.expandvars(fname)))
        _data = fpath.read_text().splitlines()
        return _data[idx - 1] if 0 < idx < len(_data) else ""

    gef_print("")
    exc_type, exc_value, exc_traceback = sys.exc_info()
    exc_name = exc_type.__name__ if exc_type else "Unknown"
    gef_print(" Exception raised ".center(80, HORIZONTAL_LINE))
    gef_print(f"{Color.colorify(exc_name, 'bold underline red')}: {exc_value}")
    gef_print(" Detailed stacktrace ".center(80, HORIZONTAL_LINE))

    for fs in traceback.extract_tb(exc_traceback)[::-1]:
        filename, lineno, method, code = fs

        if not code or not code.strip():
            code = _show_code_line(filename, lineno)

        gef_print(f"""{DOWN_ARROW} File "{Color.yellowify(filename)}", line {lineno:d}, in {Color.greenify(method)}()""")
        gef_print(f"   {RIGHT_ARROW}    {code}")

    gef_print(" Version ".center(80, HORIZONTAL_LINE))
    gdb.execute("version full")
    gef_print(" Last 10 GDB commands ".center(80, HORIZONTAL_LINE))
    gdb.execute("show commands")
    gef_print(" Runtime environment ".center(80, HORIZONTAL_LINE))
    gef_print(f"* GDB: {gdb.VERSION}")
    gef_print(f"* Python: {sys.version_info.major:d}.{sys.version_info.minor:d}.{sys.version_info.micro:d} - {sys.version_info.releaselevel}")
    gef_print(f"* OS: {platform.system()} - {platform.release()} ({platform.machine()})")

    try:
        lsb_release = which("lsb_release")
        gdb.execute(f"!'{lsb_release}' -a")
    except FileNotFoundError:
        pass

    gef_print(HORIZONTAL_LINE*80)
    gef_print("")
    return


def gef_pystring(x: bytes) -> str:
    """Returns a sanitized version as string of the bytes list given in input."""
    res = str(x, encoding="utf-8")
    substs = [("\n", "\\n"), ("\r", "\\r"), ("\t", "\\t"), ("\v", "\\v"), ("\b", "\\b"), ]
    for _x, _y in substs: res = res.replace(_x, _y)
    return res


def gef_pybytes(x: str) -> bytes:
    """Returns an immutable bytes list from the string given as input."""
    return bytes(str(x), encoding="utf-8")


@lru_cache()
def which(program: str) -> pathlib.Path:
    """Locate a command on the filesystem."""
    res = shutil.which(program)
    if not res:
        raise FileNotFoundError(f"Missing file `{program}`")
    return pathlib.Path(res)


def style_byte(b: int, color: bool = True) -> str:
    style = {
        "nonprintable": "yellow",
        "printable": "white",
        "00": "gray",
        "0a": "blue",
        "ff": "green",
    }
    sbyte = f"{b:02x}"
    if not color or gef.config["highlight.regex"]:
        return sbyte

    if sbyte in style:
        st = style[sbyte]
    elif chr(b) in (string.ascii_letters + string.digits + string.punctuation + " "):
        st = style.get("printable")
    else:
        st = style.get("nonprintable")
    if st:
        sbyte = Color.colorify(sbyte, st)
    return sbyte


def hexdump(source: ByteString, length: int = 0x10, separator: str = ".", show_raw: bool = False, show_symbol: bool = True, base: int = 0x00) -> str:
    """Return the hexdump of `src` argument.
    @param source *MUST* be of type bytes or bytearray
    @param length is the length of items per line
    @param separator is the default character to use if one byte is not printable
    @param show_raw if True, do not add the line nor the text translation
    @param base is the start address of the block being hexdump
    @return a string with the hexdump"""
    result = []
    align = gef.arch.ptrsize * 2 + 2 if is_alive() else 18

    for i in range(0, len(source), length):
        chunk = bytearray(source[i : i + length])
        hexa = " ".join([style_byte(b, color=not show_raw) for b in chunk])

        if show_raw:
            result.append(hexa)
            continue

        text = "".join([chr(b) if 0x20 <= b < 0x7F else separator for b in chunk])
        if show_symbol:
            sym = gdb_get_location_from_symbol(base + i)
            sym = f"<{sym[0]:s}+{sym[1]:04x}>" if sym else ""
        else:
            sym = ""

        result.append(f"{base + i:#0{align}x} {sym}    {hexa:<{3 * length}}    {text}")
    return "\n".join(result)


def is_debug() -> bool:
    """Check if debug mode is enabled."""
    return gef.config["gef.debug"] is True


def buffer_output() -> bool:
    """Check if output should be buffered until command completion."""
    return gef.config["gef.buffer"] is True


def hide_context() -> bool:
    """Helper function to hide the context pane."""
    gef.ui.context_hidden = True
    return True


def unhide_context() -> bool:
    """Helper function to unhide the context pane."""
    gef.ui.context_hidden = False
    return True


class DisableContextOutputContext:
    def __enter__(self) -> None:
        hide_context()
        return

    def __exit__(self, *exc: Any) -> None:
        unhide_context()
        return


class RedirectOutputContext:
    def __init__(self, to_file: str = "/dev/null") -> None:
        if " " in to_file: raise ValueError("Target filepath cannot contain spaces")
        self.redirection_target_file = to_file
        return

    def __enter__(self) -> None:
        """Redirect all GDB output to `to_file` parameter. By default, `to_file` redirects to `/dev/null`."""
        gdb.execute("set logging overwrite")
        gdb.execute(f"set logging file {self.redirection_target_file}")
        gdb.execute("set logging redirect on")

        if GDB_VERSION >= (12, 0):
            gdb.execute("set logging enabled on")
        else:
            gdb.execute("set logging on")
        return

    def __exit__(self, *exc: Any) -> None:
        """Disable the output redirection, if any."""
        if GDB_VERSION >= (12, 0):
            gdb.execute("set logging enabled off")
        else:
            gdb.execute("set logging off")
        gdb.execute("set logging redirect off")
        return


def enable_redirect_output(to_file: str = "/dev/null") -> None:
    """Redirect all GDB output to `to_file` parameter. By default, `to_file` redirects to `/dev/null`."""
    if " " in to_file: raise ValueError("Target filepath cannot contain spaces")
    gdb.execute("set logging overwrite")
    gdb.execute(f"set logging file {to_file}")
    gdb.execute("set logging redirect on")

    if GDB_VERSION >= (12, 0):
        gdb.execute("set logging enabled on")
    else:
        gdb.execute("set logging on")
    return


def disable_redirect_output() -> None:
    """Disable the output redirection, if any."""
    if GDB_VERSION >= (12, 0):
        gdb.execute("set logging enabled off")
    else:
        gdb.execute("set logging off")
    gdb.execute("set logging redirect off")
    return

@deprecated("use `pathlib.Path(...).mkdir()`")
def gef_makedirs(path: str, mode: int = 0o755) -> pathlib.Path:
    """Recursive mkdir() creation. If successful, return the absolute path of the directory created."""
    fpath = pathlib.Path(path)
    if not fpath.is_dir():
        fpath.mkdir(mode=mode, exist_ok=True, parents=True)
    return fpath.absolute()


@lru_cache()
def gdb_lookup_symbol(sym: str) -> tuple[gdb.Symtab_and_line, ...] | None:
    """Fetch the proper symbol or None if not defined."""
    try:
        res = gdb.decode_line(sym)[1] # pylint: disable=E1136
        return res
    except gdb.error:
        return None

@lru_cache(maxsize=512)
def gdb_get_location_from_symbol(address: int) -> tuple[str, int] | None:
    """Retrieve the location of the `address` argument from the symbol table.
    Return a tuple with the name and offset if found, None otherwise."""
    # this is horrible, ugly hack and shitty perf...
    # find a *clean* way to get gdb.Location from an address
    sym = str(gdb.execute(f"info symbol {address:#x}", to_string=True))
    if sym.startswith("No symbol matches"):
        return None

    # gdb outputs symbols with format: "<symbol_name> + <offset> in section <section_name> of <file>",
    # here, we are only interested in symbol name and offset.
    i = sym.find(" in section ")
    sym = sym[:i].split(" + ")
    name, offset = sym[0], 0
    if len(sym) == 2 and sym[1].isdigit():
        offset = int(sym[1])
    return name, offset


def gdb_disassemble(start_pc: int, **kwargs: int) -> Generator[Instruction, None, None]:
    """Disassemble instructions from `start_pc` (Integer). Accepts the following named
    parameters:
    - `end_pc` (Integer) only instructions whose start address fall in the interval from
      start_pc to end_pc are returned.
    - `count` (Integer) list at most this many disassembled instructions
    If `end_pc` and `count` are not provided, the function will behave as if `count=1`.
    Return an iterator of Instruction objects
    """
    frame = gdb.selected_frame()
    arch = frame.architecture()

    for insn in arch.disassemble(start_pc, **kwargs):
        assert isinstance(insn["addr"], int)
        assert isinstance(insn["length"], int)
        assert isinstance(insn["asm"], str)
        address = insn["addr"]
        asm = insn["asm"].rstrip().split(None, 1)
        if len(asm) > 1:
            mnemo, operands = asm
            operands = operands.split(",")
        else:
            mnemo, operands = asm[0], []

        loc = gdb_get_location_from_symbol(address)
        location = f"<{loc[0]}+{loc[1]:04x}>" if loc else ""

        opcodes = gef.memory.read(insn["addr"], insn["length"])

        yield Instruction(address, location, mnemo, operands, opcodes)


def gdb_get_nth_previous_instruction_address(addr: int, n: int) -> int | None:
    """Return the address (Integer) of the `n`-th instruction before `addr`."""
    # fixed-length ABI
    if gef.arch.instruction_length:
        return max(0, addr - n * gef.arch.instruction_length)

    # variable-length ABI
    cur_insn_addr = gef_current_instruction(addr).address

    # we try to find a good set of previous instructions by "guessing" disassembling backwards
    # the 15 comes from the longest instruction valid size
    for i in range(15 * n, 0, -1):
        try:
            insns = list(gdb_disassemble(addr - i, end_pc=cur_insn_addr))
        except gdb.MemoryError:
            # this is because we can hit an unmapped page trying to read backward
            break

        # 1. check that the disassembled instructions list size can satisfy
        if len(insns) < n + 1:  # we expect the current instruction plus the n before it
            continue

        # If the list of instructions is longer than what we need, then we
        # could get lucky and already have more than what we need, so slice down
        insns = insns[-n - 1 :]

        # 2. check that the sequence ends with the current address
        if insns[-1].address != cur_insn_addr:
            continue

        # 3. check all instructions are valid
        if all(insn.is_valid() for insn in insns):
            return insns[0].address

    return None


@deprecated(solution="Use `gef_instruction_n().address`")
def gdb_get_nth_next_instruction_address(addr: int, n: int) -> int:
    """Return the address of the `n`-th instruction after `addr`. """
    return gef_instruction_n(addr, n).address


def gef_instruction_n(addr: int, n: int) -> Instruction:
    """Return the `n`-th instruction after `addr` as an Instruction object. Note that `n` is treated as
    an positive index, starting from 0 (current instruction address)"""
    return list(gdb_disassemble(addr, count=n + 1))[n]


def gef_get_instruction_at(addr: int) -> Instruction:
    """Return the full Instruction found at the specified address."""
    insn = next(gef_disassemble(addr, 1))
    return insn


def gef_current_instruction(addr: int) -> Instruction:
    """Return the current instruction as an Instruction object."""
    return gef_instruction_n(addr, 0)


def gef_next_instruction(addr: int) -> Instruction:
    """Return the next instruction as an Instruction object."""
    return gef_instruction_n(addr, 1)


def gef_disassemble(addr: int, nb_insn: int, nb_prev: int = 0) -> Generator[Instruction, None, None]:
    """Disassemble `nb_insn` instructions after `addr` and `nb_prev` before `addr`.
    Return an iterator of Instruction objects."""
    nb_insn = max(1, nb_insn)

    if nb_prev:
        try:
            start_addr = gdb_get_nth_previous_instruction_address(addr, nb_prev)
            if start_addr:
                    for insn in gdb_disassemble(start_addr, count=nb_prev):
                        if insn.address == addr: break
                        yield insn
        except gdb.MemoryError:
            # If the address pointing to the previous instruction(s) is not mapped, simply skip them
            pass

    for insn in gdb_disassemble(addr, count=nb_insn):
        yield insn


def gef_execute_external(command: Sequence[str], as_list: bool = False, **kwargs: Any) -> str |  list[str]:
    """Execute an external command and return the result."""
    res = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=kwargs.get("shell", False))
    return [gef_pystring(_) for _ in res.splitlines()] if as_list else gef_pystring(res)


def gef_execute_gdb_script(commands: str) -> None:
    """Execute the parameter `source` as GDB command. This is done by writing `commands` to
    a temporary file, which is then executed via GDB `source` command. The tempfile is then deleted."""
    fd, fname = tempfile.mkstemp(suffix=".gdb", prefix="gef_")
    with os.fdopen(fd, "w") as f:
        f.write(commands)
        f.flush()

    fname = pathlib.Path(fname)
    if fname.is_file() and os.access(fname, os.R_OK):
        gdb.execute(f"source {fname}")
        fname.unlink()
    return


@deprecated("Use Elf(fname).checksec()")
def checksec(filename: str) -> dict[str, bool]:
    return Elf(filename).checksec


@deprecated("Use `gef.arch` instead")
def get_arch() -> str:
    """Return the binary's architecture."""
    if is_alive():
        arch = gdb.selected_frame().architecture()
        return arch.name()

    arch_str = (gdb.execute("show architecture", to_string=True) or "").strip()
    pat = "The target architecture is set automatically (currently "
    if arch_str.startswith(pat):
        arch_str = arch_str[len(pat):].rstrip(")")
        return arch_str

    pat = "The target architecture is assumed to be "
    if arch_str.startswith(pat):
        return arch_str[len(pat):]

    pat = "The target architecture is set to "
    if arch_str.startswith(pat):
        # GDB version >= 10.1
        if '"auto"' in arch_str:
            return re.findall(r"currently \"(.+)\"", arch_str)[0]
        return re.findall(r"\"(.+)\"", arch_str)[0]

    # Unknown, we throw an exception to be safe
    raise RuntimeError(f"Unknown architecture: {arch_str}")


@deprecated("Use `gef.binary.entry_point` instead")
def get_entry_point() -> int | None:
    """Return the binary entry point."""
    return gef.binary.entry_point if gef.binary else None


def is_pie(fpath: str) -> bool:
    return Elf(fpath).checksec["PIE"]


@deprecated("Prefer `gef.arch.endianness == Endianness.BIG_ENDIAN`")
def is_big_endian() -> bool:
    return gef.arch.endianness == Endianness.BIG_ENDIAN


@deprecated("gef.arch.endianness == Endianness.LITTLE_ENDIAN")
def is_little_endian() -> bool:
    return gef.arch.endianness == Endianness.LITTLE_ENDIAN


def flags_to_human(reg_value: int, value_table: dict[int, str]) -> str:
    """Return a human readable string showing the flag states."""
    flags = []
    for bit_index, name in value_table.items():
        flags.append(Color.boldify(name.upper()) if reg_value & (1<<bit_index) != 0 else name.lower())
    return f"[{' '.join(flags)}]"


@lru_cache()
def get_section_base_address(name: str) -> int | None:
    section = process_lookup_path(name)
    return section.page_start if section else None


@lru_cache()
def get_zone_base_address(name: str) -> int | None:
    zone = file_lookup_name_path(name, get_filepath())
    return zone.zone_start if zone else None


#
# Architecture classes
#

@deprecated("Using the decorator `register_architecture` is unnecessary")
def register_architecture(cls: Type["Architecture"]) -> Type["Architecture"]:
    return cls

class ArchitectureBase:
    """Class decorator for declaring an architecture to GEF."""
    aliases: tuple[str | Elf.Abi, ...] = ()

    def __init_subclass__(cls: Type["ArchitectureBase"], **kwargs):
        global __registered_architectures__
        super().__init_subclass__(**kwargs)
        for key in getattr(cls, "aliases"):
            if issubclass(cls, Architecture):
                if isinstance(key, str):
                    __registered_architectures__[key.lower()] = cls
                else:
                    __registered_architectures__[key] = cls
        return


class Architecture(ArchitectureBase):
    """Generic metaclass for the architecture supported by GEF."""

    # Mandatory defined attributes by inheriting classes
    arch: str
    mode: str
    all_registers: tuple[str, ...]
    nop_insn: bytes
    return_register: str
    flag_register: str | None
    instruction_length: int | None
    flags_table: dict[int, str]
    syscall_register: str | None
    syscall_instructions: tuple[str, ...]
    function_parameters: tuple[str, ...]

    # Optionally defined attributes
    _ptrsize: int | None = None
    _endianness: Endianness | None = None
    special_registers: tuple[()] |  tuple[str, ...] = ()
    maps: GefMemoryMapProvider | None = None

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        attributes = ("arch", "mode", "aliases", "all_registers", "nop_insn",
             "return_register", "flag_register", "instruction_length", "flags_table",
             "function_parameters",)
        if not all(map(lambda x: hasattr(cls, x), attributes)):
            raise NotImplementedError

    def __str__(self) -> str:
        return f"Architecture({self.arch}, {self.mode or 'None'}, {repr(self.endianness)})"

    def __repr__(self) -> str:
        return self.__str__()

    @staticmethod
    def supports_gdb_arch(gdb_arch: str) -> bool | None:
        """If implemented by a child `Architecture`, this function dictates if the current class
        supports the loaded ELF file (which can be accessed via `gef.binary`). This callback
        function will override any assumption made by GEF to determine the architecture."""
        return None

    def flag_register_to_human(self, val: int | None = None) -> str:
        raise NotImplementedError

    def is_call(self, insn: Instruction) -> bool:
        raise NotImplementedError

    def is_ret(self, insn: Instruction) -> bool:
        raise NotImplementedError

    def is_conditional_branch(self, insn: Instruction) -> bool:
        raise NotImplementedError

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        raise NotImplementedError

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        raise NotImplementedError

    def canary_address(self) -> int:
        raise NotImplementedError

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        raise NotImplementedError

    def reset_caches(self) -> None:
        self.__get_register_for_selected_frame.cache_clear()
        return

    def __get_register(self, regname: str) -> int:
        """Return a register's value."""
        curframe = gdb.selected_frame()
        key = curframe.pc() ^ int(curframe.read_register('sp')) # todo: check when/if gdb.Frame implements `level()`
        return self.__get_register_for_selected_frame(regname, int(key))

    @lru_cache()
    def __get_register_for_selected_frame(self, regname: str, hash_key: int) -> int:
        # 1st chance
        try:
            return parse_address(regname)
        except gdb.error:
            pass

        # 2nd chance - if an exception, propagate it
        regname = regname.lstrip("$")
        value = gdb.selected_frame().read_register(regname)
        return int(value)

    def register(self, name: str) -> int:
        if not is_alive():
            raise gdb.error("No debugging session active")
        return self.__get_register(name)

    @property
    def registers(self) -> Generator[str, None, None]:
        yield from self.all_registers

    @property
    def pc(self) -> int:
        return self.register("$pc")

    @property
    def sp(self) -> int:
        return self.register("$sp")

    @property
    def fp(self) -> int:
        return self.register("$fp")

    @property
    def ptrsize(self) -> int:
        if not self._ptrsize:
            res = cached_lookup_type("size_t")
            if res is not None:
                self._ptrsize = res.sizeof
            else:
                self._ptrsize = gdb.parse_and_eval("$pc").type.sizeof
        return self._ptrsize

    @property
    def endianness(self) -> Endianness:
        if not self._endianness:
            output = (gdb.execute("show endian", to_string=True) or "").strip().lower()
            if "little endian" in output:
                self._endianness = Endianness.LITTLE_ENDIAN
            elif "big endian" in output:
                self._endianness = Endianness.BIG_ENDIAN
            else:
                raise OSError(f"No valid endianness found in '{output}'")
        return self._endianness

    def get_ith_parameter(self, i: int, in_func: bool = True) -> tuple[str, int | None]:
        """Retrieves the correct parameter used for the current function call."""
        reg = self.function_parameters[i]
        val = self.register(reg)
        key = reg
        return key, val


class GenericArchitecture(Architecture):
    arch = "Generic"
    mode = ""
    aliases = ("GenericArchitecture",)
    all_registers = ()
    instruction_length = 0
    return_register = ""
    function_parameters = ()
    syscall_register = ""
    syscall_instructions = ()
    nop_insn = b""
    flag_register = None
    flags_table = {}


class RISCV(Architecture):
    arch = "RISCV"
    mode = "RISCV"
    aliases = ("RISCV", Elf.Abi.RISCV)
    all_registers = ("$zero", "$ra", "$sp", "$gp", "$tp", "$t0", "$t1",
                     "$t2", "$fp", "$s1", "$a0", "$a1", "$a2", "$a3",
                     "$a4", "$a5", "$a6", "$a7", "$s2", "$s3", "$s4",
                     "$s5", "$s6", "$s7", "$s8", "$s9", "$s10", "$s11",
                     "$t3", "$t4", "$t5", "$t6",)
    return_register = "$a0"
    function_parameters = ("$a0", "$a1", "$a2", "$a3", "$a4", "$a5", "$a6", "$a7")
    syscall_register = "$a7"
    syscall_instructions = ("ecall",)
    nop_insn = b"\x00\x00\x00\x13"
    # RISC-V has no flags registers
    flag_register = None
    flags_table = {}

    @property
    def instruction_length(self) -> int:
        return 4

    def is_call(self, insn: Instruction) -> bool:
        return insn.mnemonic == "call"

    def is_ret(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        if mnemo == "ret":
            return True
        elif (mnemo == "jalr" and insn.operands[0] == "zero" and
              insn.operands[1] == "ra" and insn.operands[2] == 0):
            return True
        elif (mnemo == "c.jalr" and insn.operands[0] == "ra"):
            return True
        return False

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        raise OSError(f"Architecture {cls.arch} not supported yet")

    @property
    def ptrsize(self) -> int:
        if self._ptrsize is not None:
            return self._ptrsize
        if is_alive():
            self._ptrsize = gdb.parse_and_eval("$pc").type.sizeof
            return self._ptrsize
        return 4

    def is_conditional_branch(self, insn: Instruction) -> bool:
        return insn.mnemonic.startswith("b")

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        def long_to_twos_complement(v: int) -> int:
            """Convert a python long value to its two's complement."""
            if is_32bit():
                if v & 0x80000000:
                    return v - 0x100000000
            elif is_64bit():
                if v & 0x8000000000000000:
                    return v - 0x10000000000000000
            else:
                raise OSError("RISC-V: ELF file is not ELF32 or ELF64. This is not currently supported")
            return v

        mnemo = insn.mnemonic
        condition = mnemo[1:]

        if condition.endswith("z"):
            # r2 is the zero register if we are comparing to 0
            rs1 = gef.arch.register(insn.operands[0])
            rs2 = gef.arch.register("$zero")
            condition = condition[:-1]
        elif len(insn.operands) > 2:
            # r2 is populated with the second operand
            rs1 = gef.arch.register(insn.operands[0])
            rs2 = gef.arch.register(insn.operands[1])
        else:
            raise OSError(f"RISC-V: Failed to get rs1 and rs2 for instruction: `{insn}`")

        # If the conditional operation is not unsigned, convert the python long into
        # its two's complement
        if not condition.endswith("u"):
            rs2 = long_to_twos_complement(rs2)
            rs1 = long_to_twos_complement(rs1)
        else:
            condition = condition[:-1]

        if condition == "eq":
            if rs1 == rs2: taken, reason = True, f"{rs1}={rs2}"
            else: taken, reason = False, f"{rs1}!={rs2}"
        elif condition == "ne":
            if rs1 != rs2: taken, reason = True, f"{rs1}!={rs2}"
            else: taken, reason = False, f"{rs1}={rs2}"
        elif condition == "lt":
            if rs1 < rs2: taken, reason = True, f"{rs1}<{rs2}"
            else: taken, reason = False, f"{rs1}>={rs2}"
        elif condition == "le":
            if rs1 <= rs2: taken, reason = True, f"{rs1}<={rs2}"
            else: taken, reason = False, f"{rs1}>{rs2}"
        elif condition == "ge":
            if rs1 >= rs2: taken, reason = True, f"{rs1}>={rs2}"
            else: taken, reason = False, f"{rs1}<{rs2}"
        else:
            raise OSError(f"RISC-V: Conditional instruction `{insn}` not supported yet")

        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        ra = None
        if self.is_ret(insn):
            ra = gef.arch.register("$ra")
        else:
            older = frame.older()
            if older:
                ra = to_unsigned_long(older.pc())
        return ra

    def flag_register_to_human(self, val: int | None = None) -> str:
        # RISC-V has no flags registers, return an empty string to
        # preserve the Architecture API
        return ""

class ARM(Architecture):
    aliases = ("ARM", Elf.Abi.ARM)
    arch = "ARM"
    all_registers = ("$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6",
                     "$r7", "$r8", "$r9", "$r10", "$r11", "$r12", "$sp",
                     "$lr", "$pc", "$cpsr",)

    nop_insn = b"\x00\xf0\x20\xe3" # hint #0
    return_register = "$r0"
    flag_register: str = "$cpsr"
    flags_table = {
        31: "negative",
        30: "zero",
        29: "carry",
        28: "overflow",
        7: "interrupt",
        6: "fast",
        5: "thumb",
    }
    function_parameters = ("$r0", "$r1", "$r2", "$r3")
    syscall_register = "$r7"
    syscall_instructions = ("swi 0x0", "swi NR")
    _endianness = Endianness.LITTLE_ENDIAN

    def is_thumb(self) -> bool:
        """Determine if the machine is currently in THUMB mode."""
        return is_alive() and (self.cpsr & (1 << 5) == 1)

    @property
    def pc(self) -> int | None:
        pc = gef.arch.register("$pc")
        if self.is_thumb():
            pc += 1
        return pc

    @property
    def cpsr(self) -> int:
        if not is_alive():
            raise RuntimeError("Cannot get CPSR, program not started?")
        return gef.arch.register(self.flag_register)

    @property
    def mode(self) -> str:
        return "THUMB" if self.is_thumb() else "ARM"

    @property
    def instruction_length(self) -> int | None:
        # Thumb instructions have variable-length (2 or 4-byte)
        return None if self.is_thumb() else 4

    @property
    def ptrsize(self) -> int:
        return 4

    def is_call(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        call_mnemos = {"bl", "blx"}
        return mnemo in call_mnemos

    def is_ret(self, insn: Instruction) -> bool:
        pop_mnemos = {"pop"}
        branch_mnemos = {"bl", "bx"}
        write_mnemos = {"ldr", "add"}
        if insn.mnemonic in pop_mnemos:
            return insn.operands[-1] == " pc}"
        if insn.mnemonic in branch_mnemos:
            return insn.operands[-1] == "lr"
        if insn.mnemonic in write_mnemos:
            return insn.operands[0] == "pc"
        return False

    def flag_register_to_human(self, val: int | None = None) -> str:
        # https://www.botskool.com/user-pages/tutorials/electronics/arm-7-tutorial-part-1
        if val is None:
            reg = self.flag_register
            val = gef.arch.register(reg)
        return flags_to_human(val, self.flags_table)

    def is_conditional_branch(self, insn: Instruction) -> bool:
        conditions = {"eq", "ne", "lt", "le", "gt", "ge", "vs", "vc", "mi", "pl", "hi", "ls", "cc", "cs"}
        return insn.mnemonic[-2:] in conditions

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo = insn.mnemonic
        # ref: https://www.davespace.co.uk/arm/introduction-to-arm/conditional.html
        flags = dict((self.flags_table[k], k) for k in self.flags_table)
        val = gef.arch.register(self.flag_register)
        taken, reason = False, ""

        if mnemo.endswith("eq"): taken, reason = bool(val&(1<<flags["zero"])), "Z"
        elif mnemo.endswith("ne"): taken, reason = not bool(val&(1<<flags["zero"])), "!Z"
        elif mnemo.endswith("lt"):
            taken, reason = bool(val&(1<<flags["negative"])) != bool(val&(1<<flags["overflow"])), "N!=V"
        elif mnemo.endswith("le"):
            taken, reason = bool(val&(1<<flags["zero"])) or \
                bool(val&(1<<flags["negative"])) != bool(val&(1<<flags["overflow"])), "Z || N!=V"
        elif mnemo.endswith("gt"):
            taken, reason = bool(val&(1<<flags["zero"])) == 0 and \
                bool(val&(1<<flags["negative"])) == bool(val&(1<<flags["overflow"])), "!Z && N==V"
        elif mnemo.endswith("ge"):
            taken, reason = bool(val&(1<<flags["negative"])) == bool(val&(1<<flags["overflow"])), "N==V"
        elif mnemo.endswith("vs"): taken, reason = bool(val&(1<<flags["overflow"])), "V"
        elif mnemo.endswith("vc"): taken, reason = not val&(1<<flags["overflow"]), "!V"
        elif mnemo.endswith("mi"):
            taken, reason = bool(val&(1<<flags["negative"])), "N"
        elif mnemo.endswith("pl"):
            taken, reason = not val&(1<<flags["negative"]), "N==0"
        elif mnemo.endswith("hi"):
            taken, reason = bool(val&(1<<flags["carry"])) and not bool(val&(1<<flags["zero"])), "C && !Z"
        elif mnemo.endswith("ls"):
            taken, reason = not val&(1<<flags["carry"]) or bool(val&(1<<flags["zero"])), "!C || Z"
        elif mnemo.endswith("cs"): taken, reason = bool(val&(1<<flags["carry"])), "C"
        elif mnemo.endswith("cc"): taken, reason = not val&(1<<flags["carry"]), "!C"
        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        if not self.is_ret(insn):
            older = frame.older()
            if not older:
                return None
            return int(older.pc())

        # If it's a pop, we have to peek into the stack, otherwise use lr
        if insn.mnemonic == "pop":
            ra_addr = gef.arch.sp + (len(insn.operands)-1) * self.ptrsize
            if not ra_addr:
                return None
            ra = dereference(ra_addr)
            if ra is None:
                return None
            return to_unsigned_long(ra)
        elif insn.mnemonic == "ldr":
            ra = dereference(gef.arch.sp)
            if ra is None:
                return None
            return to_unsigned_long(ra)
        else:  # 'bx lr' or 'add pc, lr, #0'
            return gef.arch.register("$lr")

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        _NR_mprotect = 125
        insns = [
            "push {r0-r2, r7}",
            f"mov r1, {addr & 0xffff:d}",
            f"mov r0, {(addr & 0xffff0000) >> 16:d}",
            "lsl r0, r0, 16",
            "add r0, r0, r1",
            f"mov r1, {size & 0xffff:d}",
            f"mov r2, {perm.value & 0xff:d}",
            f"mov r7, {_NR_mprotect:d}",
            "svc 0",
            "pop {r0-r2, r7}",
        ]
        return "; ".join(insns)


class AARCH64(ARM):
    aliases = ("ARM64", "AARCH64", Elf.Abi.AARCH64)
    arch = "ARM64"
    mode: str = ""

    all_registers = (
        "$x0", "$x1", "$x2", "$x3", "$x4", "$x5", "$x6", "$x7",
        "$x8", "$x9", "$x10", "$x11", "$x12", "$x13", "$x14","$x15",
        "$x16", "$x17", "$x18", "$x19", "$x20", "$x21", "$x22", "$x23",
        "$x24", "$x25", "$x26", "$x27", "$x28", "$x29", "$x30", "$sp",
        "$pc", "$cpsr", "$fpsr", "$fpcr",)
    return_register = "$x0"
    flag_register = "$cpsr"
    flags_table = {
        31: "negative",
        30: "zero",
        29: "carry",
        28: "overflow",
        7: "interrupt",
        9: "endian",
        6: "fast",
        5: "t32",
        4: "m[4]",
    }
    nop_insn = b"\x1f\x20\x03\xd5" # hint #0
    function_parameters = ("$x0", "$x1", "$x2", "$x3", "$x4", "$x5", "$x6", "$x7",)
    syscall_register = "$x8"
    syscall_instructions = ("svc $x0",)

    def is_call(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        call_mnemos = {"bl", "blr"}
        return mnemo in call_mnemos

    def flag_register_to_human(self, val: int | None = None) -> str:
        # https://events.linuxfoundation.org/sites/events/files/slides/KoreaLinuxForum-2014.pdf
        reg = self.flag_register
        if not val:
            val = gef.arch.register(reg)
        return flags_to_human(val, self.flags_table)

    def is_aarch32(self) -> bool:
        """Determine if the CPU is currently in AARCH32 mode from runtime."""
        return (self.cpsr & (1 << 4) != 0) and (self.cpsr & (1 << 5) == 0)

    def is_thumb32(self) -> bool:
        """Determine if the CPU is currently in THUMB32 mode from runtime."""
        return (self.cpsr & (1 << 4) == 1) and (self.cpsr & (1 << 5) == 1)

    @property
    def ptrsize(self) -> int:
        """Determine the size of pointer from the current CPU mode"""
        if not is_alive():
            return 8
        if self.is_aarch32():
            return 4
        if self.is_thumb32():
            return 2
        return 8

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        _NR_mprotect = 226
        insns = [
            "str x8, [sp, -16]!",
            "str x0, [sp, -16]!",
            "str x1, [sp, -16]!",
            "str x2, [sp, -16]!",
            f"mov x8, {_NR_mprotect:d}",
            f"movz x0, {addr & 0xFFFF:#x}",
            f"movk x0, {(addr >> 16) & 0xFFFF:#x}, lsl 16",
            f"movk x0, {(addr >> 32) & 0xFFFF:#x}, lsl 32",
            f"movk x0, {(addr >> 48) & 0xFFFF:#x}, lsl 48",
            f"movz x1, {size & 0xFFFF:#x}",
            f"movk x1, {(size >> 16) & 0xFFFF:#x}, lsl 16",
            f"mov x2, {perm.value:d}",
            "svc 0",
            "ldr x2, [sp], 16",
            "ldr x1, [sp], 16",
            "ldr x0, [sp], 16",
            "ldr x8, [sp], 16",
        ]
        return "; ".join(insns)

    def is_conditional_branch(self, insn: Instruction) -> bool:
        # https://www.element14.com/community/servlet/JiveServlet/previewBody/41836-102-1-229511/ARM.Reference_Manual.pdf
        # sect. 5.1.1
        mnemo = insn.mnemonic
        branch_mnemos = {"cbnz", "cbz", "tbnz", "tbz"}
        return mnemo.startswith("b.") or mnemo in branch_mnemos

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo, operands = insn.mnemonic, insn.operands
        taken, reason = False, ""

        if mnemo in {"cbnz", "cbz", "tbnz", "tbz"}:
            reg = f"${operands[0]}"
            op = gef.arch.register(reg)
            if mnemo == "cbnz":
                if op!=0: taken, reason = True, f"{reg}!=0"
                else: taken, reason = False, f"{reg}==0"
            elif mnemo == "cbz":
                if op == 0: taken, reason = True, f"{reg}==0"
                else: taken, reason = False, f"{reg}!=0"
            elif mnemo == "tbnz":
                # operands[1] has one or more white spaces in front, then a #, then the number
                # so we need to eliminate them
                i = int(operands[1].strip().lstrip("#"))
                if (op & 1<<i) != 0: taken, reason = True, f"{reg}&1<<{i}!=0"
                else: taken, reason = False, f"{reg}&1<<{i}==0"
            elif mnemo == "tbz":
                # operands[1] has one or more white spaces in front, then a #, then the number
                # so we need to eliminate them
                i = int(operands[1].strip().lstrip("#"))
                if (op & 1<<i) == 0: taken, reason = True, f"{reg}&1<<{i}==0"
                else: taken, reason = False, f"{reg}&1<<{i}!=0"

        if not reason:
            taken, reason = super().is_branch_taken(insn)
        return taken, reason


class X86(Architecture):
    aliases = ("X86", Elf.Abi.X86_32)
    arch = "X86"
    mode = "32"

    nop_insn = b"\x90"
    flag_register: str = "$eflags"
    special_registers = ("$cs", "$ss", "$ds", "$es", "$fs", "$gs", )
    gpr_registers = ("$eax", "$ebx", "$ecx", "$edx", "$esp", "$ebp", "$esi", "$edi", "$eip", )
    all_registers = gpr_registers + ( flag_register,) + special_registers
    instruction_length = None
    return_register = "$eax"
    function_parameters = ("$esp", )
    flags_table = {
        6: "zero",
        0: "carry",
        2: "parity",
        4: "adjust",
        7: "sign",
        8: "trap",
        9: "interrupt",
        10: "direction",
        11: "overflow",
        16: "resume",
        17: "virtualx86",
        21: "identification",
    }
    syscall_register = "$eax"
    syscall_instructions = ("sysenter", "int 0x80")
    _ptrsize = 4
    _endianness = Endianness.LITTLE_ENDIAN

    def flag_register_to_human(self, val: int | None = None) -> str:
        reg = self.flag_register
        if val is None:
            val = gef.arch.register(reg)
        return flags_to_human(val, self.flags_table)

    def is_call(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        call_mnemos = {"call", "callq"}
        return mnemo in call_mnemos

    def is_ret(self, insn: Instruction) -> bool:
        return insn.mnemonic == "ret"

    def is_conditional_branch(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        branch_mnemos = {
            "ja", "jnbe", "jae", "jnb", "jnc", "jb", "jc", "jnae", "jbe", "jna",
            "jcxz", "jecxz", "jrcxz", "je", "jz", "jg", "jnle", "jge", "jnl",
            "jl", "jnge", "jle", "jng", "jne", "jnz", "jno", "jnp", "jpo", "jns",
            "jo", "jp", "jpe", "js"
        }
        return mnemo in branch_mnemos

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo = insn.mnemonic
        # all kudos to fG! (https://github.com/gdbinit/Gdbinit/blob/master/gdbinit#L1654)
        flags = dict((self.flags_table[k], k) for k in self.flags_table)
        val = gef.arch.register(self.flag_register)

        taken, reason = False, ""

        if mnemo in ("ja", "jnbe"):
            taken, reason = not val&(1<<flags["carry"]) and not bool(val&(1<<flags["zero"])), "!C && !Z"
        elif mnemo in ("jae", "jnb", "jnc"):
            taken, reason = not val&(1<<flags["carry"]), "!C"
        elif mnemo in ("jb", "jc", "jnae"):
            taken, reason = bool(val&(1<<flags["carry"])) != 0, "C"
        elif mnemo in ("jbe", "jna"):
            taken, reason = bool(val&(1<<flags["carry"])) or bool(val&(1<<flags["zero"])), "C || Z"
        elif mnemo in ("jcxz", "jecxz", "jrcxz"):
            cx = gef.arch.register("$rcx") if is_x86_64() else gef.arch.register("$ecx")
            taken, reason = cx == 0, "!$CX"
        elif mnemo in ("je", "jz"):
            taken, reason = bool(val&(1<<flags["zero"])), "Z"
        elif mnemo in ("jne", "jnz"):
            taken, reason = not bool(val&(1<<flags["zero"])), "!Z"
        elif mnemo in ("jg", "jnle"):
            taken, reason = not bool(val&(1<<flags["zero"])) and bool(val&(1<<flags["overflow"])) == bool(val&(1<<flags["sign"])), "!Z && S==O"
        elif mnemo in ("jge", "jnl"):
            taken, reason = bool(val&(1<<flags["sign"])) == bool(val&(1<<flags["overflow"])), "S==O"
        elif mnemo in ("jl", "jnge"):
            taken, reason = bool(val&(1<<flags["overflow"]) != val&(1<<flags["sign"])), "S!=O"
        elif mnemo in ("jle", "jng"):
            taken, reason = bool(val&(1<<flags["zero"])) or bool(val&(1<<flags["overflow"])) != bool(val&(1<<flags["sign"])), "Z || S!=O"
        elif mnemo in ("jo",):
            taken, reason = bool(val&(1<<flags["overflow"])), "O"
        elif mnemo in ("jno",):
            taken, reason = not val&(1<<flags["overflow"]), "!O"
        elif mnemo in ("jpe", "jp"):
            taken, reason = bool(val&(1<<flags["parity"])), "P"
        elif mnemo in ("jnp", "jpo"):
            taken, reason = not val&(1<<flags["parity"]), "!P"
        elif mnemo in ("js",):
            taken, reason = bool(val&(1<<flags["sign"])) != 0, "S"
        elif mnemo in ("jns",):
            taken, reason = not val&(1<<flags["sign"]), "!S"
        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        ra = None
        if self.is_ret(insn):
            ra = dereference(gef.arch.sp)
        else:
            older = frame.older()
            if older:
                ra = older.pc()
        if ra is None:
            return None
        return to_unsigned_long(ra)

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        _NR_mprotect = 125
        insns = [
            "pushad",
            "pushfd",
            f"mov eax, {_NR_mprotect:d}",
            f"mov ebx, {addr:d}",
            f"mov ecx, {size:d}",
            f"mov edx, {perm.value:d}",
            "int 0x80",
            "popfd",
            "popad",
        ]
        return "; ".join(insns)

    def get_ith_parameter(self, i: int, in_func: bool = True) -> tuple[str, int | None]:
        if in_func:
            i += 1  # Account for RA being at the top of the stack
        sp = gef.arch.sp
        sz = gef.arch.ptrsize
        loc = sp + (i * sz)
        val = gef.memory.read_integer(loc)
        key = f"[sp + {i * sz:#x}]"
        return key, val


class X86_64(X86):
    aliases = ("X86_64", Elf.Abi.X86_64, "i386:x86-64")
    arch = "X86"
    mode = "64"

    gpr_registers = (
        "$rax", "$rbx", "$rcx", "$rdx", "$rsp", "$rbp", "$rsi", "$rdi", "$rip",
        "$r8", "$r9", "$r10", "$r11", "$r12", "$r13", "$r14", "$r15", )
    all_registers = gpr_registers + ( X86.flag_register, ) + X86.special_registers
    return_register = "$rax"
    function_parameters = ["$rdi", "$rsi", "$rdx", "$rcx", "$r8", "$r9"]
    syscall_register = "$rax"
    syscall_instructions = ["syscall"]
    # We don't want to inherit x86's stack based param getter
    get_ith_parameter = Architecture.get_ith_parameter
    _ptrsize = 8

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        _NR_mprotect = 10
        insns = [
            "pushfq",
            "push rax",
            "push rdi",
            "push rsi",
            "push rdx",
            "push rcx",
            "push r11",
            f"mov rax, {_NR_mprotect:d}",
            f"mov rdi, {addr:d}",
            f"mov rsi, {size:d}",
            f"mov rdx, {perm.value:d}",
            "syscall",
            "pop r11",
            "pop rcx",
            "pop rdx",
            "pop rsi",
            "pop rdi",
            "pop rax",
            "popfq",
        ]
        return "; ".join(insns)

    def canary_address(self) -> int:
        return self.register("fs_base") + 0x28

class PowerPC(Architecture):
    aliases = ("PowerPC", Elf.Abi.POWERPC, "PPC")
    arch = "PPC"
    mode = "PPC32"

    all_registers = (
        "$r0", "$r1", "$r2", "$r3", "$r4", "$r5", "$r6", "$r7",
        "$r8", "$r9", "$r10", "$r11", "$r12", "$r13", "$r14", "$r15",
        "$r16", "$r17", "$r18", "$r19", "$r20", "$r21", "$r22", "$r23",
        "$r24", "$r25", "$r26", "$r27", "$r28", "$r29", "$r30", "$r31",
        "$pc", "$msr", "$cr", "$lr", "$ctr", "$xer", "$trap",)
    instruction_length = 4
    nop_insn = b"\x60\x00\x00\x00" # https://developer.ibm.com/articles/l-ppc/
    return_register = "$r0"
    flag_register: str = "$cr"
    flags_table = {
        3: "negative[0]",
        2: "positive[0]",
        1: "equal[0]",
        0: "overflow[0]",
        # cr7
        31: "less[7]",
        30: "greater[7]",
        29: "equal[7]",
        28: "overflow[7]",
    }
    function_parameters = ("$i0", "$i1", "$i2", "$i3", "$i4", "$i5")
    syscall_register = "$r0"
    syscall_instructions = ("sc",)
    _ptrsize = 4


    def flag_register_to_human(self, val: int | None = None) -> str:
        # https://www.cebix.net/downloads/bebox/pem32b.pdf (% 2.1.3)
        if val is None:
            reg = self.flag_register
            val = gef.arch.register(reg)
        return flags_to_human(val, self.flags_table)

    def is_call(self, insn: Instruction) -> bool:
        return False

    def is_ret(self, insn: Instruction) -> bool:
        return insn.mnemonic == "blr"

    def is_conditional_branch(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        branch_mnemos = {"beq", "bne", "ble", "blt", "bgt", "bge"}
        return mnemo in branch_mnemos

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo = insn.mnemonic
        flags = dict((self.flags_table[k], k) for k in self.flags_table)
        val = gef.arch.register(self.flag_register)
        taken, reason = False, ""
        if mnemo == "beq":   taken, reason = bool(val&(1<<flags["equal[7]"])), "E"
        elif mnemo == "bne": taken, reason = val&(1<<flags["equal[7]"]) == 0, "!E"
        elif mnemo == "ble": taken, reason = bool(val&(1<<flags["equal[7]"])) or bool(val&(1<<flags["less[7]"])), "E || L"
        elif mnemo == "blt": taken, reason = bool(val&(1<<flags["less[7]"])), "L"
        elif mnemo == "bge": taken, reason = bool(val&(1<<flags["equal[7]"])) or bool(val&(1<<flags["greater[7]"])), "E || G"
        elif mnemo == "bgt": taken, reason = bool(val&(1<<flags["greater[7]"])), "G"
        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        ra = None
        if self.is_ret(insn):
            ra = gef.arch.register("$lr")
        else:
            older = frame.older()
            if older:
                ra = to_unsigned_long(older.pc())
        return ra

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        # Ref: https://developer.ibm.com/articles/l-ppc/
        _NR_mprotect = 125
        insns = [
            "addi 1, 1, -16",  # 1 = r1 = sp
            "stw 0, 0(1)",
            "stw 3, 4(1)",  # r0 = syscall_code | r3, r4, r5 = args
            "stw 4, 8(1)",
            "stw 5, 12(1)",
            f"li 0, {_NR_mprotect:d}",
            f"lis 3, {addr:#x}@h",
            f"ori 3, 3, {addr:#x}@l",
            f"lis 4, {size:#x}@h",
            f"ori 4, 4, {size:#x}@l",
            f"li 5, {perm.value:d}",
            "sc",
            "lwz 0, 0(1)",
            "lwz 3, 4(1)",
            "lwz 4, 8(1)",
            "lwz 5, 12(1)",
            "addi 1, 1, 16",
        ]
        return ";".join(insns)


class PowerPC64(PowerPC):
    aliases = ("PowerPC64", Elf.Abi.POWERPC64, "PPC64")
    arch = "PPC"
    mode = "PPC64"
    _ptrsize = 8


class SPARC(Architecture):
    """ Refs:
    - https://www.cse.scu.edu/~atkinson/teaching/sp05/259/sparc.pdf
    """
    aliases = ("SPARC", Elf.Abi.SPARC)
    arch = "SPARC"
    mode = ""

    all_registers = (
        "$g0", "$g1", "$g2", "$g3", "$g4", "$g5", "$g6", "$g7",
        "$o0", "$o1", "$o2", "$o3", "$o4", "$o5", "$o7",
        "$l0", "$l1", "$l2", "$l3", "$l4", "$l5", "$l6", "$l7",
        "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i7",
        "$pc", "$npc", "$sp ", "$fp ", "$psr",)
    instruction_length = 4
    nop_insn = b"\x00\x00\x00\x00"  # sethi 0, %g0
    return_register = "$i0"
    flag_register: str = "$psr"
    flags_table = {
        23: "negative",
        22: "zero",
        21: "overflow",
        20: "carry",
        7: "supervisor",
        5: "trap",
    }
    function_parameters = ("$o0 ", "$o1 ", "$o2 ", "$o3 ", "$o4 ", "$o5 ", "$o7 ",)
    syscall_register = "%g1"
    syscall_instructions = ("t 0x10",)

    def flag_register_to_human(self, val: int | None = None) -> str:
        # https://www.gaisler.com/doc/sparcv8.pdf
        reg = self.flag_register
        if val is None:
            val = gef.arch.register(reg)
        return flags_to_human(val, self.flags_table)

    def is_call(self, insn: Instruction) -> bool:
        return False

    def is_ret(self, insn: Instruction) -> bool:
        return insn.mnemonic == "ret"

    def is_conditional_branch(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        # http://moss.csc.ncsu.edu/~mueller/codeopt/codeopt00/notes/condbranch.html
        branch_mnemos = {
            "be", "bne", "bg", "bge", "bgeu", "bgu", "bl", "ble", "blu", "bleu",
            "bneg", "bpos", "bvs", "bvc", "bcs", "bcc"
        }
        return mnemo in branch_mnemos

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo = insn.mnemonic
        flags = dict((self.flags_table[k], k) for k in self.flags_table)
        val = gef.arch.register(self.flag_register)
        taken, reason = False, ""

        if mnemo == "be": taken, reason = bool(val&(1<<flags["zero"])), "Z"
        elif mnemo == "bne": taken, reason = bool(val&(1<<flags["zero"])) == 0, "!Z"
        elif mnemo == "bg": taken, reason = bool(val&(1<<flags["zero"])) == 0 and (val&(1<<flags["negative"]) == 0 or val&(1<<flags["overflow"]) == 0), "!Z && (!N || !O)"
        elif mnemo == "bge": taken, reason = val&(1<<flags["negative"]) == 0 or val&(1<<flags["overflow"]) == 0, "!N || !O"
        elif mnemo == "bgu": taken, reason = val&(1<<flags["carry"]) == 0 and val&(1<<flags["zero"]) == 0, "!C && !Z"
        elif mnemo == "bgeu": taken, reason = val&(1<<flags["carry"]) == 0, "!C"
        elif mnemo == "bl": taken, reason = bool(val&(1<<flags["negative"])) and bool(val&(1<<flags["overflow"])), "N && O"
        elif mnemo == "blu": taken, reason = bool(val&(1<<flags["carry"])), "C"
        elif mnemo == "ble": taken, reason = bool(val&(1<<flags["zero"])) or bool(val&(1<<flags["negative"]) or val&(1<<flags["overflow"])), "Z || (N || O)"
        elif mnemo == "bleu": taken, reason = bool(val&(1<<flags["carry"])) or bool(val&(1<<flags["zero"])), "C || Z"
        elif mnemo == "bneg": taken, reason = bool(val&(1<<flags["negative"])), "N"
        elif mnemo == "bpos": taken, reason = val&(1<<flags["negative"]) == 0, "!N"
        elif mnemo == "bvs": taken, reason = bool(val&(1<<flags["overflow"])), "O"
        elif mnemo == "bvc": taken, reason = val&(1<<flags["overflow"]) == 0, "!O"
        elif mnemo == "bcs": taken, reason = bool(val&(1<<flags["carry"])), "C"
        elif mnemo == "bcc": taken, reason = val&(1<<flags["carry"]) == 0, "!C"
        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        ra = None
        if self.is_ret(insn):
            ra = gef.arch.register("$o7")
        else:
            older = frame.older()
            if older:
                ra = to_unsigned_long(older.pc())
        return ra

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        hi = (addr & 0xffff0000) >> 16
        lo = (addr & 0x0000ffff)
        _NR_mprotect = 125
        insns = ["add %sp, -16, %sp",
                 "st %g1, [ %sp ]", "st %o0, [ %sp + 4 ]",
                 "st %o1, [ %sp + 8 ]", "st %o2, [ %sp + 12 ]",
                 f"sethi  %hi({hi}), %o0",
                 f"or  %o0, {lo}, %o0",
                 "clr  %o1",
                 "clr  %o2",
                 f"mov  {_NR_mprotect}, %g1",
                 "t 0x10",
                 "ld [ %sp ], %g1", "ld [ %sp + 4 ], %o0",
                 "ld [ %sp + 8 ], %o1", "ld [ %sp + 12 ], %o2",
                 "add %sp, 16, %sp",]
        return "; ".join(insns)


class SPARC64(SPARC):
    """Refs:
    - http://math-atlas.sourceforge.net/devel/assembly/abi_sysV_sparc.pdf
    - https://cr.yp.to/2005-590/sparcv9.pdf
    """
    aliases = ("SPARC64", Elf.Abi.SPARC64)
    arch = "SPARC"
    mode = "V9"

    all_registers = [
        "$g0", "$g1", "$g2", "$g3", "$g4", "$g5", "$g6", "$g7",
        "$o0", "$o1", "$o2", "$o3", "$o4", "$o5", "$o7",
        "$l0", "$l1", "$l2", "$l3", "$l4", "$l5", "$l6", "$l7",
        "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i7",
        "$pc", "$npc", "$sp", "$fp", "$state", ]

    flag_register = "$state"  # sparcv9.pdf, 5.1.5.1 (ccr)
    flags_table = {
        35: "negative",
        34: "zero",
        33: "overflow",
        32: "carry",
    }

    syscall_instructions = ["t 0x6d"]

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        hi = (addr & 0xffff0000) >> 16
        lo = (addr & 0x0000ffff)
        _NR_mprotect = 125
        insns = ["add %sp, -16, %sp",
                 "st %g1, [ %sp ]", "st %o0, [ %sp + 4 ]",
                 "st %o1, [ %sp + 8 ]", "st %o2, [ %sp + 12 ]",
                 f"sethi  %hi({hi}), %o0",
                 f"or  %o0, {lo}, %o0",
                 "clr  %o1",
                 "clr  %o2",
                 f"mov  {_NR_mprotect}, %g1",
                 "t 0x6d",
                 "ld [ %sp ], %g1", "ld [ %sp + 4 ], %o0",
                 "ld [ %sp + 8 ], %o1", "ld [ %sp + 12 ], %o2",
                 "add %sp, 16, %sp",]
        return "; ".join(insns)


class MIPS(Architecture):
    aliases = ("MIPS", Elf.Abi.MIPS)
    arch = "MIPS"
    mode = "MIPS32"

    # https://vhouten.home.xs4all.nl/mipsel/r3000-isa.html
    all_registers = (
        "$zero", "$at", "$v0", "$v1", "$a0", "$a1", "$a2", "$a3",
        "$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7",
        "$s0", "$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7",
        "$t8", "$t9", "$k0", "$k1", "$s8", "$pc", "$sp", "$hi",
        "$lo", "$fir", "$ra", "$gp", )
    instruction_length = 4
    _ptrsize = 4
    nop_insn = b"\x00\x00\x00\x00"  # sll $0,$0,0
    return_register = "$v0"
    flag_register = "$fcsr"
    flags_table = {}
    function_parameters = ("$a0", "$a1", "$a2", "$a3")
    syscall_register = "$v0"
    syscall_instructions = ("syscall",)

    def flag_register_to_human(self, val: int | None = None) -> str:
        return Color.colorify("No flag register", "yellow underline")

    def is_call(self, insn: Instruction) -> bool:
        return False

    def is_ret(self, insn: Instruction) -> bool:
        return insn.mnemonic == "jr" and insn.operands[0] == "ra"

    def is_conditional_branch(self, insn: Instruction) -> bool:
        mnemo = insn.mnemonic
        branch_mnemos = {"beq", "bne", "beqz", "bnez", "bgtz", "bgez", "bltz", "blez"}
        return mnemo in branch_mnemos

    def is_branch_taken(self, insn: Instruction) -> tuple[bool, str]:
        mnemo, ops = insn.mnemonic, insn.operands
        taken, reason = False, ""

        if mnemo == "beq":
            taken, reason = gef.arch.register(ops[0]) == gef.arch.register(ops[1]), f"{ops[0]} == {ops[1]}"
        elif mnemo == "bne":
            taken, reason = gef.arch.register(ops[0]) != gef.arch.register(ops[1]), f"{ops[0]} != {ops[1]}"
        elif mnemo == "beqz":
            taken, reason = gef.arch.register(ops[0]) == 0, f"{ops[0]} == 0"
        elif mnemo == "bnez":
            taken, reason = gef.arch.register(ops[0]) != 0, f"{ops[0]} != 0"
        elif mnemo == "bgtz":
            taken, reason = gef.arch.register(ops[0]) > 0, f"{ops[0]} > 0"
        elif mnemo == "bgez":
            taken, reason = gef.arch.register(ops[0]) >= 0, f"{ops[0]} >= 0"
        elif mnemo == "bltz":
            taken, reason = gef.arch.register(ops[0]) < 0, f"{ops[0]} < 0"
        elif mnemo == "blez":
            taken, reason = gef.arch.register(ops[0]) <= 0, f"{ops[0]} <= 0"
        return taken, reason

    def get_ra(self, insn: Instruction, frame: "gdb.Frame") -> int | None:
        ra = None
        if self.is_ret(insn):
            ra = gef.arch.register("$ra")
        else:
            older = frame.older()
            if older:
                ra = to_unsigned_long(older.pc())
        return ra

    @classmethod
    def mprotect_asm(cls, addr: int, size: int, perm: Permission) -> str:
        _NR_mprotect = 4125
        insns = ["addi $sp, $sp, -16",
                 "sw $v0, 0($sp)", "sw $a0, 4($sp)",
                 "sw $a3, 8($sp)", "sw $a3, 12($sp)",
                 f"li $v0, {_NR_mprotect:d}",
                 f"li $a0, {addr:d}",
                 f"li $a1, {size:d}",
                 f"li $a2, {perm.value:d}",
                 "syscall",
                 "lw $v0, 0($sp)", "lw $a1, 4($sp)",
                 "lw $a3, 8($sp)", "lw $a3, 12($sp)",
                 "addi $sp, $sp, 16",]
        return "; ".join(insns)


class MIPS64(MIPS):
    aliases = ("MIPS64",)
    arch = "MIPS"
    mode = "MIPS64"
    _ptrsize = 8

    @staticmethod
    def supports_gdb_arch(gdb_arch: str) -> bool | None:
        if not gef.binary or not isinstance(gef.binary, Elf):
            return False
        return gdb_arch.startswith("mips") and gef.binary.e_class == Elf.Class.ELF_64_BITS


def copy_to_clipboard(data: bytes) -> None:
    """Helper function to submit data to the clipboard"""
    if sys.platform == "linux":
        xclip = which("xclip")
        prog = [xclip, "-selection", "clipboard", "-i"]
    elif sys.platform == "darwin":
        pbcopy = which("pbcopy")
        prog = [pbcopy]
    else:
        raise NotImplementedError("copy: Unsupported OS")

    with subprocess.Popen(prog, stdin=subprocess.PIPE) as p:
        assert p.stdin
        p.stdin.write(data)
        p.stdin.close()
        p.wait()
    return


def use_stdtype() -> str:
    if is_32bit(): return "uint32_t"
    elif is_64bit(): return "uint64_t"
    return "uint16_t"


def use_default_type() -> str:
    if is_32bit(): return "unsigned int"
    elif is_64bit(): return "unsigned long"
    return "unsigned short"


def use_golang_type() -> str:
    if is_32bit(): return "uint32"
    elif is_64bit(): return "uint64"
    return "uint16"


def use_rust_type() -> str:
    if is_32bit(): return "u32"
    elif is_64bit(): return "u64"
    return "u16"


def to_unsigned_long(v: gdb.Value) -> int:
    """Cast a gdb.Value to unsigned long."""
    mask = (1 << (gef.arch.ptrsize*8)) - 1
    return int(v.cast(gdb.Value(mask).type)) & mask


def get_path_from_info_proc() -> str | None:
    for x in (gdb.execute("info proc", to_string=True) or "").splitlines():
        if x.startswith("exe = "):
            return x.split(" = ")[1].replace("'", "")
    return None


@deprecated("Use `gef.session.os`")
def get_os() -> str:
    return gef.session.os


@lru_cache()
def is_qemu() -> bool:
    if not is_remote_debug():
        return False
    response = gdb.execute("maintenance packet Qqemu.sstepbits", to_string=True, from_tty=False) or ""
    return "ENABLE=" in response


@lru_cache()
def is_qemu_usermode() -> bool:
    if not is_qemu():
        return False
    response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) or ""
    return "Text=" in response


@lru_cache()
def is_qemu_system() -> bool:
    if not is_qemu():
        return False
    response = gdb.execute("maintenance packet qOffsets", to_string=True, from_tty=False) or ""
    return "received: \"\"" in response


def is_target_coredump() -> bool:
    global gef
    if gef.session.coredump_mode is not None:
        return gef.session.coredump_mode
    lines = (gdb.execute("maintenance info section", to_string=True) or "").splitlines()
    is_coredump_mode = any(map(lambda line: line.startswith("Core file: "), lines))
    gef.session.coredump_mode = is_coredump_mode
    return is_coredump_mode


def get_filepath() -> str | None:
    """Return the local absolute path of the file currently debugged."""
    if gef.session.remote:
        return str(gef.session.remote.lfile.absolute())
    if gef.session.file:
        return str(gef.session.file.absolute())
    return None


def get_function_length(sym: str) -> int:
    """Attempt to get the length of the raw bytes of a function."""
    dis = (gdb.execute(f"disassemble '{sym}'", to_string=True) or "").splitlines()
    start_addr = int(dis[1].split()[0], 16)
    end_addr = int(dis[-2].split()[0], 16)
    return end_addr - start_addr


@lru_cache()
def get_info_files() -> list[Zone]:
    """Retrieve all the files loaded by debuggee."""
    lines = (gdb.execute("info files", to_string=True) or "").splitlines()
    infos = []
    for line in lines:
        line = line.strip()
        if not line:
            break

        if not line.startswith("0x"):
            continue

        blobs = [x.strip() for x in line.split(" ")]
        addr_start = int(blobs[0], 16)
        addr_end = int(blobs[2], 16)
        section_name = blobs[4]

        if len(blobs) == 7:
            filename = blobs[6]
        else:
            filename = get_filepath()

        infos.append(Zone(section_name, addr_start, addr_end, filename))
    return infos


def process_lookup_address(address: int) -> Section | None:
    """Look up for an address in memory.
    Return an Address object if found, None otherwise."""
    if not is_alive():
        err("Process is not running")
        return None

    if is_x86():
        if is_in_x86_kernel(address):
            return None

    for sect in gef.memory.maps:
        if sect.page_start <= address < sect.page_end:
            return sect

    return None


@lru_cache()
def process_lookup_path(name: str, perm: Permission = Permission.ALL) -> Section | None:
    """Look up for a path in the process memory mapping.
    Return a Section object if found, None otherwise."""
    if not is_alive():
        err("Process is not running")
        return None

    matches: dict[str, Section] = dict()
    for sect in gef.memory.maps:
        filename = pathlib.Path(sect.path).name

        if name in filename and sect.permission & perm:
            if sect.path not in matches.keys():
                matches[sect.path] = sect

    matches_count = len(matches)

    if matches_count == 0:
        return None

    if matches_count > 1:
        warn(f"{matches_count} matches! You should probably refine your search!")

        for x in matches.keys():
            warn(f"- '{x}'")

        warn("Returning the first match")

    return list(matches.values())[0]


@lru_cache()
def file_lookup_name_path(name: str, path: str) -> Zone | None:
    """Look up a file by name and path.
    Return a Zone object if found, None otherwise."""
    for xfile in get_info_files():
        if path == xfile.filename and name == xfile.name:
            return xfile
    return None


@lru_cache()
def file_lookup_address(address: int) -> Zone | None:
    """Look up for a file by its address.
    Return a Zone object if found, None otherwise."""
    for info in get_info_files():
        if info.zone_start <= address < info.zone_end:
            return info
    return None


@lru_cache()
def lookup_address(address: int) -> Address:
    """Try to find the address in the process address space.
    Return an Address object, with validity flag set based on success."""
    sect = process_lookup_address(address)
    info = file_lookup_address(address)
    if sect is None and info is None:
        # i.e. there is no info on this address
        return Address(value=address, valid=False)
    return Address(value=address, section=sect, info=info)


def xor(data: ByteString, key: str) -> bytearray:
    """Return `data` xor-ed with `key`."""
    key_raw = binascii.unhexlify(key.lstrip("0x"))
    return bytearray(x ^ y for x, y in zip(data, itertools.cycle(key_raw)))


def is_hex(pattern: str) -> bool:
    """Return whether provided string is a hexadecimal value."""
    if not pattern.lower().startswith("0x"):
        return False
    return len(pattern) % 2 == 0 and all(c in string.hexdigits for c in pattern[2:])


def continue_handler(_: "gdb.ContinueEvent") -> None:
    """GDB event handler for new object continue cases."""
    return


def hook_stop_handler(_: "gdb.StopEvent") -> None:
    """GDB event handler for stop cases."""
    reset_all_caches()
    gdb.execute("context")
    return


def new_objfile_handler(evt: "gdb.NewObjFileEvent | None") -> None:
    """GDB event handler for new object file cases."""
    reset_all_caches()
    progspace = gdb.current_progspace()
    if evt:
        path = evt.new_objfile.filename or ""
    elif progspace:
        path = progspace.filename or ""
    else:
        raise RuntimeError("Cannot determine file path")
    try:
        if gef.session.root and path.startswith("target:"):
            # If the process is in a container, replace the "target:" prefix
            # with the actual root directory of the process.
            path = path.replace("target:", str(gef.session.root), 1)
        target = pathlib.Path(path)
        FileFormatClasses = list(filter(lambda fmtcls: fmtcls.is_valid(target), __registered_file_formats__))
        GuessedFileFormatClass : Type[FileFormat] = FileFormatClasses.pop() if len(FileFormatClasses) else Elf
        binary = GuessedFileFormatClass(target)
        if not gef.binary:
            gef.binary = binary
            reset_architecture()
        else:
            gef.session.modules.append(binary)
    except FileNotFoundError as fne:
        # Linux automatically maps the vDSO into our process, and GDB
        # will give us the string 'system-supplied DSO' as a path.
        # This is normal, so we shouldn't warn the user about it
        if "system-supplied DSO" not in path:
            warn(f"Failed to find objfile or not a valid file format: {str(fne)}")
    except RuntimeError as re:
        warn(f"Not a valid file format: {str(re)}")
    return


def exit_handler(_: "gdb.ExitedEvent") -> None:
    """GDB event handler for exit cases."""
    global gef
    # flush the caches
    reset_all_caches()

    # disconnect properly the remote session
    gef.session.qemu_mode = False
    if gef.session.remote:
        gef.session.remote.close()
        del gef.session.remote
        gef.session.remote = None
        gef.session.remote_initializing = False

    # if `autosave_breakpoints_file` setting is configured, save the breakpoints to disk
    setting = (gef.config["gef.autosave_breakpoints_file"] or "").strip()
    if not setting:
        return

    bkp_fpath = pathlib.Path(setting).expanduser().absolute()
    if bkp_fpath.exists():
        warn(f"{bkp_fpath} exists, content will be overwritten")

    with bkp_fpath.open("w") as fd:
        for bp in list(gdb.breakpoints()):
            if not bp.enabled or not bp.is_valid:
                continue
            fd.write(f"{'t' if bp.temporary else ''}break {bp.location}\n")
    return


def memchanged_handler(_: "gdb.MemoryChangedEvent") -> None:
    """GDB event handler for mem changes cases."""
    reset_all_caches()
    return


def regchanged_handler(_: "gdb.RegisterChangedEvent") -> None:
    """GDB event handler for reg changes cases."""
    reset_all_caches()
    return


def get_terminal_size() -> tuple[int, int]:
    """Return the current terminal size."""
    if is_debug():
        return 600, 100

    if platform.system() == "Windows":
        from ctypes import create_string_buffer, windll # type: ignore
        hStdErr = -12
        herr = windll.kernel32.GetStdHandle(hStdErr)
        csbi = create_string_buffer(22)
        res = windll.kernel32.GetConsoleScreenBufferInfo(herr, csbi)
        if res:
            _, _, _, _, _, left, top, right, bottom, _, _ = struct.unpack("hhhhHhhhhhh", csbi.raw)
            tty_columns = right - left + 1
            tty_rows = bottom - top + 1
            return tty_rows, tty_columns
        else:
            return 600, 100
    else:
        import fcntl
        import termios
        try:
            tty_rows, tty_columns = struct.unpack("hh", fcntl.ioctl(1, termios.TIOCGWINSZ, "1234")) # type: ignore
            return tty_rows, tty_columns
        except OSError:
            return 600, 100


@lru_cache()
def is_64bit() -> bool:
    """Checks if current target is 64bit."""
    return gef.arch.ptrsize == 8


@lru_cache()
def is_32bit() -> bool:
    """Checks if current target is 32bit."""
    return gef.arch.ptrsize == 4


@lru_cache()
def is_x86_64() -> bool:
    """Checks if current target is x86-64"""
    return Elf.Abi.X86_64 in gef.arch.aliases


@lru_cache()
def is_x86_32():
    """Checks if current target is an x86-32"""
    return Elf.Abi.X86_32 in gef.arch.aliases


@lru_cache()
def is_x86() -> bool:
    return is_x86_32() or is_x86_64()


@lru_cache()
def is_arch(arch: Elf.Abi) -> bool:
    return arch in gef.arch.aliases


def reset_architecture(arch: str | None = None) -> None:
    """Sets the current architecture.
    If an architecture is explicitly specified by parameter, try to use that one. If this fails, an `OSError`
    exception will occur.
    If no architecture is specified, then GEF will attempt to determine automatically based on the current
    ELF target. If this fails, an `OSError` exception will occur.
    """
    global gef
    arches = __registered_architectures__

    # check if the architecture is forced by parameter
    if arch:
        try:
            gef.arch = arches[arch.lower()]()
            gef.arch_reason = "The architecture has been set manually"
        except KeyError:
            raise OSError(f"Specified arch {arch.upper()} is not supported")
        return

    # check for bin running
    if is_alive():
        gdb_arch = gdb.selected_frame().architecture().name()
        preciser_arch = next((a for a in arches.values() if a.supports_gdb_arch(gdb_arch)), None)
        if preciser_arch:
            gef.arch = preciser_arch()
            gef.arch_reason = "The architecture has been detected by GDB"
            return

    # last resort, use the info from elf header to find it from the known architectures
    if gef.binary and isinstance(gef.binary, Elf):
        try:
            gef.arch = arches[gef.binary.e_machine]()
            gef.arch_reason = "The architecture has been detected via the ELF headers"
        except KeyError:
            raise OSError(f"CPU type is currently not supported: {gef.binary.e_machine}")
        return

    warn("Did not find any way to guess the correct architecture :(")


@lru_cache()
def cached_lookup_type(_type: str) -> gdb.Type | None:
    try:
        return gdb.lookup_type(_type).strip_typedefs()
    except RuntimeError:
        return None


@deprecated("Use `gef.arch.ptrsize` instead")
def get_memory_alignment(in_bits: bool = False) -> int:
    """Try to determine the size of a pointer on this system.
    First, try to parse it out of the ELF header.
    Next, use the size of `size_t`.
    Finally, try the size of $pc.
    If `in_bits` is set to True, the result is returned in bits, otherwise in
    bytes."""
    res = cached_lookup_type("size_t")
    if res is not None:
        return res.sizeof if not in_bits else res.sizeof * 8

    try:
        return gdb.parse_and_eval("$pc").type.sizeof
    except Exception:
        pass

    raise OSError("GEF is running under an unsupported mode")


def clear_screen(tty: str = "") -> None:
    """Clear the screen."""
    clean_sequence = "\x1b[H\x1b[J"
    if tty:
        pathlib.Path(tty).write_text(clean_sequence)
    else:
        sys.stdout.write(clean_sequence)

    return


def format_address(addr: int) -> str:
    """Format the address according to its size."""
    memalign_size = gef.arch.ptrsize
    addr = align_address(addr)
    return f"0x{addr:016x}" if memalign_size == 8 else f"0x{addr:08x}"


def format_address_spaces(addr: int, left: bool = True) -> str:
    """Format the address according to its size, but with spaces instead of zeroes."""
    width = gef.arch.ptrsize * 2 + 2
    addr = align_address(addr)

    if not left:
        return f"{addr:#x}".rjust(width)

    return f"{addr:#x}".ljust(width)


def align_address(address: int) -> int:
    """Align the provided address to the process's native length."""
    return address & 0xFFFFFFFFFFFFFFFF if gef.arch.ptrsize == 8 else address & 0xFFFFFFFF


def align_address_to_size(address: int, align: int) -> int:
    """Align the address to the given size."""
    return address + ((align - (address % align)) % align)


def align_address_to_page(address: int) -> int:
    """Align the address to a page."""
    a = align_address(address) >> DEFAULT_PAGE_ALIGN_SHIFT
    return a << DEFAULT_PAGE_ALIGN_SHIFT


def parse_address(address: str) -> int:
    """Parse an address and return it as an Integer."""
    if is_hex(address):
        return int(address, 16)
    return int(gdb.parse_and_eval(address))


def is_in_x86_kernel(address: int) -> bool:
    address = align_address(address)
    memalign = gef.arch.ptrsize*8 - 1
    return (address >> memalign) == 0xF


def is_remote_debug() -> bool:
    """"Return True is the current debugging session is running through GDB remote session."""
    return gef.session.remote_initializing or gef.session.remote is not None


def de_bruijn(alphabet: bytes, n: int) -> Generator[int, None, None]:
    """De Bruijn sequence for alphabet and subsequences of length n (for compat. w/ pwnlib)."""
    k = len(alphabet)
    a = [0] * k * n

    def db(t: int, p: int) -> Generator[int, None, None]:
        if t > n:
            if n % p == 0:
                for j in range(1, p + 1):
                    yield alphabet[a[j]]
        else:
            a[t] = a[t - p]
            yield from db(t + 1, p)

            for j in range(a[t - p] + 1, k):
                a[t] = j
                yield from db(t + 1, t)

    return db(1, 1)


def generate_cyclic_pattern(length: int, cycle: int = 4) -> bytearray:
    """Create a `length` byte bytearray of a de Bruijn cyclic pattern."""
    charset = bytearray(b"abcdefghijklmnopqrstuvwxyz")
    return bytearray(itertools.islice(de_bruijn(charset, cycle), length))


def safe_parse_and_eval(value: str) -> "gdb.Value | None":
    """GEF wrapper for gdb.parse_and_eval(): this function returns None instead of raising
    gdb.error if the eval failed."""
    try:
        return gdb.parse_and_eval(value)
    except gdb.error as e:
        dbg(f"gdb.parse_and_eval() failed, reason: {str(e)}")
    return None


@lru_cache()
def dereference(addr: int) -> "gdb.Value | None":
    """GEF wrapper for gdb dereference function."""
    try:
        ulong_t = cached_lookup_type(use_stdtype()) or \
                  cached_lookup_type(use_default_type()) or \
                  cached_lookup_type(use_golang_type()) or \
                  cached_lookup_type(use_rust_type())
        if not ulong_t:
            raise gdb.MemoryError("Failed to determine unsigned long type")
        unsigned_long_type = ulong_t.pointer()
        res = gdb.Value(addr).cast(unsigned_long_type).dereference()
        # GDB does lazy fetch by default so we need to force access to the value
        res.fetch_lazy()
        return res
    except gdb.MemoryError as e:
        dbg(str(e))
    return None


def gef_convenience(value: str |  bytes) -> str:
    """Defines a new convenience value."""
    global gef
    var_name = f"$_gef{gef.session.convenience_vars_index:d}"
    gef.session.convenience_vars_index += 1
    if isinstance(value, str):
        gdb.execute(f"""set {var_name} = "{value}" """)
    elif isinstance(value, bytes):
        value_as_array = "{" + ", ".join([f"0x{b:02x}" for b in value]) + "}"
        gdb.execute(f"""set {var_name} = {value_as_array} """)
    else:
        raise TypeError
    return var_name


def parse_string_range(s: str) -> Iterator[int]:
    """Parses an address range (e.g. 0x400000-0x401000)"""
    addrs = s.split("-")
    return map(lambda x: int(x, 16), addrs)


@lru_cache()
def is_syscall(instruction: Instruction | int) -> bool:
    """Checks whether an instruction or address points to a system call."""
    if isinstance(instruction, int):
        instruction = gef_current_instruction(instruction)
    insn_str = instruction.mnemonic
    if len(instruction.operands):
        insn_str += f" {', '.join(instruction.operands)}"
    return insn_str in gef.arch.syscall_instructions


#
# Deprecated API
#

@deprecated("Use `gef.session.pie_breakpoints[num]`")
def gef_get_pie_breakpoint(num: int) -> "PieVirtualBreakpoint":
    return gef.session.pie_breakpoints[num]


@deprecated("Use `str(gef.arch.endianness)` instead")
def endian_str() -> str:
    return str(gef.arch.endianness)


@deprecated("Use `gef.config[key]`")
def get_gef_setting(name: str) -> Any:
    return gef.config[name]


@deprecated("Use `gef.config[key] = value`")
def set_gef_setting(name: str, value: Any) -> None:
    gef.config[name] = value
    return


@deprecated("Use `gef.session.pagesize`")
def gef_getpagesize() -> int:
    return gef.session.pagesize


@deprecated("Use `gef.session.canary`")
def gef_read_canary() -> tuple[int, int] | None:
    return gef.session.canary


@deprecated("Use `gef.session.pid`")
def get_pid() -> int:
    return gef.session.pid


@deprecated("Use `gef.session.file.name`")
def get_filename() -> str:
    assert gef.session.file
    return gef.session.file.name


@deprecated("Use `gef.heap.main_arena`")
def get_glibc_arena() -> GlibcArena | None:
    return gef.heap.main_arena


@deprecated("Use `gef.arch.register(regname)`")
def get_register(regname) -> int | None:
    return gef.arch.register(regname)


@deprecated("Use `gef.memory.maps`")
def get_process_maps() -> list[Section]:
    return gef.memory.maps


@deprecated("Use `reset_architecture`")
def set_arch(arch: str | None = None, _: str | None = None) -> None:
    return reset_architecture(arch)

#
# GDB event hooking
#

@only_if_events_supported("cont")
def gef_on_continue_hook(func: Callable[["gdb.ContinueEvent"], None]) -> None:
    gdb.events.cont.connect(func)


@only_if_events_supported("cont")
def gef_on_continue_unhook(func: Callable[["gdb.ThreadEvent"], None]) -> None:
    gdb.events.cont.disconnect(func)


@only_if_events_supported("stop")
def gef_on_stop_hook(func: Callable[["gdb.StopEvent"], None]) -> None:
    gdb.events.stop.connect(func)


@only_if_events_supported("stop")
def gef_on_stop_unhook(func: Callable[["gdb.StopEvent"], None]) -> None:
    gdb.events.stop.disconnect(func)


@only_if_events_supported("exited")
def gef_on_exit_hook(func: Callable[["gdb.ExitedEvent"], None]) -> None:
    gdb.events.exited.connect(func)


@only_if_events_supported("exited")
def gef_on_exit_unhook(func: Callable[["gdb.ExitedEvent"], None]) -> None:
    gdb.events.exited.disconnect(func)


@only_if_events_supported("new_objfile")
def gef_on_new_hook(func: Callable[["gdb.NewObjFileEvent"], None]) -> None:
    gdb.events.new_objfile.connect(func)


@only_if_events_supported("new_objfile")
def gef_on_new_unhook(func: Callable[["gdb.NewObjFileEvent"], None]) -> None:
    gdb.events.new_objfile.disconnect(func)


@only_if_events_supported("clear_objfiles")
def gef_on_unload_objfile_hook(func: Callable[["gdb.ClearObjFilesEvent"], None]) -> None:
    gdb.events.clear_objfiles.connect(func)


@only_if_events_supported("clear_objfiles")
def gef_on_unload_objfile_unhook(func: Callable[["gdb.ClearObjFilesEvent"], None]) -> None:
    gdb.events.clear_objfiles.disconnect(func)


@only_if_events_supported("memory_changed")
def gef_on_memchanged_hook(func: Callable[["gdb.MemoryChangedEvent"], None]) -> None:
    gdb.events.memory_changed.connect(func)


@only_if_events_supported("memory_changed")
def gef_on_memchanged_unhook(func: Callable[["gdb.MemoryChangedEvent"], None]) -> None:
    gdb.events.memory_changed.disconnect(func)


@only_if_events_supported("register_changed")
def gef_on_regchanged_hook(func: Callable[["gdb.RegisterChangedEvent"], None]) -> None:
    gdb.events.register_changed.connect(func)


@only_if_events_supported("register_changed")
def gef_on_regchanged_unhook(func: Callable[["gdb.RegisterChangedEvent"], None]) -> None:
    gdb.events.register_changed.disconnect(func)


#
# Virtual breakpoints
#

class PieVirtualBreakpoint:
    """PIE virtual breakpoint (not real breakpoint)."""

    def __init__(self, set_func: Callable[[int], str], vbp_num: int, addr: int) -> None:
        # set_func(base): given a base address return a
        # "set breakpoint" gdb command string
        self.set_func = set_func
        self.vbp_num = vbp_num
        # breakpoint num, 0 represents not instantiated yet
        self.bp_num = 0
        self.bp_addr = 0
        # this address might be a symbol, just to know where to break
        if isinstance(addr, int):
            self.addr: int |  str = hex(addr)
        else:
            self.addr = addr
        return

    def instantiate(self, base: int) -> None:
        if self.bp_num:
            self.destroy()

        try:
            res = gdb.execute(self.set_func(base), to_string=True) or ""
            if not res: return
        except gdb.error as e:
            err(str(e))
            return

        if "Breakpoint" not in res:
            err(res)
            return

        res_list = res.split()
        self.bp_num = res_list[1]
        self.bp_addr = res_list[3]
        return

    def destroy(self) -> None:
        if not self.bp_num:
            err("Destroy PIE breakpoint not even set")
            return
        gdb.execute(f"delete {self.bp_num}")
        self.bp_num = 0
        return


#
# Breakpoints
#

class FormatStringBreakpoint(gdb.Breakpoint):
    """Inspect stack for format string."""
    def __init__(self, spec: str, num_args: int) -> None:
        super().__init__(spec, type=gdb.BP_BREAKPOINT, internal=False)
        self.num_args = num_args
        self.enabled = True
        return

    def stop(self) -> bool:
        reset_all_caches()
        msg = []
        ptr, addr = gef.arch.get_ith_parameter(self.num_args)
        addr = lookup_address(addr)

        if not addr.valid:
            return False

        if addr.section.is_writable():
            content = gef.memory.read_cstring(addr.value)
            name = addr.info.name if addr.info else addr.section.path
            msg.append(Color.colorify("Format string helper", "yellow bold"))
            msg.append(f"Possible insecure format string: {self.location}('{ptr}' {RIGHT_ARROW} {addr.value:#x}: '{content}')")
            msg.append(f"Reason: Call to '{self.location}()' with format string argument in position "
                       f"#{self.num_args:d} is in page {addr.section.page_start:#x} ({name}) that has write permission")
            push_context_message("warn", "\n".join(msg))
            return True

        return False


class StubBreakpoint(gdb.Breakpoint):
    """Create a breakpoint to permanently disable a call (fork/alarm/signal/etc.)."""

    def __init__(self, func: str, retval: int | None) -> None:
        super().__init__(func, gdb.BP_BREAKPOINT, internal=False)
        self.func = func
        self.retval = retval

        m = f"All calls to '{self.func}' will be skipped"
        if self.retval is not None:
            m += f" (with return value set to {self.retval:#x})"
        info(m)
        return

    def stop(self) -> bool:
        size = "long" if gef.arch.ptrsize == 8 else "int"
        gdb.execute(f"return (unsigned {size}){self.retval:#x}")
        ok(f"Ignoring call to '{self.func}' "
           f"(setting return value to {self.retval:#x})")
        return False


class ChangePermissionBreakpoint(gdb.Breakpoint):
    """When hit, this temporary breakpoint will restore the original code, and position
    $pc correctly."""

    def __init__(self, loc: str, code: ByteString, pc: int) -> None:
        super().__init__(loc, gdb.BP_BREAKPOINT, internal=False)
        self.original_code = code
        self.original_pc = pc
        return

    def stop(self) -> bool:
        info("Restoring original context")
        gef.memory.write(self.original_pc, self.original_code, len(self.original_code))
        info("Restoring $pc")
        gdb.execute(f"set $pc = {self.original_pc:#x}")
        return True


class TraceMallocBreakpoint(gdb.Breakpoint):
    """Track allocations done with malloc() or calloc()."""

    def __init__(self, name: str) -> None:
        super().__init__(name, gdb.BP_BREAKPOINT, internal=True)
        self.silent = True
        self.name = name
        return

    def stop(self) -> bool:
        reset_all_caches()
        _, size = gef.arch.get_ith_parameter(0)
        assert size
        self.retbp = TraceMallocRetBreakpoint(size, self.name)
        return False


class TraceMallocRetBreakpoint(gdb.FinishBreakpoint):
    """Internal temporary breakpoint to retrieve the return value of malloc()."""

    def __init__(self, size: int, name: str) -> None:
        super().__init__(gdb.newest_frame(), internal=True)
        self.size = size
        self.name = name
        self.silent = True
        return

    def stop(self) -> bool:
        if self.return_value:
            loc = int(self.return_value)
        else:
            loc = parse_address(gef.arch.return_register)

        size = self.size
        ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - {self.name}({size})={loc:#x}")
        check_heap_overlap = gef.config["heap-analysis-helper.check_heap_overlap"]

        # pop from free-ed list if it was in it
        if gef.session.heap_freed_chunks:
            idx = 0
            for item in gef.session.heap_freed_chunks:
                addr = item[0]
                if addr == loc:
                    gef.session.heap_freed_chunks.remove(item)
                    continue
                idx += 1

        # pop from uaf watchlist
        if gef.session.heap_uaf_watchpoints:
            idx = 0
            for wp in gef.session.heap_uaf_watchpoints:
                wp_addr = wp.address
                if loc <= wp_addr < loc + size:
                    gef.session.heap_uaf_watchpoints.remove(wp)
                    wp.enabled = False
                    continue
                idx += 1

        item = (loc, size)

        if check_heap_overlap:
            # seek all the currently allocated chunks, read their effective size and check for overlap
            msg = []
            align = gef.arch.ptrsize
            for chunk_addr, _ in gef.session.heap_allocated_chunks:
                current_chunk = GlibcChunk(chunk_addr)
                current_chunk_size = current_chunk.size

                if chunk_addr <= loc < chunk_addr + current_chunk_size:
                    offset = loc - chunk_addr - 2*align
                    if offset < 0: continue # false positive, discard

                    msg.append(Color.colorify("Heap-Analysis", "yellow bold"))
                    msg.append("Possible heap overlap detected")
                    msg.append(f"Reason {RIGHT_ARROW} new allocated chunk {loc:#x} (of size {size:d}) overlaps in-used chunk {chunk_addr:#x} (of size {current_chunk_size:#x})")
                    msg.append(f"Writing {offset:d} bytes from {chunk_addr:#x} will reach chunk {loc:#x}")
                    msg.append(f"Payload example for chunk {chunk_addr:#x} (to overwrite {loc:#x} headers):")
                    msg.append(f"  data = 'A'*{offset:d} + 'B'*{align:d} + 'C'*{align:d}")
                    push_context_message("warn", "\n".join(msg))
                    return True

        # add it to alloc-ed list
        gef.session.heap_allocated_chunks.append(item)
        return False


class TraceReallocBreakpoint(gdb.Breakpoint):
    """Track re-allocations done with realloc()."""

    def __init__(self) -> None:
        super().__init__("__libc_realloc", gdb.BP_BREAKPOINT, internal=True)
        self.silent = True
        return

    def stop(self) -> bool:
        _, ptr = gef.arch.get_ith_parameter(0)
        _, size = gef.arch.get_ith_parameter(1)
        assert ptr is not None and size is not None
        self.retbp = TraceReallocRetBreakpoint(ptr, size)
        return False


class TraceReallocRetBreakpoint(gdb.FinishBreakpoint):
    """Internal temporary breakpoint to retrieve the return value of realloc()."""

    def __init__(self, ptr: int, size: int) -> None:
        super().__init__(gdb.newest_frame(), internal=True)
        self.ptr = ptr
        self.size = size
        self.silent = True
        return

    def stop(self) -> bool:
        if self.return_value:
            newloc = int(self.return_value)
        else:
            newloc = parse_address(gef.arch.return_register)

        title = Color.colorify("Heap-Analysis", "yellow bold")
        if newloc != self:
            loc = Color.colorify(f"{newloc:#x}", "green")
            ok(f"{title} - realloc({self.ptr:#x}, {self.size})={loc}")
        else:
            loc = Color.colorify(f"{newloc:#x}", "red")
            ok(f"{title} - realloc({self.ptr:#x}, {self.size})={loc}")

        item = (newloc, self.size)

        try:
            # check if item was in alloc-ed list
            idx = [x for x, y in gef.session.heap_allocated_chunks].index(self.ptr)
            # if so pop it out
            item = gef.session.heap_allocated_chunks.pop(idx)
        except ValueError:
            if is_debug():
                warn(f"Chunk {self.ptr:#x} was not in tracking list")
        finally:
            # add new item to alloc-ed list
            gef.session.heap_allocated_chunks.append(item)

        return False


class TraceFreeBreakpoint(gdb.Breakpoint):
    """Track calls to free() and attempts to detect inconsistencies."""

    def __init__(self) -> None:
        super().__init__("__libc_free", gdb.BP_BREAKPOINT, internal=True)
        self.silent = True
        return

    def stop(self) -> bool:
        reset_all_caches()
        _, addr = gef.arch.get_ith_parameter(0)
        msg = []
        check_free_null = gef.config["heap-analysis-helper.check_free_null"]
        check_double_free = gef.config["heap-analysis-helper.check_double_free"]
        check_weird_free = gef.config["heap-analysis-helper.check_weird_free"]
        check_uaf = gef.config["heap-analysis-helper.check_uaf"]

        ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - free({addr:#x})")
        if not addr:
            if check_free_null:
                msg.append(Color.colorify("Heap-Analysis", "yellow bold"))
                msg.append(f"Attempting to free(NULL) at {gef.arch.pc:#x}")
                msg.append("Reason: if NULL page is allocatable, this can lead to code execution.")
                push_context_message("warn", "\n".join(msg))
                return True
            return False

        if addr in [x for (x, _) in gef.session.heap_freed_chunks]:
            if check_double_free:
                msg.append(Color.colorify("Heap-Analysis", "yellow bold"))
                msg.append(f"Double-free detected {RIGHT_ARROW} free({addr:#x}) is called at {gef.arch.pc:#x} but is already in the free-ed list")
                msg.append("Execution will likely crash...")
                push_context_message("warn", "\n".join(msg))
                return True
            return False

        # if here, no error
        # 1. move alloc-ed item to free list
        try:
            # pop from alloc-ed list
            idx = [x for x, y in gef.session.heap_allocated_chunks].index(addr)
            item = gef.session.heap_allocated_chunks.pop(idx)

        except ValueError:
            if check_weird_free:
                msg.append(Color.colorify("Heap-Analysis", "yellow bold"))
                msg.append("Heap inconsistency detected:")
                msg.append(f"Attempting to free an unknown value: {addr:#x}")
                push_context_message("warn", "\n".join(msg))
                return True
            return False

        # 2. add it to free-ed list
        gef.session.heap_freed_chunks.append(item)

        self.retbp = None
        if check_uaf:
            # 3. (opt.) add a watchpoint on pointer
            self.retbp = TraceFreeRetBreakpoint(addr)
        return False


class TraceFreeRetBreakpoint(gdb.FinishBreakpoint):
    """Internal temporary breakpoint to track free()d values."""

    def __init__(self, addr: int) -> None:
        super().__init__(gdb.newest_frame(), internal=True)
        self.silent = True
        self.addr = addr
        return

    def stop(self) -> bool:
        reset_all_caches()
        wp = UafWatchpoint(self.addr)
        gef.session.heap_uaf_watchpoints.append(wp)
        return False


class UafWatchpoint(gdb.Breakpoint):
    """Custom watchpoints set TraceFreeBreakpoint() to monitor free()d pointers being used."""

    def __init__(self, addr: int) -> None:
        super().__init__(f"*{addr:#x}", gdb.BP_WATCHPOINT, internal=True)
        self.address = addr
        self.silent = True
        self.enabled = True
        return

    def stop(self) -> bool:
        """If this method is triggered, we likely have a UaF. Break the execution and report it."""
        reset_all_caches()
        frame = gdb.selected_frame()
        if frame.name() in ("_int_malloc", "malloc_consolidate", "__libc_calloc", ):
            return False

        # software watchpoints stop after the next statement (see
        # https://sourceware.org/gdb/onlinedocs/gdb/Set-Watchpoints.html)
        pc = gdb_get_nth_previous_instruction_address(gef.arch.pc, 2)
        assert pc
        insn = gef_current_instruction(pc)
        msg = []
        msg.append(Color.colorify("Heap-Analysis", "yellow bold"))
        msg.append(f"Possible Use-after-Free in '{get_filepath()}': "
                   f"pointer {self.address:#x} was freed, but is attempted to be used at {pc:#x}")
        msg.append(f"{insn.address:#x}   {insn.mnemonic} {Color.yellowify(', '.join(insn.operands))}")
        push_context_message("warn", "\n".join(msg))
        return True


class EntryBreakBreakpoint(gdb.Breakpoint):
    """Breakpoint used internally to stop execution at the most convenient entry point."""

    def __init__(self, location: str) -> None:
        super().__init__(location, gdb.BP_BREAKPOINT, internal=True, temporary=True)
        self.silent = True
        return

    def stop(self) -> bool:
        reset_all_caches()
        return True


class NamedBreakpoint(gdb.Breakpoint):
    """Breakpoint which shows a specified name, when hit."""

    def __init__(self, location: str, name: str) -> None:
        super().__init__(spec=location, type=gdb.BP_BREAKPOINT, internal=False, temporary=False)
        self.name = name
        self.loc = location
        return

    def stop(self) -> bool:
        reset_all_caches()
        push_context_message("info", f"Hit breakpoint {self.loc} ({Color.colorify(self.name, 'red bold')})")
        return True


class JustSilentStopBreakpoint(gdb.Breakpoint):
    """When hit, this temporary breakpoint stop the execution."""

    def __init__(self, loc: str) -> None:
        super().__init__(loc, gdb.BP_BREAKPOINT, temporary=True)
        self.silent = True
        return


#
# Context Panes
#

def register_external_context_pane(pane_name: str, display_pane_function: Callable[[], None], pane_title_function: Callable[[], str | None], condition : Callable[[], bool] | None = None) -> None:
    """
    Registering function for new GEF Context View.
    pane_name: a string that has no spaces (used in settings)
    display_pane_function: a function that uses gef_print() to print strings
    pane_title_function: a function that returns a string or None, which will be displayed as the title.
    If None, no title line is displayed.
    condition: an optional callback: if not None, the callback will be executed first. If it returns true,
      then only the pane title and content will displayed. Otherwise, it's simply skipped.

    Example usage for a simple text to show when we hit a syscall:
    def only_syscall(): return gef_current_instruction(gef.arch.pc).is_syscall()
    def display_pane():
      gef_print("Wow, I am a context pane!")
    def pane_title():
      return "example:pane"
    register_external_context_pane("example_pane", display_pane, pane_title, only_syscall)
    """
    gef.gdb.add_context_pane(pane_name, display_pane_function, pane_title_function, condition)
    return

def register_external_context_layout_mapping(current_pane_name: str, display_pane_function: Callable[[], None], pane_title_function: Callable[[], str | None], condition : Callable[[], bool] | None = None) -> None:
    gef.gdb.add_context_layout_mapping(current_pane_name, display_pane_function, pane_title_function, condition)
    return


#
# Commands
#
@deprecated("Use `register()`, and inherit from `GenericCommand` instead")
def register_external_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]:
    """Registering function for new GEF (sub-)command to GDB."""
    return cls

@deprecated("Use `register()`, and inherit from `GenericCommand` instead")
def register_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]:
    """Decorator for registering new GEF (sub-)command to GDB."""
    return cls

@deprecated("")
def register_priority_command(cls: Type["GenericCommand"]) -> Type["GenericCommand"]:
    """Decorator for registering new command with priority, meaning that it must
    loaded before the other generic commands."""
    return cls


ValidCommandType = TypeVar("ValidCommandType", bound="GenericCommand")
ValidFunctionType = TypeVar("ValidFunctionType", bound="GenericFunction")

def register(cls: Type["ValidCommandType"] |  Type["ValidFunctionType"]) -> Type["ValidCommandType"] |  Type["ValidFunctionType"]:
    global __registered_commands__, __registered_functions__
    if issubclass(cls, GenericCommand):
        assert hasattr(cls, "_cmdline_")
        assert hasattr(cls, "do_invoke")
        if any(map(lambda x: x._cmdline_ == cls._cmdline_, __registered_commands__)):
            raise AlreadyRegisteredException(cls._cmdline_)
        __registered_commands__.add(cls)
        return cls

    if issubclass(cls, GenericFunction):
        assert hasattr(cls, "_function_")
        assert hasattr(cls, "invoke")
        if any(map(lambda x: x._function_ == cls._function_, __registered_functions__)):
            raise AlreadyRegisteredException(cls._function_)
        __registered_functions__.add(cls)
        return cls

    raise TypeError(f"`{cls.__class__}` is an illegal class for `register`")


class GenericCommand(gdb.Command):
    """This is an abstract class for invoking commands, should not be instantiated."""

    _cmdline_: str
    _syntax_: str
    _example_: str | list[str] = ""
    _aliases_: list[str] = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        attributes = ("_cmdline_", "_syntax_", )
        if not all(map(lambda x: hasattr(cls, x), attributes)):
            raise NotImplementedError

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self.pre_load()
        syntax = Color.yellowify("\nSyntax: ") + self._syntax_
        example = Color.yellowify("\nExamples: \n\t")
        if isinstance(self._example_, list):
            example += "\n\t".join(self._example_)
        elif isinstance(self._example_, str):
            example += self._example_
        self.__doc__ = (self.__doc__ or "").replace(" "*4, "") + syntax + example
        self.repeat = False
        self.repeat_count = 0
        self.__last_command = None
        command_type = kwargs.setdefault("command", gdb.COMMAND_USER)
        complete_type = kwargs.setdefault("complete", -1) # -1=allow user-defined `complete()`
        prefix = kwargs.setdefault("prefix", False)
        super().__init__(name=self._cmdline_, command_class=command_type,
                         completer_class=complete_type, prefix=prefix)
        self.post_load()
        return

    def invoke(self, args: str, from_tty: bool) -> None:
        try:
            argv = gdb.string_to_argv(args)
            self.__set_repeat_count(argv, from_tty)
            bufferize(self.do_invoke)(argv)
        except Exception as e:
            # Note: since we are intercepting cleaning exceptions here, commands preferably should avoid
            # catching generic Exception, but rather specific ones. This is allows a much cleaner use.
            if is_debug():
                show_last_exception()
                if gef.config["gef.propagate_debug_exception"] is True:
                    raise
            else:
                err(f"Command '{self._cmdline_}' failed to execute properly, reason: {e}")
        return

    def usage(self) -> None:
        err(f"Syntax\n{self._syntax_}")
        return

    def do_invoke(self, argv: list[str]) -> None:
        raise NotImplementedError

    def pre_load(self) -> None:
        return

    def post_load(self) -> None:
        return

    def __get_setting_name(self, name: str) -> str:
        clsname = self.__class__._cmdline_.replace(" ", "-")
        return f"{clsname}.{name}"

    def __iter__(self) -> Generator[str, None, None]:
        for key in gef.config.keys():
            if key.startswith(self._cmdline_):
                yield key.replace(f"{self._cmdline_}.", "", 1)

    @property
    def settings(self) -> list[str]:
        """Return the list of settings for this command."""
        return list(iter(self))

    @deprecated("Use `self[setting_name]` instead")
    def get_setting(self, name: str) -> Any:
        return self.__getitem__(name)

    def __getitem__(self, name: str) -> Any:
        key = self.__get_setting_name(name)
        return gef.config[key]

    @deprecated("Use `setting_name in self` instead")
    def has_setting(self, name: str) -> bool:
        return self.__contains__(name)

    def __contains__(self, name: str) -> bool:
        return self.__get_setting_name(name) in gef.config

    @deprecated("Use `self[setting_name] = value` instead")
    def add_setting(self, name: str, value: tuple[Any, type, str], description: str = "") -> None:
        return self.__setitem__(name, (value, description))

    def __setitem__(self, name: str, value: "GefSetting | tuple[Any, str]") -> None:
        # make sure settings are always associated to the root command (which derives from GenericCommand)
        if "GenericCommand" not in [x.__name__ for x in self.__class__.__bases__]:
            return
        key = self.__get_setting_name(name)
        if key in gef.config:
            # If the setting already exists, update the entry
            setting = gef.config.raw_entry(key)
            setting.value = value
            return

        # otherwise create it
        if isinstance(value, GefSetting):
            gef.config[key] = value
        else:
            if len(value) == 1:
                gef.config[key] = GefSetting(value[0])
            elif len(value) == 2:
                gef.config[key] = GefSetting(value[0], description=value[1])
        return

    @deprecated("Use `del self[setting_name]` instead")
    def del_setting(self, name: str) -> None:
        return self.__delitem__(name)

    def __delitem__(self, name: str) -> None:
        del gef.config[self.__get_setting_name(name)]
        return

    def __set_repeat_count(self, argv: list[str], from_tty: bool) -> None:
        if not from_tty:
            self.repeat = False
            self.repeat_count = 0
            return

        command = (gdb.execute("show commands", to_string=True) or "").strip().split("\n")[-1]
        self.repeat = self.__last_command == command
        self.repeat_count = self.repeat_count + 1 if self.repeat else 0
        self.__last_command = command
        return


@register
class ArchCommand(GenericCommand):
    """Manage the current loaded architecture."""

    _cmdline_ = "arch"
    _syntax_ = f"{_cmdline_} (list|get|set) ..."
    _example_ = f"{_cmdline_} set X86"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            self.usage()
        return

@register
class ArchGetCommand(GenericCommand):
    """Get the current loaded architecture."""

    _cmdline_ = "arch get"
    _syntax_ = f"{_cmdline_}"
    _example_ = f"{_cmdline_}"

    def do_invoke(self, args: list[str]) -> None:
        gef_print(f"{Color.greenify('Arch')}: {gef.arch}")
        gef_print(f"{Color.greenify('Reason')}: {gef.arch_reason}")


@register
class ArchSetCommand(GenericCommand):
    """Set the current loaded architecture."""

    _cmdline_ = "arch set"
    _syntax_ = f"{_cmdline_} <arch>"
    _example_ = f"{_cmdline_} X86"

    def do_invoke(self, args: list[str]) -> None:
        reset_architecture(args[0] if args else None)

    def complete(self, text: str, word: str) -> list[str]:
        return sorted(x for x in __registered_architectures__.keys() if
                       isinstance(x, str) and x.lower().startswith(text.lower().strip()))

@register
class ArchListCommand(GenericCommand):
    """List the available architectures."""

    _cmdline_ = "arch list"
    _syntax_ = f"{_cmdline_}"
    _example_ = f"{_cmdline_}"

    def do_invoke(self, args: list[str]) -> None:
        gef_print(Color.greenify("Available architectures:"))
        for arch in sorted(set(__registered_architectures__.values()), key=lambda x: x.arch):
            if arch is GenericArchitecture:
                continue

            gef_print(' ' + Color.yellowify(str(arch())))
            for alias in arch.aliases:
                if isinstance(alias, str):
                    gef_print(f"  {alias}")


@register
class VersionCommand(GenericCommand):
    """Display GEF version info."""

    _cmdline_ = "version"
    _syntax_ = f"{_cmdline_}"
    _example_ = f"{_cmdline_}"

    def do_invoke(self, argv: list[str]) -> None:
        gef_fpath = pathlib.Path(inspect.stack()[0][1]).expanduser().absolute()
        gef_dir = gef_fpath.parent
        gef_hash = hashlib.sha256(gef_fpath.read_bytes()).hexdigest()

        try:
            git = which("git")
        except FileNotFoundError:
            git = None

        if git:
            if (gef_dir / ".git").is_dir():
                ver = subprocess.check_output("git log --format='%H' -n 1 HEAD", cwd=gef_dir, shell=True).decode("utf8").strip()
                extra = "dirty" if len(subprocess.check_output("git ls-files -m", cwd=gef_dir, shell=True).decode("utf8").strip()) else "clean"
                gef_print(f"GEF: rev:{ver} (Git - {extra})")
            else:
                gef_blob_hash = subprocess.check_output(f"git hash-object {gef_fpath}", shell=True).decode().strip()
                gef_print("GEF: (Standalone)")
                gef_print(f"Blob Hash({gef_fpath}): {gef_blob_hash}")
        gef_print(f"SHA256({gef_fpath}): {gef_hash}")
        gef_print(f"GDB: {gdb.VERSION}")
        py_ver = f"{sys.version_info.major:d}.{sys.version_info.minor:d}"
        gef_print(f"GDB-Python: {py_ver}")

        if "full" in argv:
            gef_print(f"Loaded commands: {', '.join(gef.gdb.loaded_command_names)}")
        return


@register
class PrintFormatCommand(GenericCommand):
    """Print bytes format in commonly used formats, such as literals in high level languages."""

    valid_formats = ("py", "c", "js", "asm", "hex", "bytearray")
    valid_bitness = (8, 16, 32, 64)

    _cmdline_ = "print-format"
    _aliases_ = ["pf",]
    _syntax_  = (f"{_cmdline_} [--lang LANG] [--bitlen SIZE] [(--length,-l) LENGTH] [--clip] LOCATION"
                 f"\t--lang LANG specifies the output format for programming language (available: {valid_formats!s}, default 'py')."
                 f"\t--bitlen SIZE specifies size of bit (possible values: {valid_bitness!s}, default is 8)."
                 "\t--length LENGTH specifies length of array (default is 256)."
                 "\t--clip The output data will be copied to clipboard"
                 "\tLOCATION specifies where the address of bytes is stored.")
    _example_ = f"{_cmdline_} --lang py -l 16 $rsp"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        self["max_size_preview"] = (10, "max size preview of bytes")
        return

    @property
    def format_matrix(self) -> dict[int, tuple[str, str, str]]:
        # `gef.arch.endianness` is a runtime property, should not be defined as a class property
        return {
            8:  (f"{gef.arch.endianness}B", "char", "db"),
            16: (f"{gef.arch.endianness}H", "short", "dw"),
            32: (f"{gef.arch.endianness}I", "int", "dd"),
            64: (f"{gef.arch.endianness}Q", "long long", "dq"),
        }

    @only_if_gdb_running
    @parse_arguments({"location": "$pc", }, {("--length", "-l"): 256, "--bitlen": 0, "--lang": "py", "--clip": False,})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        """Default value for print-format command."""
        args: argparse.Namespace = kwargs["arguments"]
        args.bitlen = args.bitlen or gef.arch.ptrsize * 2

        valid_bitlens = self.format_matrix.keys()
        if args.bitlen not in valid_bitlens:
            err(f"Size of bit must be in: {valid_bitlens!s}")
            return

        if args.lang not in self.valid_formats:
            err(f"Language must be in: {self.valid_formats!s}")
            return

        start_addr = parse_address(args.location)
        size = int(args.bitlen / 8)
        end_addr = start_addr + args.length * size
        fmt = self.format_matrix[args.bitlen][0]
        data = []

        if args.lang != "bytearray":
            for addr in range(start_addr, end_addr, size):
                value = struct.unpack(fmt, gef.memory.read(addr, size))[0]
                data += [value]
            sdata = ", ".join(map(hex, data))
        else:
            sdata = ""

        if args.lang == "bytearray":
            data = gef.memory.read(start_addr, args.length)
            preview = str(data[0:self["max_size_preview"]])
            out = f"Saved data {preview}... in '{gef_convenience(data)}'"
        elif args.lang == "py":
            out = f"buf = [{sdata}]"
        elif args.lang == "c":
            c_type = self.format_matrix[args.bitlen][1]
            out = f"unsigned {c_type} buf[{args.length}] = {{{sdata}}};"
        elif args.lang == "js":
            out = f"var buf = [{sdata}]"
        elif args.lang == "asm":
            asm_type = self.format_matrix[args.bitlen][2]
            out = f"buf {asm_type} {sdata}"
        elif args.lang == "hex":
            out = gef.memory.read(start_addr, end_addr-start_addr).hex()
        else:
            raise ValueError(f"Invalid format: {args.lang}")

        if args.clip:
            if copy_to_clipboard(gef_pybytes(out)):
                info("Copied to clipboard")
            else:
                warn("There's a problem while copying")

        gef_print(out)
        return


@register
class PieCommand(GenericCommand):
    """PIE breakpoint support."""

    _cmdline_ = "pie"
    _syntax_ = f"{_cmdline_} (breakpoint|info|delete|run|attach|remote)"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            self.usage()
        return


@register
class PieBreakpointCommand(GenericCommand):
    """Set a PIE breakpoint at an offset from the target binaries base address."""

    _cmdline_ = "pie breakpoint"
    _syntax_ = f"{_cmdline_} OFFSET"

    @parse_arguments({"offset": ""}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not args.offset:
            self.usage()
            return

        addr = parse_address(args.offset)
        self.set_pie_breakpoint(lambda base: f"b *{base + addr}", addr)

        # When the process is already on, set real breakpoints immediately
        if is_alive():
            vmmap = gef.memory.maps
            base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0]
            for bp_ins in gef.session.pie_breakpoints.values():
                bp_ins.instantiate(base_address)
        return

    @staticmethod
    def set_pie_breakpoint(set_func: Callable[[int], str], addr: int) -> None:
        gef.session.pie_breakpoints[gef.session.pie_counter] = PieVirtualBreakpoint(set_func, gef.session.pie_counter, addr)
        gef.session.pie_counter += 1
        return


@register
class PieInfoCommand(GenericCommand):
    """Display breakpoint info."""

    _cmdline_ = "pie info"
    _syntax_ = f"{_cmdline_} BREAKPOINT"

    @parse_arguments({"breakpoints": [-1,]}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if args.breakpoints[0] == -1:
            # No breakpoint info needed
            bps = gef.session.pie_breakpoints.values()
        else:
            bps = [gef.session.pie_breakpoints[x]
                   for x in args.breakpoints
                   if x in gef.session.pie_breakpoints]

        lines = [f"{'VNum':6s}  {'Num':6s}  {'Addr':18s}"]
        lines += [
            f"{x.vbp_num:6d}  {str(x.bp_num) if x.bp_num else 'N/A':6s}  {x.addr:18s}" for x in bps
        ]
        gef_print("\n".join(lines))
        return


@register
class PieDeleteCommand(GenericCommand):
    """Delete a PIE breakpoint."""

    _cmdline_ = "pie delete"
    _syntax_ = f"{_cmdline_} [BREAKPOINT]"

    @parse_arguments({"breakpoints": [-1,]}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        global gef
        args : argparse.Namespace = kwargs["arguments"]
        if args.breakpoints[0] == -1:
            # no arg, delete all
            to_delete = list(gef.session.pie_breakpoints.values())
            self.delete_bp(to_delete)
        else:
            self.delete_bp([gef.session.pie_breakpoints[x]
                            for x in args.breakpoints
                            if x in gef.session.pie_breakpoints])
        return


    @staticmethod
    def delete_bp(breakpoints: list[PieVirtualBreakpoint]) -> None:
        global gef
        for bp in breakpoints:
            # delete current real breakpoints if exists
            if bp.bp_num:
                gdb.execute(f"delete {bp.bp_num}")
            # delete virtual breakpoints
            del gef.session.pie_breakpoints[bp.vbp_num]
        return


@register
class PieRunCommand(GenericCommand):
    """Run process with PIE breakpoint support."""

    _cmdline_ = "pie run"
    _syntax_ = _cmdline_

    def do_invoke(self, argv: list[str]) -> None:
        global gef
        fpath = get_filepath()
        if not fpath:
            warn("No executable to debug, use `file` to load a binary")
            return

        if not os.access(fpath, os.X_OK):
            warn(f"The file '{fpath}' is not executable.")
            return

        if is_alive():
            warn("gdb is already running. Restart process.")

        # get base address
        gdb.execute("set stop-on-solib-events 1")
        hide_context()
        gdb.execute(f"run {' '.join(argv)}")
        unhide_context()
        gdb.execute("set stop-on-solib-events 0")
        vmmap = gef.memory.maps
        base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0]
        info(f"base address {hex(base_address)}")

        # modify all breakpoints
        for bp_ins in gef.session.pie_breakpoints.values():
            bp_ins.instantiate(base_address)

        try:
            gdb.execute("continue")
        except gdb.error as e:
            err(str(e))
            gdb.execute("kill")
        return


@register
class PieAttachCommand(GenericCommand):
    """Do attach with PIE breakpoint support."""

    _cmdline_ = "pie attach"
    _syntax_ = f"{_cmdline_} PID"

    def do_invoke(self, argv: list[str]) -> None:
        try:
            gdb.execute(f"attach {' '.join(argv)}", to_string=True)
        except gdb.error as e:
            err(str(e))
            return
        # after attach, we are stopped so that we can
        # get base address to modify our breakpoint
        vmmap = gef.memory.maps
        base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0]

        for bp_ins in gef.session.pie_breakpoints.values():
            bp_ins.instantiate(base_address)
        gdb.execute("context")
        return


@register
class PieRemoteCommand(GenericCommand):
    """Attach to a remote connection with PIE breakpoint support."""

    _cmdline_ = "pie remote"
    _syntax_ = f"{_cmdline_} REMOTE"

    def do_invoke(self, argv: list[str]) -> None:
        try:
            gdb.execute(f"gef-remote {' '.join(argv)}")
        except gdb.error as e:
            err(str(e))
            return
        # after remote attach, we are stopped so that we can
        # get base address to modify our breakpoint
        vmmap = gef.memory.maps
        base_address = [x.page_start for x in vmmap if x.realpath == get_filepath()][0]

        for bp_ins in gef.session.pie_breakpoints.values():
            bp_ins.instantiate(base_address)
        gdb.execute("context")
        return


@register
class SmartEvalCommand(GenericCommand):
    """SmartEval: Smart eval (vague approach to mimic WinDBG `?`)."""

    _cmdline_ = "$"
    _syntax_  = f"{_cmdline_} EXPR\n{_cmdline_} ADDRESS1 ADDRESS2"
    _example_ = (f"\n{_cmdline_} $pc+1"
                 f"\n{_cmdline_} 0x00007ffff7a10000 0x00007ffff7bce000")

    def do_invoke(self, argv: list[str]) -> None:
        argc = len(argv)
        if argc == 1:
            self.evaluate(argv)
            return

        if argc == 2:
            self.distance(argv)
        return

    def evaluate(self, expr: list[str]) -> None:
        def show_as_int(i: int) -> None:
            off = gef.arch.ptrsize*8
            def comp2_x(x: Any) -> str: return f"{(x + (1 << off)) % (1 << off):x}"
            def comp2_b(x: Any) -> str: return f"{(x + (1 << off)) % (1 << off):b}"

            try:
                s_i = comp2_x(res)
                s_i = s_i.rjust(len(s_i)+1, "0") if len(s_i)%2 else s_i
                gef_print(f"{i:d}")
                gef_print("0x" + comp2_x(res))
                gef_print("0b" + comp2_b(res))
                gef_print(f"{binascii.unhexlify(s_i)}")
                gef_print(f"{binascii.unhexlify(s_i)[::-1]}")
            except Exception:
                pass
            return

        parsed_expr = []
        for xp in expr:
            try:
                xp = gdb.parse_and_eval(xp)
                xp = int(xp)
                parsed_expr.append(f"{xp:d}")
            except gdb.error:
                parsed_expr.append(str(xp))

        try:
            res = eval(" ".join(parsed_expr))
            if isinstance(res, int):
                show_as_int(res)
            else:
                gef_print(f"{res}")
        except SyntaxError:
            gef_print(" ".join(parsed_expr))
        return

    def distance(self, args: list[str]) -> None:
        try:
            x = int(args[0], 16) if is_hex(args[0]) else int(args[0])
            y = int(args[1], 16) if is_hex(args[1]) else int(args[1])
            gef_print(f"{abs(x - y)}")
        except ValueError:
            warn(f"Distance requires 2 numbers: {self._cmdline_} 0 0xffff")
        return


@register
class CanaryCommand(GenericCommand):
    """Shows the canary value of the current process."""

    _cmdline_ = "canary"
    _syntax_ = _cmdline_

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        self.dont_repeat()

        fname = get_filepath()
        assert fname
        has_canary = Elf(fname).checksec["Canary"]
        if not has_canary:
            warn("This binary was not compiled with SSP.")
            return

        res = gef.session.canary
        if not res:
            err("Failed to get the canary")
            return

        canary, location = res
        info(f"The canary of process {gef.session.pid} is at {location:#x}, value is {canary:#x}")
        return


@register
class ProcessStatusCommand(GenericCommand):
    """Extends the info given by GDB `info proc`, by giving an exhaustive description of the
    process status (file descriptors, ancestor, descendants, etc.)."""

    _cmdline_ = "process-status"
    _syntax_  = _cmdline_
    _aliases_ = ["status", ]

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_NONE)
        return

    @only_if_gdb_running
    @only_if_gdb_target_local
    def do_invoke(self, argv: list[str]) -> None:
        self.show_info_proc()
        self.show_ancestor()
        self.show_descendants()
        self.show_fds()
        self.show_connections()
        return

    def get_state_of(self, pid: int) -> dict[str, str]:
        res = {}
        with open(f"/proc/{pid}/status", "r") as f:
            file = f.readlines()
        for line in file:
            key, value = line.split(":", 1)
            res[key.strip()] = value.strip()
        return res

    def get_cmdline_of(self, pid: int) -> str:
        with open(f"/proc/{pid}/cmdline", "r") as f:
            return f.read().replace("\x00", "\x20").strip()

    def get_process_path_of(self, pid: int) -> str:
        return os.readlink(f"/proc/{pid}/exe")

    def get_children_pids(self, pid: int) -> list[int]:
        cmd = [gef.session.constants["ps"], "-o", "pid", "--ppid", f"{pid}", "--noheaders"]
        try:
            return [int(x) for x in gef_execute_external(cmd, as_list=True)]
        except Exception:
            return []

    def show_info_proc(self) -> None:
        info("Process Information")
        pid = gef.session.pid
        cmdline = self.get_cmdline_of(pid)
        gef_print(f"\tPID {RIGHT_ARROW} {pid}",
                  f"\tExecutable {RIGHT_ARROW} {self.get_process_path_of(pid)}",
                  f"\tCommand line {RIGHT_ARROW} '{cmdline}'", sep="\n")
        return

    def show_ancestor(self) -> None:
        info("Parent Process Information")
        ppid = int(self.get_state_of(gef.session.pid)["PPid"])
        state = self.get_state_of(ppid)
        cmdline = self.get_cmdline_of(ppid)
        gef_print(f"\tParent PID {RIGHT_ARROW} {state['Pid']}",
                  f"\tCommand line {RIGHT_ARROW} '{cmdline}'", sep="\n")
        return

    def show_descendants(self) -> None:
        info("Children Process Information")
        children = self.get_children_pids(gef.session.pid)
        if not children:
            gef_print("\tNo child process")
            return

        for child_pid in children:
            state = self.get_state_of(child_pid)
            pid = int(state["Pid"])
            gef_print(f"\tPID {RIGHT_ARROW} {pid} (Name: '{self.get_process_path_of(pid)}',"
                      f" CmdLine: '{self.get_cmdline_of(pid)}')")
            return

    def show_fds(self) -> None:
        pid = gef.session.pid
        path = f"/proc/{pid:d}/fd"

        info("File Descriptors:")
        items = os.listdir(path)
        if not items:
            gef_print("\tNo FD opened")
            return

        for fname in items:
            fullpath = os.path.join(path, fname)
            if os.path.islink(fullpath):
                gef_print(f"\t{fullpath} {RIGHT_ARROW} {os.readlink(fullpath)}")
        return

    def list_sockets(self, pid: int) -> list[int]:
        sockets = []
        path = f"/proc/{pid:d}/fd"
        items = os.listdir(path)
        for fname in items:
            fullpath = os.path.join(path, fname)
            if os.path.islink(fullpath) and os.readlink(fullpath).startswith("socket:"):
                p = os.readlink(fullpath).replace("socket:", "")[1:-1]
                sockets.append(int(p))
        return sockets

    def parse_ip_port(self, addr: str) -> tuple[str, int]:
        ip, port = addr.split(":")
        return socket.inet_ntoa(struct.pack("<I", int(ip, 16))), int(port, 16)

    def show_connections(self) -> None:
        # https://github.com/torvalds/linux/blob/v4.7/include/net/tcp_states.h#L16
        tcp_states_str = {
            0x01: "TCP_ESTABLISHED",
            0x02: "TCP_SYN_SENT",
            0x03: "TCP_SYN_RECV",
            0x04: "TCP_FIN_WAIT1",
            0x05: "TCP_FIN_WAIT2",
            0x06: "TCP_TIME_WAIT",
            0x07: "TCP_CLOSE",
            0x08: "TCP_CLOSE_WAIT",
            0x09: "TCP_LAST_ACK",
            0x0A: "TCP_LISTEN",
            0x0B: "TCP_CLOSING",
            0x0C: "TCP_NEW_SYN_RECV",
        }

        udp_states_str = {
            0x07: "UDP_LISTEN",
        }

        info("Network Connections")
        pid = gef.session.pid
        sockets = self.list_sockets(pid)
        if not sockets:
            gef_print("\tNo open connections")
            return

        entries = dict()
        with open(f"/proc/{pid:d}/net/tcp", "r") as tcp:
            entries["TCP"] = [x.split() for x in tcp.readlines()[1:]]
        with open(f"/proc/{pid:d}/net/udp", "r") as udp:
            entries["UDP"] = [x.split() for x in udp.readlines()[1:]]

        for proto in entries:
            for entry in entries[proto]:
                local, remote, state = entry[1:4]
                inode = int(entry[9])
                if inode in sockets:
                    local = self.parse_ip_port(local)
                    remote = self.parse_ip_port(remote)
                    state = int(state, 16)
                    state_str = tcp_states_str[state] if proto == "TCP" else udp_states_str[state]

                    gef_print(f"\t{local[0]}:{local[1]} {RIGHT_ARROW} {remote[0]}:{remote[1]} ({state_str})")
        return


@register
class GefThemeCommand(GenericCommand):
    """Customize GEF appearance."""

    _cmdline_ = "theme"
    _syntax_ = f"{_cmdline_} [KEY [VALUE]]"
    _example_ = (f"{_cmdline_} address_stack green")

    def __init__(self) -> None:
        super().__init__(self._cmdline_)
        self["context_title_line"] = ("gray", "Color of the borders in context window")
        self["context_title_message"] = ("cyan", "Color of the title in context window")
        self["default_title_line"] = ("gray", "Default color of borders")
        self["default_title_message"] = ("cyan", "Default color of title")
        self["table_heading"] = ("blue", "Color of the column headings to tables (e.g. vmmap)")
        self["old_context"] = ("gray", "Color to use to show things such as code that is not immediately relevant")
        self["disassemble_current_instruction"] = ("green", "Color to use to highlight the current $pc when disassembling")
        self["dereference_string"] = ("yellow", "Color of dereferenced string")
        self["dereference_code"] = ("gray", "Color of dereferenced code")
        self["dereference_base_address"] = ("cyan", "Color of dereferenced address")
        self["dereference_register_value"] = ("bold blue", "Color of dereferenced register")
        self["registers_register_name"] = ("blue", "Color of the register name in the register window")
        self["registers_value_changed"] = ("bold red", "Color of the changed register in the register window")
        self["address_stack"] = ("pink", "Color to use when a stack address is found")
        self["address_heap"] = ("green", "Color to use when a heap address is found")
        self["address_code"] = ("red", "Color to use when a code address is found")
        self["source_current_line"] = ("green", "Color to use for the current code line in the source window")
        return

    def do_invoke(self, args: list[str]) -> None:
        self.dont_repeat()
        argc = len(args)

        if argc == 0:
            for key in self.settings:
                setting = self[key]
                value = Color.colorify(setting, setting)
                gef_print(f"{key:40s}: {value}")
            return

        setting_name = args[0]
        if setting_name not in self:
            err("Invalid key")
            return

        if argc == 1:
            value = self[setting_name]
            gef_print(f"{setting_name:40s}: {Color.colorify(value, value)}")
            return

        colors = (color for color in args[1:] if color in Color.colors)
        self[setting_name] = " ".join(colors) # type: ignore // this is valid since we overwrote __setitem__()


class ExternalStructureManager:
    class Structure:
        def __init__(self, manager: "ExternalStructureManager", mod_path: pathlib.Path, struct_name: str) -> None:
            self.manager = manager
            self.module_path = mod_path
            self.name = struct_name
            self.class_type = self.__get_structure_class()
            # if the symbol points to a class factory method and not a class
            if not hasattr(self.class_type, "_fields_") and callable(self.class_type):
                self.class_type = self.class_type(gef)
            return

        def __str__(self) -> str:
            return self.name

        def pprint(self) -> None:
            res: list[str] = []
            for _name, _type in self.class_type._fields_: # type: ignore
                size = ctypes.sizeof(_type)
                name = Color.colorify(_name, gef.config["pcustom.structure_name"])
                type = Color.colorify(_type.__name__, gef.config["pcustom.structure_type"])
                size = Color.colorify(hex(size), gef.config["pcustom.structure_size"])
                offset = Color.boldify(f"{getattr(self.class_type, _name).offset:04x}")
                res.append(f"{offset}   {name:32s}   {type:16s}  /* size={size} */")
            gef_print("\n".join(res))
            return

        def __get_structure_class(self) -> Type[ctypes.Structure]:
            """Returns a tuple of (class, instance) if modname!classname exists"""
            fpath = self.module_path
            spec = importlib.util.spec_from_file_location(fpath.stem, fpath)
            assert spec and spec.loader, "Failed to determine module specification"
            module = importlib.util.module_from_spec(spec)
            sys.modules[fpath.stem] = module
            spec.loader.exec_module(module)
            _class = getattr(module, self.name)
            return _class

        def apply_at(self, address: int, max_depth: int, depth: int = 0) -> None:
            """Apply (recursively if possible) the structure format to the given address."""
            if depth >= max_depth:
                warn("maximum recursion level reached")
                return

            # read the data at the specified address
            assert isinstance(self.class_type, type)
            _structure = self.class_type()
            _sizeof_structure = ctypes.sizeof(_structure)

            try:
                data = gef.memory.read(address, _sizeof_structure)
            except gdb.MemoryError:
                err(f"{' ' * depth}Cannot read memory {address:#x}")
                return

            # deserialize the data
            length = min(len(data), _sizeof_structure)
            ctypes.memmove(ctypes.addressof(_structure), data, length)

            # pretty print all the fields (and call recursively if possible)
            ptrsize = gef.arch.ptrsize
            unpack = u32 if ptrsize == 4 else u64
            for field in _structure._fields_:
                assert len(field) == 2
                _name, _type = field
                _value = getattr(_structure, _name)
                _offset = getattr(self.class_type, _name).offset

                if ((ptrsize == 4 and _type is ctypes.c_uint32)
                    or (ptrsize == 8 and _type is ctypes.c_uint64)
                    or (ptrsize == ctypes.sizeof(ctypes.c_void_p) and _type is ctypes.c_void_p)):
                    # try to dereference pointers
                    _value = RIGHT_ARROW.join(dereference_from(_value))

                line = f"{'  ' * depth}"
                line += f"{address:#x}+{_offset:#04x} {_name} : ".ljust(40)
                line += f"{_value} ({_type.__name__})"
                parsed_value = self.__get_ctypes_value(_structure, _name, _value)
                if parsed_value:
                    line += f"{RIGHT_ARROW} {parsed_value}"
                gef_print(line)

                if issubclass(_type, ctypes.Structure):
                    self.apply_at(address + _offset, max_depth, depth + 1)
                elif _type.__name__.startswith("LP_"):
                    # Pointer to a structure of a different type
                    __sub_type_name = _type.__name__.lstrip("LP_")
                    result = self.manager.find(__sub_type_name)
                    if result:
                        _, __structure = result
                        __address = unpack(gef.memory.read(address + _offset, ptrsize))
                        __structure.apply_at(__address, max_depth, depth + 1)
            return

        def __get_ctypes_value(self, struct, item, value) -> str:
            if not hasattr(struct, "_values_"): return ""
            default = ""
            for name, values in struct._values_:
                if name != item: continue
                if callable(values):
                    return str(values(value))
                try:
                    for val, desc in values:
                        if value == val: return desc
                        if val is None: default = desc
                except Exception as e:
                    err(f"Error parsing '{name}': {e}")
            return default

    class Module(dict):
        def __init__(self, manager: "ExternalStructureManager", path: pathlib.Path) -> None:
            self.manager = manager
            self.path = path
            self.name = path.stem
            self.raw = self.__load()

            for entry in self:
                structure = ExternalStructureManager.Structure(manager, self.path, entry)
                self[structure.name] = structure
            return

        def __load(self) -> ModuleType:
            """Load a custom module, and return it."""
            fpath = self.path
            spec = importlib.util.spec_from_file_location(fpath.stem, fpath)
            assert spec and spec.loader
            module = importlib.util.module_from_spec(spec)
            sys.modules[fpath.stem] = module
            spec.loader.exec_module(module)
            return module

        def __str__(self) -> str:
            return self.name

        def __iter__(self) -> Generator[str, None, None]:
            _invalid = {"BigEndianStructure", "LittleEndianStructure", "Structure"}
            for x in dir(self.raw):
                if x in _invalid: continue
                _attr = getattr(self.raw, x)

                # if it's a ctypes.Structure class, add it
                if inspect.isclass(_attr) and issubclass(_attr, ctypes.Structure):
                    yield x
                    continue

                # also accept class factory functions
                if callable(_attr) and _attr.__module__ == self.name and x.endswith("_t"):
                    yield x
                    continue
            return

    class Modules(dict):
        def __init__(self, manager: "ExternalStructureManager") -> None:
            self.manager: "ExternalStructureManager" = manager
            self.root: pathlib.Path = manager.path

            for entry in self.root.iterdir():
                if not entry.is_file(): continue
                if entry.suffix != ".py": continue
                if entry.name == "__init__.py": continue
                module = ExternalStructureManager.Module(manager, entry)
                self[module.name] = module
            return

        def __contains__(self, structure_name: str) -> bool:
            """Return True if the structure name is found in any of the modules"""
            for module in self.values():
                if structure_name in module:
                    return True
            return False

    def __init__(self) -> None:
        self.clear_caches()
        return

    def clear_caches(self) -> None:
        self._path = None
        self._modules = None
        return

    @property
    def modules(self) -> "ExternalStructureManager.Modules":
        if not self._modules:
            self._modules = ExternalStructureManager.Modules(self)
        return self._modules

    @property
    def path(self) -> pathlib.Path:
        if not self._path:
            self._path = gef.config["pcustom.struct_path"].expanduser().absolute()
        return self._path

    @property
    def structures(self) -> Generator[tuple["ExternalStructureManager.Module", "ExternalStructureManager.Structure"], None, None]:
        for module in self.modules.values():
            for structure in module.values():
                yield module, structure
        return

    @lru_cache()
    def find(self, structure_name: str) -> tuple["ExternalStructureManager.Module", "ExternalStructureManager.Structure"] | None:
        """Return the module and structure for the given structure name; `None` if the structure name was not found."""
        for module in self.modules.values():
            if structure_name in module:
                return module, module[structure_name]
        return None


@register
class PCustomCommand(GenericCommand):
    """Dump user defined structure.
    This command attempts to reproduce WinDBG awesome `dt` command for GDB and allows
    to apply structures (from symbols or custom) directly to an address.
    Custom structures can be defined in pure Python using ctypes, and should be stored
    in a specific directory, whose path must be stored in the `pcustom.struct_path`
    configuration setting."""

    _cmdline_ = "pcustom"
    _syntax_  = f"{_cmdline_} [list|edit <StructureName>|show <StructureName>]|<StructureName> 0xADDRESS]"

    def __init__(self) -> None:
        global gef
        super().__init__(prefix=True)
        self["max_depth"] = (4, "Maximum level of recursion supported")
        self["structure_name"] = ("bold blue", "Color of the structure name")
        self["structure_type"] = ("bold red", "Color of the attribute type")
        self["structure_size"] = ("green", "Color of the attribute size")
        gef.config[f"{self._cmdline_}.struct_path"] = GefSetting( gef.config["gef.tempdir"] / "structs", pathlib.Path,
                                                       "Path to store/load the structure ctypes files",
                                                       hooks={"on_write": [GefSetting.create_folder_tree,]})
        return

    @parse_arguments({"type": "", "address": ""}, {})
    def do_invoke(self, *_: Any, **kwargs: dict[str, Any]) -> None:
        args = cast(argparse.Namespace, kwargs["arguments"])
        if not args.type:
            gdb.execute("pcustom list")
            return

        structname = self.explode_type(args.type)[1]

        if not args.address:
            gdb.execute(f"pcustom show {structname}")
            return

        if not is_alive():
            err("Session is not active")
            return

        manager = ExternalStructureManager()
        address = parse_address(args.address)
        result = manager.find(structname)
        if not result:
            err(f"No structure named '{structname}' found")
            return

        structure = result[1]
        structure.apply_at(address, self["max_depth"])
        return

    def explode_type(self, arg: str) -> tuple[str, str]:
        modname, structname = arg.split(":", 1) if ":" in arg else (arg, arg)
        structname = structname.split(".", 1)[0] if "." in structname else structname
        return modname, structname


@register
class PCustomListCommand(PCustomCommand):
    """PCustom: list available structures"""

    _cmdline_ = "pcustom list"
    _syntax_ = f"{_cmdline_}"

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, _: list[str]) -> None:
        """Dump the list of all the structures and their respective."""
        manager = ExternalStructureManager()
        info(f"Listing custom structures from '{manager.path}'")
        struct_color = gef.config["pcustom.structure_type"]
        filename_color = gef.config["pcustom.structure_name"]
        for module in manager.modules.values():
            __modules = ", ".join([Color.colorify(str(structure), struct_color) for structure in module.values()])
            __filename = Color.colorify(str(module.path), filename_color)
            gef_print(f"{RIGHT_ARROW} {__filename} ({__modules})")
        return


@register
class PCustomShowCommand(PCustomCommand):
    """PCustom: show the content of a given structure"""

    _cmdline_ = "pcustom show"
    _syntax_ = f"{_cmdline_} StructureName"
    _aliases_ = ["pcustom create", "pcustom update"]

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) == 0:
            self.usage()
            return

        _, structname = self.explode_type(argv[0])
        manager = ExternalStructureManager()
        result = manager.find(structname)
        if result:
            _, structure = result
            structure.pprint()
        else:
            err(f"No structure named '{structname}' found")
        return


@register
class PCustomEditCommand(PCustomCommand):
    """PCustom: edit the content of a given structure"""

    _cmdline_ = "pcustom edit"
    _syntax_ = f"{_cmdline_} StructureName"
    __aliases__ = ["pcustom create", "pcustom new", "pcustom update"]

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) == 0:
            self.usage()
            return

        modname, structname = self.explode_type(argv[0])
        self.__create_or_edit_structure(modname, structname)
        return

    def __create_or_edit_structure(self, mod_name: str, struct_name: str) -> int:
        path = gef.config["pcustom.struct_path"].expanduser() / f"{mod_name}.py"
        if path.is_file():
            info(f"Editing '{path}'")
        else:
            ok(f"Creating '{path}' from template")
            self.__create_template(struct_name, path)

        cmd = (os.getenv("EDITOR") or "nano").split()
        cmd.append(str(path.absolute()))
        return subprocess.call(cmd)

    def __create_template(self, structname: str, fpath: pathlib.Path) -> None:
        template = f"""from ctypes import *

class {structname}(Structure):
    _fields_ = []

    _values_ = []
"""
        with fpath.open("w") as f:
            f.write(template)
        return


@register
class ChangeFdCommand(GenericCommand):
    """ChangeFdCommand: redirect file descriptor during runtime."""

    _cmdline_ = "hijack-fd"
    _syntax_ = f"{_cmdline_} FD_NUM NEW_OUTPUT"
    _example_ = f"{_cmdline_} 2 /tmp/stderr_output.txt"

    @only_if_gdb_running
    @only_if_gdb_target_local
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) != 2:
            self.usage()
            return

        if not os.access(f"/proc/{gef.session.pid:d}/fd/{argv[0]}", os.R_OK):
            self.usage()
            return

        old_fd = int(argv[0])
        new_output = argv[1]

        if ":" in new_output:
            address = socket.gethostbyname(new_output.split(":")[0])
            port = int(new_output.split(":")[1])

            AF_INET = 2
            SOCK_STREAM = 1
            res = gdb.execute(f"call (int)socket({AF_INET}, {SOCK_STREAM}, 0)", to_string=True) or ""
            new_fd = self.get_fd_from_result(res)

            # fill in memory with sockaddr_in struct contents
            # we will do this in the stack, since connect() wants a pointer to a struct
            vmmap = gef.memory.maps
            stack_addr = [entry.page_start for entry in vmmap if entry.path == "[stack]"][0]
            original_contents = gef.memory.read(stack_addr, 8)

            gef.memory.write(stack_addr, b"\x02\x00", 2)
            gef.memory.write(stack_addr + 0x2, struct.pack("<H", socket.htons(port)), 2)
            gef.memory.write(stack_addr + 0x4, socket.inet_aton(address), 4)

            info(f"Trying to connect to {new_output}")
            res = gdb.execute(f"""call (int)connect({new_fd}, {stack_addr}, {16})""", to_string=True)
            if res is None:
                err("Call to `connect` failed")
                return

            # recover stack state
            gef.memory.write(stack_addr, original_contents, 8)

            res = self.get_fd_from_result(res)
            if res == -1:
                err(f"Failed to connect to {address}:{port}")
                return

            info(f"Connected to {new_output}")
        else:
            res = gdb.execute(f"""call (int)open("{new_output}", 66, 0666)""", to_string=True)
            if res is None:
                err("Call to `open` failed")
                return
            new_fd = self.get_fd_from_result(res)

        info(f"Opened '{new_output}' as fd #{new_fd:d}")
        gdb.execute(f"""call (int)dup2({new_fd:d}, {old_fd:d})""", to_string=True)
        info(f"Duplicated fd #{new_fd:d}{RIGHT_ARROW}#{old_fd:d}")
        gdb.execute(f"""call (int)close({new_fd:d})""", to_string=True)
        info(f"Closed extra fd #{new_fd:d}")
        ok("Success")
        return

    def get_fd_from_result(self, res: str) -> int:
        # Output example: $1 = 3
        res = gdb.execute(f"p/d {int(res.split()[2], 0)}", to_string=True) or ""
        return int(res.split()[2], 0)


@register
class ScanSectionCommand(GenericCommand):
    """Search for addresses that are located in a memory mapping (haystack) that belonging
    to another (needle)."""

    _cmdline_ = "scan"
    _syntax_  = f"{_cmdline_} HAYSTACK NEEDLE"
    _aliases_ = ["lookup",]
    _example_ = f"\n{_cmdline_} stack libc"

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) != 2:
            self.usage()
            return

        haystack = argv[0]
        needle = argv[1]

        info(f"Searching for addresses in '{Color.yellowify(haystack)}' "
             f"that point to '{Color.yellowify(needle)}'")

        fpath = get_filepath() or ""

        if haystack == "binary":
            haystack = fpath

        if needle == "binary":
            needle = fpath

        needle_sections = []
        haystack_sections = []

        if "0x" in haystack:
            start, end = parse_string_range(haystack)
            haystack_sections.append((start, end, ""))

        if "0x" in needle:
            start, end = parse_string_range(needle)
            needle_sections.append((start, end))

        for sect in gef.memory.maps:
            if sect.path is None:
                continue
            if haystack in sect.path:
                haystack_sections.append((sect.page_start, sect.page_end, os.path.basename(sect.path)))
            if needle in sect.path:
                needle_sections.append((sect.page_start, sect.page_end))

        step = gef.arch.ptrsize
        unpack = u32 if step == 4 else u64

        dereference_cmd = gef.gdb.commands["dereference"]
        assert isinstance(dereference_cmd, DereferenceCommand)

        for hstart, hend, hname in haystack_sections:
            try:
                mem = gef.memory.read(hstart, hend - hstart)
            except gdb.MemoryError:
                continue

            for i in range(0, len(mem), step):
                target = unpack(mem[i:i+step])
                for nstart, nend in needle_sections:
                    if target >= nstart and target < nend:
                        deref = dereference_cmd.pprint_dereferenced(hstart, int(i / step))
                        if hname != "":
                            name = Color.colorify(hname, "yellow")
                            gef_print(f"{name}: {deref}")
                        else:
                            gef_print(f" {deref}")

        return


@register
class SearchPatternCommand(GenericCommand):
    """SearchPatternCommand: search a pattern in memory. If given an hex value (starting with 0x)
    the command will also try to look for upwards cross-references to this address."""

    _cmdline_ = "search-pattern"
    _syntax_ = f"{_cmdline_} PATTERN [little|big] [section]"
    _aliases_ = ["grep", "xref"]
    _example_ = [f"{_cmdline_} AAAAAAAA",
                 f"{_cmdline_} 0x555555554000 little stack",
                 f"{_cmdline_} AAAA 0x600000-0x601000",
                 f"{_cmdline_} --regex 0x401000 0x401500 ([\\\\x20-\\\\x7E]{{2,}})(?=\\\\x00)   <-- It matches null-end-printable(from x20-x7e) C strings (min size 2 bytes)"]

    def __init__(self) -> None:
        super().__init__()
        self["max_size_preview"] = (10, "max size preview of bytes")
        self["nr_pages_chunk"] = (0x400, "number of pages readed for each memory read chunk")
        return

    def print_section(self, section: Section) -> None:
        title = "In "
        if section.path:
            title += f"'{Color.blueify(section.path)}'"

        title += f"({section.page_start:#x}-{section.page_end:#x})"
        title += f", permission={section.permission}"
        ok(title)
        return

    def print_loc(self, loc: tuple[int, int, str]) -> None:
        gef_print(f"""  {loc[0]:#x} - {loc[1]:#x} {RIGHT_ARROW}  "{Color.pinkify(loc[2])}" """)
        return

    def search_pattern_by_address(self, pattern: str, start_address: int, end_address: int) -> list[tuple[int, int, str]]:
        """Search a pattern within a range defined by arguments."""
        _pattern = gef_pybytes(pattern)
        step = self["nr_pages_chunk"] * gef.session.pagesize
        locations = []

        for chunk_addr in range(start_address, end_address, step):
            if chunk_addr + step > end_address:
                chunk_size = end_address - chunk_addr
            else:
                chunk_size = step

            try:
                mem = gef.memory.read(chunk_addr, chunk_size)
            except gdb.MemoryError:
                return []

            for match in re.finditer(_pattern, mem):
                start = chunk_addr + match.start()
                ustr = ""
                if is_ascii_string(start):
                    ustr = gef.memory.read_ascii_string(start) or ""
                    end = start + len(ustr)
                else:
                    ustr = gef_pystring(_pattern) + "[...]"
                    end = start + len(_pattern)
                locations.append((start, end, ustr))

            del mem

        return locations

    def search_binpattern_by_address(self, binpattern: bytes, start_address: int, end_address: int) -> list[tuple[int, int, str]]:
        """Search a binary pattern within a range defined by arguments."""

        step = self["nr_pages_chunk"] * gef.session.pagesize
        locations = []

        for chunk_addr in range(start_address, end_address, step):
            if chunk_addr + step > end_address:
                chunk_size = end_address - chunk_addr
            else:
                chunk_size = step

            try:
                mem = gef.memory.read(chunk_addr, chunk_size)
            except gdb.MemoryError:
                return []
            preview_size = self["max_size_preview"]
            preview = ""
            for match in re.finditer(binpattern, mem):
                start = chunk_addr + match.start()
                preview = str(mem[slice(*match.span())][0:preview_size]) + "..."
                size_match = match.span()[1] - match.span()[0]
                if size_match > 0:
                    size_match -= 1
                end = start + size_match
                locations.append((start, end, preview))

            del mem

        return locations

    def search_pattern(self, pattern: str, section_name: str) -> None:
        """Search a pattern within the whole userland memory."""
        for section in gef.memory.maps:
            if not section.permission & Permission.READ: continue
            if section.path == "[vvar]": continue
            if section_name not in section.path: continue

            start = section.page_start
            end = section.page_end - 1
            old_section = None

            for loc in self.search_pattern_by_address(pattern, start, end):
                addr_loc_start = lookup_address(loc[0])
                if addr_loc_start and addr_loc_start.section:
                    if old_section != addr_loc_start.section:
                        self.print_section(addr_loc_start.section)
                        old_section = addr_loc_start.section

                self.print_loc(loc)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        argc = len(argv)
        if argc < 1:
            self.usage()
            return

        if argc > 3 and argv[0].startswith("--regex"):
            pattern = ' '.join(argv[3:])
            pattern = ast.literal_eval("b'" + pattern + "'")

            addr_start = parse_address(argv[1])
            addr_end = parse_address(argv[2])

            for loc in self.search_binpattern_by_address(pattern, addr_start, addr_end):
                self.print_loc(loc)

            return

        pattern = argv[0]
        endian = gef.arch.endianness

        if argc >= 2:
            if argv[1].lower() == "big": endian = Endianness.BIG_ENDIAN
            elif argv[1].lower() == "little": endian = Endianness.LITTLE_ENDIAN

        if is_hex(pattern):
            if endian == Endianness.BIG_ENDIAN:
                pattern = "".join(["\\x" + pattern[i:i + 2] for i in range(2, len(pattern), 2)])
            else:
                pattern = "".join(["\\x" + pattern[i:i + 2] for i in range(len(pattern) - 2, 0, -2)])

        if argc == 3:
            info(f"Searching '{Color.yellowify(pattern)}' in {argv[2]}")

            if "0x" in argv[2]:
                start, end = parse_string_range(argv[2])

                loc = lookup_address(start)
                if loc.valid:
                    self.print_section(loc.section)

                for loc in self.search_pattern_by_address(pattern, start, end):
                    self.print_loc(loc)
            else:
                section_name = argv[2]
                if section_name == "binary":
                    section_name = get_filepath() or ""

                self.search_pattern(pattern, section_name)
        else:
            info(f"Searching '{Color.yellowify(pattern)}' in memory")
            self.search_pattern(pattern, "")
        return


@register
class FlagsCommand(GenericCommand):
    """Edit flags in a human friendly way."""

    _cmdline_ = "edit-flags"
    _syntax_  = f"{_cmdline_} [(+|-|~)FLAGNAME ...]"
    _aliases_ = ["flags",]
    _example_ = (f"\n{_cmdline_}"
                 f"\n{_cmdline_} +zero # sets ZERO flag")

    def do_invoke(self, argv: list[str]) -> None:
        if not gef.arch.flag_register:
            warn(f"The architecture {gef.arch.arch}:{gef.arch.mode} doesn't have flag register.")
            return

        for flag in argv:
            if len(flag) < 2:
                continue

            action = flag[0]
            name = flag[1:].lower()

            if action not in ("+", "-", "~"):
                err(f"Invalid action for flag '{flag}'")
                continue

            if name not in gef.arch.flags_table.values():
                err(f"Invalid flag name '{flag[1:]}'")
                continue

            for off in gef.arch.flags_table:
                if gef.arch.flags_table[off] != name:
                    continue
                old_flag = gef.arch.register(gef.arch.flag_register)
                if action == "+":
                    new_flags = old_flag | (1 << off)
                elif action == "-":
                    new_flags = old_flag & ~(1 << off)
                else:
                    new_flags = old_flag ^ (1 << off)

                gdb.execute(f"set ({gef.arch.flag_register}) = {new_flags:#x}")

        gef_print(gef.arch.flag_register_to_human())
        return


@register
class RemoteCommand(GenericCommand):
    """GDB `target remote` command on steroids. This command will use the remote procfs to create
    a local copy of the execution environment, including the target binary and its libraries
    in the local temporary directory (the value by default is in `gef.config.tempdir`). Additionally, it
    will fetch all the /proc/PID/maps and loads all its information. If procfs is not available remotely, the command
    will likely fail. You can however still use the limited command provided by GDB `target remote`."""

    _cmdline_ = "gef-remote"
    _syntax_  = f"{_cmdline_} [OPTIONS] TARGET"
    _example_  = [f"{_cmdline_} localhost 1234",
                  f"{_cmdline_} --pid 6789 localhost 1234",
                  f"{_cmdline_} --qemu-user --qemu-binary /bin/debugme localhost 4444 "]

    def __init__(self) -> None:
        super().__init__(prefix=False)
        return

    @parse_arguments({"host": "", "port": 0}, {"--pid": -1, "--qemu-user": False, "--qemu-binary": ""})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        if gef.session.remote is not None:
            err("You already are in remote session. Close it first before opening a new one...")
            return

        # argument check
        args : argparse.Namespace = kwargs["arguments"]
        if not args.host or not args.port:
            err("Missing parameters")
            return

        # qemu-user support
        qemu_binary: pathlib.Path | None = None
        if args.qemu_user:
            try:
                qemu_binary = pathlib.Path(args.qemu_binary).expanduser().absolute() if args.qemu_binary else gef.session.file
                if not qemu_binary or not qemu_binary.exists():
                    raise FileNotFoundError(f"{qemu_binary} does not exist")
            except Exception as e:
                err(f"Failed to initialize qemu-user mode, reason: {str(e)}")
                return

        # Try to establish the remote session, throw on error
        # Set `.remote_initializing` to True here - `GefRemoteSessionManager` invokes code which
        # calls `is_remote_debug` which checks if `remote_initializing` is True or `.remote` is None
        # This prevents some spurious errors being thrown during startup
        gef.session.remote_initializing = True
        session = GefRemoteSessionManager(args.host, args.port, args.pid, qemu_binary)

        dbg(f"[remote] initializing remote session with {session.target} under {session.root}")
        if not session.connect(args.pid) or not session.setup():
            gef.session.remote = None
            gef.session.remote_initializing = False
            raise EnvironmentError("Failed to setup remote target")

        gef.session.remote_initializing = False
        gef.session.remote = session
        reset_all_caches()
        gdb.execute("context")
        return


@register
class SkipiCommand(GenericCommand):
    """Skip N instruction(s) execution"""

    _cmdline_ = "skipi"
    _syntax_  = (f"{_cmdline_} [LOCATION] [--n NUM_INSTRUCTIONS]"
                "\n\tLOCATION\taddress/symbol from where to skip"
                 "\t--n NUM_INSTRUCTIONS\tSkip the specified number of instructions instead of the default 1.")

    _example_ = [f"{_cmdline_}",
                 f"{_cmdline_} --n 3",
                 f"{_cmdline_} 0x69696969",
                 f"{_cmdline_} 0x69696969 --n 6",]

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    @parse_arguments({"address": "$pc"}, {"--n": 1})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        address = parse_address(args.address)
        num_instructions = args.n

        last_insn = gef_instruction_n(address, num_instructions-1)
        total_bytes = (last_insn.address - address) + last_insn.size()
        target_addr  = address + total_bytes

        info(f"skipping {num_instructions} instructions ({total_bytes} bytes) from {address:#x} to {target_addr:#x}")
        gdb.execute(f"set $pc = {target_addr:#x}")
        return


@register
class StepoverCommand(GenericCommand):
    """Breaks on the instruction immediately following this one. Ex: Step over call instruction"""

    _cmdline_ = "stepover"
    _syntax_  = (f"{_cmdline_}"
                "\n\tBreaks on the instruction immediately following this one. Ex: Step over call instruction.")
    _aliases_ = ["so",]
    _example_ = [f"{_cmdline_}",]

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, _: list[str]) -> None:
        target_addr = gef_next_instruction(parse_address("$pc")).address
        JustSilentStopBreakpoint("".join(["*",  str(target_addr)]))
        gdb.execute("continue")
        return


@register
class NopCommand(GenericCommand):
    """Patch the instruction(s) pointed by parameters with NOP. Note: this command is architecture
    aware."""

    _cmdline_ = "nop"
    _syntax_  = (f"{_cmdline_} [LOCATION] [--i ITEMS] [--f] [--n] [--b]"
                 "\n\tLOCATION\taddress/symbol to patch (by default this command replaces whole instructions)"
                 "\t--i ITEMS\tnumber of items to insert (default 1)"
                 "\t--f\tForce patch even when the selected settings could overwrite partial instructions"
                 "\t--n\tInstead of replacing whole instructions, insert ITEMS nop instructions, no matter how many instructions it overwrites"
                 "\t--b\tInstead of replacing whole instructions, fill ITEMS bytes with nops")
    _example_ = [f"{_cmdline_}",
                 f"{_cmdline_} $pc+3",
                 f"{_cmdline_} --i 2 $pc+3",
                 f"{_cmdline_} --b",
                 f"{_cmdline_} --b $pc+3",
                 f"{_cmdline_} --f --b --i 2 $pc+3"
                 f"{_cmdline_} --n --i 2 $pc+3",]

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    @parse_arguments({"address": "$pc"}, {"--i": 1, "--b": False, "--f": False, "--n": False})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        address = parse_address(args.address)
        nop = gef.arch.nop_insn
        num_items = int(args.i) or 1
        fill_bytes = bool(args.b)
        fill_nops = bool(args.n)
        force_flag = bool(args.f) or False

        if fill_nops and fill_bytes:
            err("--b and --n cannot be specified at the same time.")
            return

        total_bytes = 0
        if fill_bytes:
            total_bytes = num_items
        elif fill_nops:
            total_bytes = num_items * len(nop)
        else:
            try:
                last_insn = gef_instruction_n(address, num_items-1)
                last_addr = last_insn.address
            except Exception as e:
                err(f"Cannot patch instruction at {address:#x} reaching unmapped area, reason: {e}")
                return
            total_bytes = (last_addr - address) + gef_get_instruction_at(last_addr).size()

        if len(nop) > total_bytes or total_bytes % len(nop):
            warn(f"Patching {total_bytes} bytes at {address:#x} will result in LAST-NOP "
                 f"(byte nr {total_bytes % len(nop):#x}) broken and may cause a crash or "
                 "break disassembly.")
            if not force_flag:
                warn("Use --f (force) to ignore this warning.")
                return

        target_end_address = address + total_bytes
        curr_ins = gef_current_instruction(address)
        while curr_ins.address + curr_ins.size() < target_end_address:
            if not Address(value=curr_ins.address + 1).valid:
                err(f"Cannot patch instruction at {address:#x}: reaching unmapped area")
                return
            curr_ins = gef_next_instruction(curr_ins.address)

        final_ins_end_addr = curr_ins.address + curr_ins.size()

        if final_ins_end_addr != target_end_address:
            warn(f"Patching {total_bytes} bytes at {address:#x} will result in LAST-INSTRUCTION "
                 f"({curr_ins.address:#x}) being partial overwritten and may cause a crash or "
                 "break disassembly.")
            if not force_flag:
                warn("Use --f (force) to ignore this warning.")
                return

        nops = bytearray(nop * total_bytes)
        end_address = Address(value=address + total_bytes - 1)
        if not end_address.valid:
            err(f"Cannot patch instruction at {address:#x}: reaching unmapped "
                f"area: {end_address:#x}")
            return

        ok(f"Patching {total_bytes} bytes from {address:#x}")
        gef.memory.write(address, nops, total_bytes)

        return


@register
class StubCommand(GenericCommand):
    """Stub out the specified function. This function is useful when needing to skip one
    function to be called and disrupt your runtime flow (ex. fork)."""

    _cmdline_ = "stub"
    _syntax_  = (f"{_cmdline_} [--retval RETVAL] [address]"
                 "\taddress\taddress/symbol to stub out"
                 "\t--retval RETVAL\tSet the return value")
    _example_ = f"{_cmdline_} --retval 0 fork"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    @parse_arguments({"address": ""}, {("-r", "--retval"): 0})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        loc = args.address if args.address else f"*{gef.arch.pc:#x}"
        StubBreakpoint(loc, args.retval)
        return


@register
class GlibcHeapCommand(GenericCommand):
    """Base command to get information about the Glibc heap structure."""

    _cmdline_ = "heap"
    _syntax_  = f"{_cmdline_} (chunk|chunks|bins|arenas|set-arena)"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    @only_if_gdb_running
    def do_invoke(self, _: list[str]) -> None:
        self.usage()
        return


@register
class GlibcHeapSetArenaCommand(GenericCommand):
    """Set the address of the main_arena or the currently selected arena."""

    _cmdline_ = "heap set-arena"
    _syntax_  = f"{_cmdline_} [address|&symbol]"
    _example_ = f"{_cmdline_} 0x001337001337"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    @parse_arguments({"addr": ""}, {"--reset": False})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        global gef

        args: argparse.Namespace = kwargs["arguments"]

        if args.reset:
            gef.heap.reset_caches()
            return

        if not args.addr:
            ok(f"Current arena set to: '{gef.heap.selected_arena}'")
            return

        try:
            new_arena_address = parse_address(args.addr)
        except gdb.error:
            err("Invalid symbol for arena")
            return

        new_arena = GlibcArena( f"*{new_arena_address:#x}")
        if new_arena in gef.heap.arenas:
            # if entered arena is in arena list then just select it
            gef.heap.selected_arena = new_arena
        else:
            # otherwise set the main arena to the entered arena
            gef.heap.main_arena = new_arena
        return


@register
class GlibcHeapArenaCommand(GenericCommand):
    """Display information on a heap chunk."""

    _cmdline_ = "heap arenas"
    _syntax_  = _cmdline_

    @only_if_gdb_running
    def do_invoke(self, _: list[str]) -> None:
        for arena in gef.heap.arenas:
            gef_print(str(arena))
        return


@register
class GlibcHeapChunkCommand(GenericCommand):
    """Display information on a heap chunk.
    See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123."""

    _cmdline_ = "heap chunk"
    _syntax_  = f"{_cmdline_} [-h] [--allow-unaligned] [--number] address"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({"address": ""}, {"--allow-unaligned": False, "--number": 1})
    @only_if_gdb_running
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not args.address:
            err("Missing chunk address")
            self.usage()
            return

        addr = parse_address(args.address)
        current_chunk = GlibcChunk(addr, allow_unaligned=args.allow_unaligned)

        if args.number > 1:
            for _i in range(args.number):
                if current_chunk.size == 0:
                    break

                gef_print(str(current_chunk))
                next_chunk_addr = current_chunk.get_next_chunk_addr()
                if not Address(value=next_chunk_addr).valid:
                    break

                next_chunk = current_chunk.get_next_chunk()
                if next_chunk is None:
                    break

                current_chunk = next_chunk
        else:
            gef_print(current_chunk.psprint())
        return


class GlibcHeapChunkSummary:
    def __init__(self, desc = ""):
        self.desc = desc
        self.count = 0
        self.total_bytes = 0

    def process_chunk(self, chunk: GlibcChunk) -> None:
        self.count += 1
        self.total_bytes += chunk.size


class GlibcHeapArenaSummary:
    def __init__(self, resolve_type = False) -> None:
        self.resolve_symbol = resolve_type
        self.size_distribution = {}
        self.flag_distribution = {
            "PREV_INUSE": GlibcHeapChunkSummary(),
            "IS_MMAPPED": GlibcHeapChunkSummary(),
            "NON_MAIN_ARENA": GlibcHeapChunkSummary()
        }

    def process_chunk(self, chunk: GlibcChunk) -> None:
        chunk_type = "" if not self.resolve_symbol else chunk.resolve_type()

        per_size_summary = self.size_distribution.get((chunk.size, chunk_type), None)
        if per_size_summary is None:
            per_size_summary = GlibcHeapChunkSummary(desc=chunk_type)
            self.size_distribution[(chunk.size, chunk_type)] = per_size_summary
        per_size_summary.process_chunk(chunk)

        if chunk.has_p_bit():
            self.flag_distribution["PREV_INUSE"].process_chunk(chunk)
        if chunk.has_m_bit():
            self.flag_distribution["IS_MAPPED"].process_chunk(chunk)
        if chunk.has_n_bit():
            self.flag_distribution["NON_MAIN_ARENA"].process_chunk(chunk)

    def print(self) -> None:
        gef_print("== Chunk distribution by size ==")
        gef_print(f"{'ChunkBytes':<10s}\t{'Count':<10s}\t{'TotalBytes':15s}\t{'Description':s}")
        for chunk_info, chunk_summary in sorted(self.size_distribution.items(), key=lambda x: x[1].total_bytes, reverse=True):
            gef_print(f"{chunk_info[0]:<10d}\t{chunk_summary.count:<10d}\t{chunk_summary.total_bytes:<15d}\t{chunk_summary.desc:s}")

        gef_print("\n== Chunk distribution by flag ==")
        gef_print(f"{'Flag':<15s}\t{'TotalCount':<10s}\t{'TotalBytes':s}")
        for chunk_flag, chunk_summary in self.flag_distribution.items():
            gef_print(f"{chunk_flag:<15s}\t{chunk_summary.count:<10d}\t{chunk_summary.total_bytes:<d}")

class GlibcHeapWalkContext:
    def __init__(self, print_arena: bool = False, allow_unaligned: bool = False, min_size: int = 0, max_size: int = 0, count: int = -1, resolve_type: bool = False, summary: bool = False) -> None:
        self.print_arena = print_arena
        self.allow_unaligned = allow_unaligned
        self.min_size = min_size
        self.max_size = max_size
        self.remaining_chunk_count = count
        self.summary = summary
        self.resolve_type = resolve_type

@register
class GlibcHeapChunksCommand(GenericCommand):
    """Display all heap chunks for the current arena. As an optional argument
    the base address of a different arena can be passed"""

    _cmdline_ = "heap chunks"
    _syntax_  = f"{_cmdline_} [-h] [--all] [--allow-unaligned] [--summary] [--min-size MIN_SIZE] [--max-size MAX_SIZE] [--count COUNT] [--resolve] [arena_address]"
    _example_ = (f"\n{_cmdline_}"
                 f"\n{_cmdline_} 0x555555775000")

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        self["peek_nb_byte"] = (16, "Hexdump N first byte(s) inside the chunk data (0 to disable)")
        return

    @parse_arguments({"arena_address": ""}, {("--all", "-a"): False, "--allow-unaligned": False, "--min-size": 0, "--max-size": 0, ("--count", "-n"): -1, ("--summary", "-s"): False, "--resolve": False})
    @only_if_gdb_running
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args = kwargs["arguments"]
        ctx = GlibcHeapWalkContext(print_arena=args.all, allow_unaligned=args.allow_unaligned, min_size=args.min_size, max_size=args.max_size, count=args.count, resolve_type=args.resolve, summary=args.summary)
        if args.all or not args.arena_address:
            for arena in gef.heap.arenas:
                self.dump_chunks_arena(arena, ctx)
                if not args.all:
                    return
        try:
            arena_addr = parse_address(args.arena_address)
            arena = GlibcArena(f"*{arena_addr:#x}")
            self.dump_chunks_arena(arena, ctx)
        except gdb.error:
            err("Invalid arena")
            return

    def dump_chunks_arena(self, arena: GlibcArena, ctx: GlibcHeapWalkContext) -> None:
        heap_addr = arena.heap_addr(allow_unaligned=ctx.allow_unaligned)
        if heap_addr is None:
            err("Could not find heap for arena")
            return
        if ctx.print_arena:
            gef_print(str(arena))
        if arena.is_main_arena():
            heap_end = arena.top + GlibcChunk(arena.top, from_base=True).size
            self.dump_chunks_heap(heap_addr, heap_end, arena, ctx)
        else:
            heap_info_structs = arena.get_heap_info_list() or []
            for heap_info in heap_info_structs:
                if not self.dump_chunks_heap(heap_info.heap_start, heap_info.heap_end, arena, ctx):
                    break
        return

    def dump_chunks_heap(self, start: int, end: int, arena: GlibcArena, ctx: GlibcHeapWalkContext) -> bool:
        nb = self["peek_nb_byte"]
        chunk_iterator = GlibcChunk(start, from_base=True, allow_unaligned=ctx.allow_unaligned)
        heap_summary = GlibcHeapArenaSummary(resolve_type=ctx.resolve_type)
        for chunk in chunk_iterator:
            heap_corrupted = chunk.base_address > end
            should_process = self.should_process_chunk(chunk, ctx)

            if not ctx.summary and chunk.base_address == arena.top:
                if should_process:
                    gef_print(
                        f"{chunk!s} {LEFT_ARROW} {Color.greenify('top chunk')}")
                break

            if heap_corrupted:
                err("Corrupted heap, cannot continue.")
                return False

            if not should_process:
                continue

            if ctx.remaining_chunk_count == 0:
                break

            if ctx.summary:
                heap_summary.process_chunk(chunk)
            else:
                line = str(chunk)
                if nb:
                    line += f"\n    [{hexdump(gef.memory.read(chunk.data_address, nb), nb, base=chunk.data_address)}]"
                gef_print(line)

            ctx.remaining_chunk_count -= 1

        if ctx.summary:
            heap_summary.print()

        return True

    def should_process_chunk(self, chunk: GlibcChunk, ctx: GlibcHeapWalkContext) -> bool:
        if chunk.size < ctx.min_size:
            return False

        if 0 < ctx.max_size < chunk.size:
            return False

        return True


@register
class GlibcHeapBinsCommand(GenericCommand):
    """Display information on the bins on an arena (default: main_arena).
    See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123."""

    _bin_types_ = ("tcache", "fast", "unsorted", "small", "large")
    _cmdline_ = "heap bins"
    _syntax_ = f"{_cmdline_} [{'|'.join(_bin_types_)}]"

    def __init__(self) -> None:
        super().__init__(prefix=True, complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            for bin_t in self._bin_types_:
                gdb.execute(f"heap bins {bin_t}")
            return

        bin_t = argv[0]
        if bin_t not in self._bin_types_:
            self.usage()
            return

        gdb.execute(f"heap bins {bin_t}")
        return

    def pprint_bin(self, arena_addr: str, index: int, _type: str = "") -> int:
        arena = GlibcArena(arena_addr)

        fd, bk = arena.bin(index)
        if (fd, bk) == (0x00, 0x00):
            warn("Invalid backward and forward bin pointers(fw==bk==NULL)")
            return -1

        if _type == "tcache":
            chunkClass = GlibcTcacheChunk
        elif _type == "fast":
            chunkClass = GlibcFastChunk
        else:
            chunkClass = GlibcChunk

        nb_chunk = 0
        head = chunkClass(bk, from_base=True).fd
        if fd == head:
            return nb_chunk

        ok(f"{_type}bins[{index:d}]: fw={fd:#x}, bk={bk:#x}")

        m = []
        while fd != head:
            chunk = chunkClass(fd, from_base=True)
            m.append(f"{RIGHT_ARROW}  {chunk!s}")
            fd = chunk.fd
            nb_chunk += 1

        if m:
            gef_print("  ".join(m))
        return nb_chunk


@register
class GlibcHeapTcachebinsCommand(GenericCommand):
    """Display information on the Tcachebins on an arena (default: main_arena).
    See https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc."""

    _cmdline_ = "heap bins tcache"
    _syntax_  = f"{_cmdline_} [all] [thread_ids...]"

    TCACHE_MAX_BINS = 0x40

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        # Determine if we are using libc with tcache built in (2.26+)
        if gef.libc.version and gef.libc.version < (2, 26):
            info("No Tcache in this version of libc")
            return

        current_thread = gdb.selected_thread()
        if current_thread is None:
            err("Couldn't find current thread")
            return

        # As a nicety, we want to display threads in ascending order by gdb number
        threads = sorted(gdb.selected_inferior().threads(), key=lambda t: t.num)
        if argv:
            if "all" in argv:
                tids = [t.num for t in threads]
            else:
                tids = self.check_thread_ids([int(a) for a in argv])
        else:
            tids = [current_thread.num]

        for thread in threads:
            if thread.num not in tids:
                continue

            thread.switch()

            tcache_addr = self.find_tcache()
            if tcache_addr == 0:
                info(f"Uninitialized tcache for thread {thread.num:d}")
                continue

            gef_print(titlify(f"Tcachebins for thread {thread.num:d}"))
            tcache_empty = True
            for i in range(self.TCACHE_MAX_BINS):
                chunk, count = self.tcachebin(tcache_addr, i)
                chunks = set()
                msg = []
                chunk_size = 0

                # Only print the entry if there are valid chunks. Don't trust count
                while True:
                    if chunk is None:
                        break

                    try:
                        msg.append(f"{LEFT_ARROW} {chunk!s} ")
                        if not chunk_size:
                            chunk_size = chunk.usable_size

                        if chunk.data_address in chunks:
                            msg.append(f"{RIGHT_ARROW} [loop detected]")
                            break

                        chunks.add(chunk.data_address)

                        next_chunk = chunk.fd
                        if next_chunk == 0:
                            break

                        chunk = GlibcTcacheChunk(next_chunk)
                    except gdb.MemoryError:
                        msg.append(f"{LEFT_ARROW} [Corrupted chunk at {chunk.data_address:#x}]")
                        break

                if msg:
                    tcache_empty = False
                    tidx = gef.heap.csize2tidx(chunk_size)
                    size = gef.heap.tidx2size(tidx)
                    count = len(chunks)
                    gef_print(f"Tcachebins[idx={tidx:d}, size={size:#x}, count={count}]", end="")
                    gef_print("".join(msg))

            if tcache_empty:
                gef_print("All tcachebins are empty")

        current_thread.switch()
        return

    def find_tcache(self) -> int:
        """Return the location of the current thread's tcache."""
        try:
            # For multithreaded binaries, the tcache symbol (in thread local
            # storage) will give us the correct address.
            tcache_addr = parse_address("(void *) tcache")
        except gdb.error:
            # In binaries not linked with pthread (and therefore there is only
            # one thread), we can't use the tcache symbol, but we can guess the
            # correct address because the tcache is consistently the first
            # allocation in the main arena.
            heap_base = gef.heap.base_address
            if heap_base is None:
                err("No heap section")
                return 0x0
            tcache_addr = heap_base + 0x10
        return tcache_addr

    def check_thread_ids(self, tids: list[int]) -> list[int]:
        """Return the subset of tids that are currently valid."""
        existing_tids = set(t.num for t in gdb.selected_inferior().threads())
        return list(set(tids) & existing_tids)

    def tcachebin(self, tcache_base: int, i: int) -> tuple[GlibcTcacheChunk | None, int]:
        """Return the head chunk in tcache[i] and the number of chunks in the bin."""
        if i >= self.TCACHE_MAX_BINS:
            err("Incorrect index value, index value must be between 0 and "
                f"{self.TCACHE_MAX_BINS}-1, given {i}"
            )
            return None, 0

        tcache_chunk = GlibcTcacheChunk(tcache_base)

        # Glibc changed the size of the tcache in version 2.30; this fix has
        # been backported inconsistently between distributions. We detect the
        # difference by checking the size of the allocated chunk for the
        # tcache.
        # Minimum usable size of allocated tcache chunk = ?
        #   For new tcache:
        #   TCACHE_MAX_BINS * _2_ + TCACHE_MAX_BINS * ptrsize
        #   For old tcache:
        #   TCACHE_MAX_BINS * _1_ + TCACHE_MAX_BINS * ptrsize
        new_tcache_min_size = (
                self.TCACHE_MAX_BINS * 2 +
                self.TCACHE_MAX_BINS * gef.arch.ptrsize)

        if tcache_chunk.usable_size < new_tcache_min_size:
            tcache_count_size = 1
            count = ord(gef.memory.read(tcache_base + tcache_count_size*i, 1))
        else:
            tcache_count_size = 2
            count = u16(gef.memory.read(tcache_base + tcache_count_size*i, 2))

        chunk = dereference(tcache_base + tcache_count_size*self.TCACHE_MAX_BINS + i*gef.arch.ptrsize)
        chunk = GlibcTcacheChunk(int(chunk)) if chunk else None
        return chunk, count


@register
class GlibcHeapFastbinsYCommand(GenericCommand):
    """Display information on the fastbinsY on an arena (default: main_arena).
    See https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1123."""

    _cmdline_ = "heap bins fast"
    _syntax_  = f"{_cmdline_} [ARENA_ADDRESS]"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({"arena_address": ""}, {})
    @only_if_gdb_running
    def do_invoke(self, *_: Any, **kwargs: Any) -> None:
        def fastbin_index(sz: int) -> int:
            return (sz >> 4) - 2 if SIZE_SZ == 8 else (sz >> 3) - 2

        args : argparse.Namespace = kwargs["arguments"]
        if not gef.heap.main_arena:
            err("Heap not initialized")
            return

        SIZE_SZ = gef.arch.ptrsize
        MAX_FAST_SIZE = 80 * SIZE_SZ // 4
        NFASTBINS = fastbin_index(MAX_FAST_SIZE) - 1

        arena = GlibcArena(f"*{args.arena_address}") if args.arena_address else gef.heap.selected_arena
        if arena is None:
            err("Invalid Glibc arena")
            return

        gef_print(titlify(f"Fastbins for arena at {arena.addr:#x}"))
        for i in range(NFASTBINS):
            gef_print(f"Fastbins[idx={i:d}, size={(i+2)*SIZE_SZ*2:#x}] ", end="")
            chunk = arena.fastbin(i)
            chunks = set()

            while True:
                if chunk is None:
                    gef_print("0x00", end="")
                    break

                try:
                    gef_print(f"{LEFT_ARROW} {chunk!s} ", end="")
                    if chunk.data_address in chunks:
                        gef_print(f"{RIGHT_ARROW} [loop detected]", end="")
                        break

                    if fastbin_index(chunk.size) != i:
                        gef_print("[incorrect fastbin_index] ", end="")

                    chunks.add(chunk.data_address)

                    next_chunk = chunk.fd
                    if next_chunk == 0:
                        break

                    chunk = GlibcFastChunk(next_chunk, from_base=True)
                except gdb.MemoryError:
                    gef_print(f"{LEFT_ARROW} [Corrupted chunk at {chunk.data_address:#x}]", end="")
                    break
            gef_print()
        return


@register
class GlibcHeapUnsortedBinsCommand(GenericCommand):
    """Display information on the Unsorted Bins of an arena (default: main_arena).
    See: https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c#L1689."""

    _cmdline_ = "heap bins unsorted"
    _syntax_  = f"{_cmdline_} [ARENA_ADDRESS]"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({"arena_address": ""}, {})
    @only_if_gdb_running
    def do_invoke(self, *_: Any, **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not gef.heap.main_arena or not gef.heap.selected_arena:
            err("Heap not initialized")
            return

        arena_addr = args.arena_address if args.arena_address else f"{gef.heap.selected_arena.addr:#x}"
        gef_print(titlify(f"Unsorted Bin for arena at {arena_addr}"))
        heap_bins_cmd = gef.gdb.commands["heap bins"]
        assert isinstance(heap_bins_cmd, GlibcHeapBinsCommand)
        nb_chunk = heap_bins_cmd.pprint_bin(f"*{arena_addr}", 0, "unsorted_")
        if nb_chunk >= 0:
            info(f"Found {nb_chunk:d} chunks in unsorted bin.")
        return


@register
class GlibcHeapSmallBinsCommand(GenericCommand):
    """Convenience command for viewing small bins."""

    _cmdline_ = "heap bins small"
    _syntax_  = f"{_cmdline_} [ARENA_ADDRESS]"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({"arena_address": ""}, {})
    @only_if_gdb_running
    def do_invoke(self, *_: Any, **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not gef.heap.main_arena or not gef.heap.selected_arena:
            err("Heap not initialized")
            return

        arena_address = args.arena_address or f"{gef.heap.selected_arena.address:#x}"
        gef_print(titlify(f"Small Bins for arena at {arena_address}"))
        bins: dict[int, int] = {}
        heap_bins_cmd = gef.gdb.commands["heap bins"]
        assert isinstance (heap_bins_cmd, GlibcHeapBinsCommand)
        for i in range(1, 63):
            nb_chunk = heap_bins_cmd.pprint_bin(f"*{arena_address}", i, "small_")
            if nb_chunk < 0:
                break
            if nb_chunk > 0:
                bins[i] = nb_chunk
        info(f"Found {sum(list(bins.values())):d} chunks in {len(bins):d} small non-empty bins.")
        return


@register
class GlibcHeapLargeBinsCommand(GenericCommand):
    """Convenience command for viewing large bins."""

    _cmdline_ = "heap bins large"
    _syntax_  = f"{_cmdline_} [ARENA_ADDRESS]"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({"arena_address": ""}, {})
    @only_if_gdb_running
    def do_invoke(self, *_: Any, **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not gef.heap.main_arena or not gef.heap.selected_arena:
            err("Heap not initialized")
            return

        arena_addr = args.arena_address if args.arena_address else f"{gef.heap.selected_arena.addr:#x}"
        gef_print(titlify(f"Large Bins for arena at {arena_addr}"))
        bins = {}
        heap_bins_cmd = gef.gdb.commands["heap bins"]
        assert isinstance(heap_bins_cmd, GlibcHeapBinsCommand)
        for i in range(63, 126):
            nb_chunk = heap_bins_cmd.pprint_bin(f"*{arena_addr}", i, "large_")
            if nb_chunk < 0:
                break
            if nb_chunk > 0:
                bins[i] = nb_chunk
        info(f"Found {sum(bins.values()):d} chunks in {len(bins):d} large non-empty bins.")
        return


@register
class DetailRegistersCommand(GenericCommand):
    """Display full details on one, many or all registers value from current architecture."""

    _cmdline_ = "registers"
    _syntax_  = f"{_cmdline_} [[Register1][Register2] ... [RegisterN]]"
    _example_ = (f"\n{_cmdline_}"
                 f"\n{_cmdline_} $eax $eip $esp")

    @only_if_gdb_running
    @parse_arguments({"registers": [""]}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        unchanged_color = gef.config["theme.registers_register_name"]
        changed_color = gef.config["theme.registers_value_changed"]
        string_color = gef.config["theme.dereference_string"]
        regs = gef.arch.all_registers

        args : argparse.Namespace = kwargs["arguments"]
        if args.registers and args.registers[0]:
            all_regs = set(gef.arch.all_registers)
            regs = [reg for reg in args.registers if reg in all_regs]
            invalid_regs = [reg for reg in args.registers if reg not in all_regs]
            if invalid_regs:
                err(f"invalid registers for architecture: {', '.join(invalid_regs)}")

        memsize = gef.arch.ptrsize
        endian = str(gef.arch.endianness)
        charset = string.printable
        widest = max(map(len, gef.arch.all_registers))
        special_line = ""

        for regname in regs:
            reg = gdb.parse_and_eval(regname)
            if reg.type.code == gdb.TYPE_CODE_VOID:
                continue

            padreg = regname.ljust(widest, " ")

            if str(reg) == "<unavailable>":
                gef_print(f"{Color.colorify(padreg, unchanged_color)}: "
                          f"{Color.colorify('no value', 'yellow underline')}")
                continue

            value = align_address(int(reg))
            ctx_cmd = gef.gdb.commands["context"]
            assert isinstance(ctx_cmd, ContextCommand)
            old_value = ctx_cmd.old_registers.get(regname, 0)
            if value == old_value:
                color = unchanged_color
            else:
                color = changed_color

            # Special (e.g. segment) registers go on their own line
            if regname in gef.arch.special_registers:
                special_line += f"{Color.colorify(regname, color)}: "
                special_line += f"{gef.arch.register(regname):#04x} "
                continue

            line = f"{Color.colorify(padreg, color)}: "

            if regname == gef.arch.flag_register:
                line += gef.arch.flag_register_to_human()
                gef_print(line)
                continue

            addr = lookup_address(align_address(int(value)))
            if addr.valid:
                line += str(addr)
            else:
                line += format_address_spaces(value)
            addrs = dereference_from(value)

            if len(addrs) > 1:
                sep = f" {RIGHT_ARROW} "
                line += sep
                line += sep.join(addrs[1:])

            # check to see if reg value is ascii
            try:
                fmt = f"{endian}{'I' if memsize == 4 else 'Q'}"
                last_addr = int(addrs[-1], 16)
                val = gef_pystring(struct.pack(fmt, last_addr))
                if all([_ in charset for _ in val]):
                    line += f" (\"{Color.colorify(val, string_color)}\"?)"
            except ValueError:
                pass

            gef_print(line)

        if special_line:
            gef_print(special_line)
        return


@register
class ShellcodeCommand(GenericCommand):
    """ShellcodeCommand uses @JonathanSalwan simple-yet-awesome shellcode API to
    download shellcodes."""

    _cmdline_ = "shellcode"
    _syntax_  = f"{_cmdline_} (search|get)"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    def do_invoke(self, _: list[str]) -> None:
        err("Missing sub-command (search|get)")
        self.usage()
        return


@register
class ShellcodeSearchCommand(GenericCommand):
    """Search pattern in shell-storm's shellcode database."""

    _cmdline_ = "shellcode search"
    _syntax_  = f"{_cmdline_} PATTERN1 PATTERN2"
    _aliases_ = ["sc-search",]

    api_base = "http://shell-storm.org"
    search_url = f"{api_base}/api/?s="

    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            err("Missing pattern to search")
            self.usage()
            return

        # API : http://shell-storm.org/shellcode/
        args = "*".join(argv)

        res = http_get(self.search_url + args)
        if res is None:
            err("Could not query search page")
            return

        ret = gef_pystring(res)

        # format: [author, OS/arch, cmd, id, link]
        lines = ret.split("\\n")
        refs = [line.split("::::") for line in lines]

        if refs:
            info("Showing matching shellcodes")
            info("\t".join(["Id", "Platform", "Description"]))
            for ref in refs:
                try:
                    _, arch, cmd, sid, _ = ref
                    gef_print("\t".join([sid, arch, cmd]))
                except ValueError:
                    continue

            info("Use `shellcode get <id>` to fetch shellcode")
        return


@register
class ShellcodeGetCommand(GenericCommand):
    """Download shellcode from shell-storm's shellcode database."""

    _cmdline_ = "shellcode get"
    _syntax_  = f"{_cmdline_} SHELLCODE_ID"
    _aliases_ = ["sc-get",]

    api_base = "http://shell-storm.org"
    get_url = f"{api_base}/shellcode/files/shellcode-{{:d}}.html"

    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) != 1:
            err("Missing ID to download")
            self.usage()
            return

        if not argv[0].isdigit():
            err("ID is not a number")
            self.usage()
            return

        self.get_shellcode(int(argv[0]))
        return

    def get_shellcode(self, sid: int) -> None:
        info(f"Downloading shellcode id={sid}")
        res = http_get(self.get_url.format(sid))
        if res is None:
            err(f"Failed to fetch shellcode #{sid}")
            return

        ok("Downloaded, written to disk...")
        with tempfile.NamedTemporaryFile(prefix="sc-", suffix=".txt", mode='w+b', delete=False, dir=gef.config["gef.tempdir"]) as fd:
            shellcode = res.split(b"<pre>")[1].split(b"</pre>")[0]
            shellcode = shellcode.replace(b"&quot;", b'"')
            fd.write(shellcode)
            ok(f"Shellcode written to '{fd.name}'")
        return


@register
class ProcessListingCommand(GenericCommand):
    """List and filter process. If a PATTERN is given as argument, results shown will be grepped
    by this pattern."""

    _cmdline_ = "process-search"
    _syntax_  = f"{_cmdline_} [-h] [--attach] [--smart-scan] [REGEX_PATTERN]"
    _aliases_ = ["ps"]
    _example_ = f"{_cmdline_} gdb.*"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        self["ps_command"] = (f"{gef.session.constants['ps']} auxww", "`ps` command to get process information")
        return

    @parse_arguments({"pattern": ""}, {"--attach": False, "--smart-scan": False})
    def do_invoke(self, _: list, **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        do_attach = args.attach
        smart_scan = args.smart_scan
        pattern = args.pattern
        pattern = re.compile("^.*$") if not args else re.compile(pattern)

        for process in self.get_processes():
            pid = int(process["pid"])
            command = process["command"]

            if not re.search(pattern, command):
                continue

            if smart_scan:
                if command.startswith("[") and command.endswith("]"): continue
                if command.startswith("socat "): continue
                if command.startswith("grep "): continue
                if command.startswith("gdb "): continue

            if args and do_attach:
                ok(f"Attaching to process='{process['command']}' pid={pid:d}")
                gdb.execute(f"attach {pid:d}")
                return None

            line = [process[i] for i in ("pid", "user", "cpu", "mem", "tty", "command")]
            gef_print("\t\t".join(line))

        return None

    def get_processes(self) -> Generator[dict[str, str], None, None]:
        output = gef_execute_external(self["ps_command"].split(), True)
        names = [x.lower().replace("%", "") for x in output[0].split()]

        for line in output[1:]:
            fields = line.split()
            t = {}

            for i, name in enumerate(names):
                if i == len(names) - 1:
                    t[name] = " ".join(fields[i:])
                else:
                    t[name] = fields[i]

            yield t

        return


@register
class ElfInfoCommand(GenericCommand):
    """Display a limited subset of ELF header information. If no argument is provided, the command will
    show information about the current ELF being debugged."""

    _cmdline_ = "elf-info"
    _syntax_  = f"{_cmdline_} [FILE]"
    _example_  = f"{_cmdline_} /bin/ls"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @parse_arguments({}, {"--filename": ""})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]

        if is_qemu_system():
            err("Unsupported")
            return

        filename = args.filename or get_filepath()
        if filename is None:
            return

        try:
            elf = Elf(filename)
        except ValueError:
            err(f"`{filename}` is an invalid value for ELF file")
            return

        data = [
            ("Magic", f"{hexdump(struct.pack('>I', elf.e_magic), show_raw=True)}"),
            ("Class", f"{elf.e_class.value:#x} - {elf.e_class.name}"),
            ("Endianness", f"{elf.e_endianness.value:#x} - {Endianness(elf.e_endianness).name}"),
            ("Version", f"{elf.e_eiversion:#x}"),
            ("OS ABI", f"{elf.e_osabi.value:#x} - {elf.e_osabi.name if elf.e_osabi else ''}"),
            ("ABI Version", f"{elf.e_abiversion:#x}"),
            ("Type", f"{elf.e_type.value:#x} - {elf.e_type.name}"),
            ("Machine", f"{elf.e_machine.value:#x} - {elf.e_machine.name}"),
            ("Program Header Table", f"{format_address(elf.e_phoff)}"),
            ("Section Header Table", f"{format_address(elf.e_shoff)}"),
            ("Header Table", f"{format_address(elf.e_phoff)}"),
            ("ELF Version", f"{elf.e_version:#x}"),
            ("Header size", f"{elf.e_ehsize} ({elf.e_ehsize:#x})"),
            ("Entry point", f"{format_address(elf.e_entry)}"),
        ]

        for title, content in data:
            gef_print(f"{Color.boldify(f'{title:<22}')}: {content}")

        gef_print("")
        gef_print(titlify("Program Header"))

        gef_print(f"  [{'#':>2s}] {'Type':12s} {'Offset':>8s} {'Virtaddr':>10s} {'Physaddr':>10s}"
                  f" {'FileSiz':>8s} {'MemSiz':>8s} {'Flags':5s} {'Align':>8s}")

        for i, p in enumerate(elf.phdrs):
            p_type = p.p_type.name if p.p_type else ""
            p_flags = str(p.p_flags.name).lstrip("Flag.") if p.p_flags else "???"

            gef_print(f"  [{i:2d}] {p_type:12s} {p.p_offset:#8x} {p.p_vaddr:#10x} {p.p_paddr:#10x}"
                      f" {p.p_filesz:#8x} {p.p_memsz:#8x} {p_flags:5s} {p.p_align:#8x}")

        gef_print("")
        gef_print(titlify("Section Header"))
        gef_print(f"  [{'#':>2s}] {'Name':20s} {'Type':>15s} {'Address':>10s} {'Offset':>8s}"
                  f" {'Size':>8s} {'EntSiz':>8s} {'Flags':5s} {'Link':4s} {'Info':4s} {'Align':>8s}")

        for i, s in enumerate(elf.shdrs):
            sh_type = s.sh_type.name if s.sh_type else "UNKN"
            sh_flags = str(s.sh_flags).lstrip("Flags.") if s.sh_flags else "UNKN"

            gef_print(f"  [{i:2d}] {s.name:20s} {sh_type:>15s} {s.sh_addr:#10x} {s.sh_offset:#8x} "
                      f"{s.sh_size:#8x} {s.sh_entsize:#8x} {sh_flags:5s} {s.sh_link:#4x} {s.sh_info:#4x} {s.sh_addralign:#8x}")
        return


@register
class EntryPointBreakCommand(GenericCommand):
    """Tries to find best entry point and sets a temporary breakpoint on it. The command will test for
    well-known symbols for entry points, such as `main`, `_main`, `__libc_start_main`, etc. defined by
    the setting `entrypoint_symbols`."""

    _cmdline_ = "entry-break"
    _syntax_  = _cmdline_
    _aliases_ = ["start",]

    def __init__(self) -> None:
        super().__init__()
        self["entrypoint_symbols"] = ("main _main __libc_start_main __uClibc_main start _start", "Possible symbols for entry points")
        return

    def do_invoke(self, argv: list[str]) -> None:
        fpath = get_filepath()
        if fpath is None:
            warn("No executable to debug, use `file` to load a binary")
            return

        if not os.access(fpath, os.X_OK):
            warn(f"The file '{fpath}' is not executable.")
            return

        if is_alive() and not gef.session.qemu_mode:
            warn("gdb is already running")
            return

        bp = None
        entrypoints = self["entrypoint_symbols"].split()

        for sym in entrypoints:
            try:
                value = parse_address(sym)
                info(f"Breaking at '{value:#x}'")
                bp = EntryBreakBreakpoint(sym)
                gdb.execute(f"run {' '.join(argv)}")
                return

            except gdb.error as gdb_error:
                if 'The "remote" target does not support "run".' in str(gdb_error):
                    # this case can happen when doing remote debugging
                    gdb.execute("continue")
                    return
                continue

        # if here, clear the breakpoint if any set
        if bp:
            bp.delete()

        assert gef.binary
        # break at entry point
        entry = gef.binary.entry_point

        if is_pie(fpath):
            self.set_init_tbreak_pie(entry, argv)
            gdb.execute("continue")
            return

        self.set_init_tbreak(entry)
        gdb.execute(f"run {' '.join(argv)}")
        return

    def set_init_tbreak(self, addr: int) -> EntryBreakBreakpoint:
        info(f"Breaking at entry-point: {addr:#x}")
        bp = EntryBreakBreakpoint(f"*{addr:#x}")
        return bp

    def set_init_tbreak_pie(self, addr: int, argv: list[str]) -> EntryBreakBreakpoint:
        warn("PIC binary detected, retrieving text base address")
        gdb.execute("set stop-on-solib-events 1")
        hide_context()
        gdb.execute(f"run {' '.join(argv)}")
        unhide_context()
        gdb.execute("set stop-on-solib-events 0")
        vmmap = gef.memory.maps
        base_address = [x.page_start for x in vmmap if x.path == get_filepath()][0]
        return self.set_init_tbreak(base_address + addr)


@register
class NamedBreakpointCommand(GenericCommand):
    """Sets a breakpoint and assigns a name to it, which will be shown, when it's hit."""

    _cmdline_ = "name-break"
    _syntax_  = f"{_cmdline_} name [address]"
    _aliases_ = ["nb",]
    _example  = f"{_cmdline_} main *0x4008a9"

    def __init__(self) -> None:
        super().__init__()
        return

    @parse_arguments({"name": "", "address": "*$pc"}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not args.name:
            err("Missing name for breakpoint")
            self.usage()
            return

        NamedBreakpoint(args.address, args.name)
        return


@register
class ContextCommand(GenericCommand):
    """Displays a comprehensive and modular summary of runtime context. Unless setting `enable` is
    set to False, this command will be spawned automatically every time GDB hits a breakpoint, a
    watchpoint, or any kind of interrupt. By default, it will show panes that contain the register
    states, the stack, and the disassembly code around $pc."""

    _cmdline_ = "context"
    _syntax_  = f"{_cmdline_} [legend|regs|stack|code|args|memory|source|trace|threads|extra]"
    _aliases_ = ["ctx",]

    old_registers: dict[str, int | None] = {}

    def __init__(self) -> None:
        super().__init__()
        self["enable"] = (True, "Enable/disable printing the context when breaking")
        self["show_source_code_variable_values"] = (True, "Show extra PC context info in the source code")
        self["show_full_source_file_name_max_len"] = (30, "Show full source path name, if less than this value")
        self["show_basename_source_file_name_max_len"] = (20, "Show the source basename in full, if less than this value")
        self["show_prefix_source_path_name_len"] = (10, "When truncating source path, show this many path prefix characters")
        self["show_stack_raw"] = (False, "Show the stack pane as raw hexdump (no dereference)")
        self["show_registers_raw"] = (False, "Show the registers pane with raw values (no dereference)")
        self["show_opcodes_size"] = (0, "Number of bytes of opcodes to display next to the disassembly")
        self["peek_calls"] = (True, "Peek into calls")
        self["peek_ret"] = (True, "Peek at return address")
        self["nb_lines_stack"] = (8, "Number of line in the stack pane")
        self["grow_stack_down"] = (False, "Order of stack downward starts at largest down to stack pointer")
        self["nb_lines_backtrace"] = (10, "Number of line in the backtrace pane")
        self["nb_lines_backtrace_before"] = (2, "Number of line in the backtrace pane before selected frame")
        self["nb_lines_threads"] = (-1, "Number of line in the threads pane")
        self["nb_lines_code"] = (6, "Number of instruction after $pc")
        self["nb_lines_code_prev"] = (3, "Number of instruction before $pc")
        self["ignore_registers"] = ("", "Space-separated list of registers not to display (e.g. '$cs $ds $gs')")
        self["clear_screen"] = (True, "Clear the screen before printing the context")
        self["layout"] = ("legend regs stack code args source memory threads trace extra", "Change the order/presence of the context sections")
        self["redirect"] = ("", "Redirect the context information to another TTY")
        self["libc_args"] = (False, "[DEPRECATED - Unused] Show libc function call args description")
        self["libc_args_path"] = ("", "[DEPRECATED - Unused] Path to libc function call args json files, provided via gef-extras")

        self.layout_mapping: dict[str, tuple[Callable, Callable | None, Callable | None]] = {
            "legend": (self.show_legend, None, None),
            "regs": (self.context_regs, None, None),
            "stack": (self.context_stack, None, None),
            "code": (self.context_code, None, None),
            "args": (self.context_args, None, None),
            "memory": (self.context_memory, None, None),
            "source": (self.context_source, None, None),
            "trace": (self.context_trace, None, None),
            "threads": (self.context_threads, None, None),
            "extra": (self.context_additional_information, None, None),
        }

        self.instruction_iterator = gef_disassemble
        return

    def post_load(self) -> None:
        gef_on_continue_hook(self.update_registers)
        gef_on_continue_hook(self.empty_extra_messages)
        return

    def show_legend(self) -> None:
        if gef.config["gef.disable_color"] is True:
            return
        changed_register_title = Color.colorify("Modified register", gef.config["theme.registers_value_changed"])
        code_title = Color.colorify("Code", gef.config["theme.address_code"])
        heap_title = Color.colorify("Heap", gef.config["theme.address_heap"])
        stack_title = Color.colorify("Stack", gef.config["theme.address_stack"])
        str_title = Color.colorify("String", gef.config["theme.dereference_string"])
        gef_print(f"[ Legend: {changed_register_title} | {code_title} | {heap_title} | {stack_title} | {str_title} ]")
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if not self["enable"] or gef.ui.context_hidden:
            return

        if not all(_ in self.layout_mapping for _ in argv):
            self.usage()
            return

        if len(argv) > 0:
            current_layout = argv
        else:
            current_layout = self["layout"].strip().split()

        if not current_layout:
            return

        self.tty_rows, self.tty_columns = get_terminal_size()

        redirect = self["redirect"]
        if redirect and os.access(redirect, os.W_OK):
            enable_redirect_output(to_file=redirect)

        if self["clear_screen"] and len(argv) == 0:
            clear_screen(redirect)

        for section in current_layout:
            if section[0] == "-":
                continue

            try:
                display_pane_function, pane_title_function, condition = self.layout_mapping[section]
                if condition:
                    if not condition():
                        continue
                if pane_title_function:
                    self.context_title(pane_title_function())
                display_pane_function()
            except gdb.MemoryError as e:
                # a MemoryError will happen when $pc is corrupted (invalid address)
                err(str(e))
            except IndexError:
                # the `section` is not present, just skip
                pass

        self.context_title("")

        if redirect and os.access(redirect, os.W_OK):
            disable_redirect_output()
        return

    def context_title(self, m: str | None) -> None:
        # allow for not displaying a title line
        if m is None:
            return

        line_color = gef.config["theme.context_title_line"]
        msg_color = gef.config["theme.context_title_message"]

        # print an empty line in case of ""
        if not m:
            gef_print(Color.colorify(HORIZONTAL_LINE * self.tty_columns, line_color))
            return

        trail_len = len(m) + 6
        title = ""
        width = max(self.tty_columns - trail_len, 0)
        padd = HORIZONTAL_LINE
        title += Color.colorify(f"{'':{padd}<{width}} ", line_color)
        title += Color.colorify(m, msg_color)
        title += Color.colorify(f" {'':{padd}<4}", line_color)
        gef_print(title)
        return

    def context_regs(self) -> None:
        self.context_title("registers")
        ignored_registers = set(self["ignore_registers"].split())

        # Defer to DetailRegisters by default
        if self["show_registers_raw"] is False:
            regs = [reg for reg in gef.arch.all_registers if reg not in ignored_registers]
            printable_registers = " ".join(regs)
            gdb.execute(f"registers {printable_registers}")
            return

        widest = curlen = max(map(len, gef.arch.all_registers))
        curlen += 5
        curlen += gef.arch.ptrsize * 2
        nb = get_terminal_size()[1] // curlen
        i = 1
        line = ""
        changed_color = gef.config["theme.registers_value_changed"]
        regname_color = gef.config["theme.registers_register_name"]

        for reg in gef.arch.all_registers:
            if reg in ignored_registers:
                continue

            try:
                r = gdb.parse_and_eval(reg)
                if r.type.code == gdb.TYPE_CODE_VOID:
                    continue

                new_value_type_flag = r.type.code == gdb.TYPE_CODE_FLAGS
                new_value = int(r)

            except (gdb.MemoryError, gdb.error):
                # If this exception is triggered, it means that the current register
                # is corrupted. Just use the register "raw" value (not eval-ed)
                new_value = gef.arch.register(reg)
                new_value_type_flag = False

            except Exception:
                new_value = 0
                new_value_type_flag = False

            old_value = self.old_registers.get(reg, 0)

            padreg = reg.ljust(widest, " ")
            value = align_address(new_value)
            old_value = align_address(old_value or 0)
            if value == old_value:
                line += f"{Color.colorify(padreg, regname_color)}: "
            else:
                line += f"{Color.colorify(padreg, changed_color)}: "
            if new_value_type_flag:
                line += f"{format_address_spaces(value)} "
            else:
                addr = lookup_address(align_address(int(value)))
                if addr.valid:
                    line += f"{addr!s} "
                else:
                    line += f"{format_address_spaces(value)} "

            if i % nb == 0:
                gef_print(line)
                line = ""
            i += 1

        if line:
            gef_print(line)

        gef_print(f"Flags: {gef.arch.flag_register_to_human()}")
        return

    def context_stack(self) -> None:
        self.context_title("stack")

        show_raw = self["show_stack_raw"]
        nb_lines = self["nb_lines_stack"]

        try:
            sp = gef.arch.sp
            if show_raw is True:
                mem = gef.memory.read(sp, 0x10 * nb_lines)
                gef_print(hexdump(mem, base=sp))
            else:
                gdb.execute(f"dereference -l {nb_lines:d} {sp:#x}")

        except gdb.MemoryError:
            err("Cannot read memory from $SP (corrupted stack pointer?)")

        return

    def addr_has_breakpoint(self, address: int, bp_locations: list[str]) -> bool:
        return any(hex(address) in b for b in bp_locations)

    def context_code(self) -> None:
        nb_insn = self["nb_lines_code"]
        nb_insn_prev = self["nb_lines_code_prev"]
        show_opcodes_size = "show_opcodes_size" in self and self["show_opcodes_size"]
        past_insns_color = gef.config["theme.old_context"]
        cur_insn_color = gef.config["theme.disassemble_current_instruction"]
        pc = gef.arch.pc
        breakpoints = gdb.breakpoints() or []
        # breakpoint.locations was introduced in gdb 13.1
        if len(breakpoints) and hasattr(breakpoints[-1], "locations"):
            bp_locations = [hex(location.address) for b in breakpoints for location in b.locations if location is not None] # type: ignore
        else:
            # location relies on the user setting the breakpoints with "b *{hex(address)}"
            bp_locations = [b.location for b in breakpoints if b.location and b.location.startswith("*")]

        frame = gdb.selected_frame()
        arch_name = f"{gef.arch.arch.lower()}:{gef.arch.mode}"

        self.context_title(f"code:{arch_name}")

        try:


            for insn in self.instruction_iterator(pc, nb_insn, nb_prev=nb_insn_prev):
                line = []
                is_taken  = False
                target    = None
                bp_prefix = Color.redify(BP_GLYPH) if self.addr_has_breakpoint(insn.address, bp_locations) else " "

                if show_opcodes_size == 0:
                    text = str(insn)
                else:
                    insn_fmt = f"{{:{show_opcodes_size}o}}"
                    text = insn_fmt.format(insn)

                if insn.address < pc:
                    line += f"{bp_prefix}  {Color.colorify(text, past_insns_color)}"

                elif insn.address == pc:
                    line += f"{bp_prefix}{Color.colorify(f'{RIGHT_ARROW[1:]}{text}', cur_insn_color)}"

                    if gef.arch.is_conditional_branch(insn):
                        is_taken, reason = gef.arch.is_branch_taken(insn)
                        if is_taken:
                            target = insn.operands[-1].split()[0]
                            reason = f"[Reason: {reason}]" if reason else ""
                            line += Color.colorify(f"\tTAKEN {reason}", "bold green")
                        else:
                            reason = f"[Reason: !({reason})]" if reason else ""
                            line += Color.colorify(f"\tNOT taken {reason}", "bold red")
                    elif gef.arch.is_call(insn) and self["peek_calls"] is True:
                        target = insn.operands[-1].split()[0]
                    elif gef.arch.is_ret(insn) and self["peek_ret"] is True:
                        target = gef.arch.get_ra(insn, frame)

                else:
                    line += f"{bp_prefix}  {text}"

                gef_print("".join(line))

                if target:
                    try:
                        address = int(target, 0) if isinstance(target, str) else target
                    except ValueError:
                        # If the operand isn't an address right now we can't parse it
                        continue
                    for i, tinsn in enumerate(self.instruction_iterator(address, nb_insn)):
                        text= f"   {DOWN_ARROW if i == 0 else ' '}  {tinsn!s}"
                        gef_print(text)
                    break

        except gdb.MemoryError:
            err("Cannot disassemble from $PC")
        return

    def context_args(self) -> None:
        insn = gef_current_instruction(gef.arch.pc)
        if not gef.arch.is_call(insn):
            return

        self.size2type = {
            1: "BYTE",
            2: "WORD",
            4: "DWORD",
            8: "QWORD",
        }

        if insn.operands[-1].startswith(self.size2type[gef.arch.ptrsize]+" PTR"):
            target = "*" + insn.operands[-1].split()[-1]
        elif "$"+insn.operands[0] in gef.arch.all_registers:
            target = f"*{gef.arch.register('$' + insn.operands[0]):#x}"
        else:
            # is there a symbol?
            ops = " ".join(insn.operands)
            if "<" in ops and ">" in ops:
                # extract it
                target = re.sub(r".*<([^\(> ]*).*", r"\1", ops)
            else:
                # it's an address, just use as is
                target = re.sub(r".*(0x[a-fA-F0-9]*).*", r"\1", ops)

        sym = gdb.lookup_global_symbol(target)
        if sym is None:
            self.print_guessed_arguments(target)
            return

        if sym.type and sym.type.code != gdb.TYPE_CODE_FUNC:
            err(f"Symbol '{target}' is not a function: type={sym.type.code}")
            return

        self.print_arguments_from_symbol(target, sym)
        return

    def print_arguments_from_symbol(self, function_name: str, symbol: "gdb.Symbol") -> None:
        """If symbols were found, parse them and print the argument adequately."""
        args = []
        fields = symbol.type.fields() if symbol.type else []
        for i, f in enumerate(fields):
            if not f.type:
                continue
            _value = gef.arch.get_ith_parameter(i, in_func=False)[1]
            _value = RIGHT_ARROW.join(dereference_from(_value))
            _name = f.name or f"var_{i}"
            _type = f.type.name or self.size2type[f.type.sizeof]
            args.append(f"{_type} {_name} = {_value}")

        self.context_title("arguments")

        if not args:
            gef_print(f"{function_name} (<void>)")
            return

        gef_print(f"{function_name} (\n   "+",\n   ".join(args)+"\n)")
        return

    def print_guessed_arguments(self, function_name: str) -> None:
        """When no symbol, read the current basic block and look for "interesting" instructions."""

        def __get_current_block_start_address() -> int | None:
            pc = gef.arch.pc
            try:
                block = gdb.block_for_pc(pc)
                block_start = block.start if block else gdb_get_nth_previous_instruction_address(pc, 5)
            except RuntimeError:
                block_start = gdb_get_nth_previous_instruction_address(pc, 5)
            return block_start

        parameter_set = set()
        pc = gef.arch.pc
        block_start = __get_current_block_start_address()
        if not block_start:
            return

        function_parameters = gef.arch.function_parameters
        arg_key_color = gef.config["theme.registers_register_name"]

        for insn in self.instruction_iterator(block_start, pc - block_start):
            if not insn.operands:
                continue

            if is_x86_32():
                if insn.mnemonic == "push":
                    parameter_set.add(insn.operands[0])
            else:
                op = "$" + insn.operands[0]
                if op in function_parameters:
                    parameter_set.add(op)

                if is_x86_64():
                    # also consider extended registers
                    extended_registers = {"$rdi": ["$edi", "$di"],
                                          "$rsi": ["$esi", "$si"],
                                          "$rdx": ["$edx", "$dx"],
                                          "$rcx": ["$ecx", "$cx"],
                                         }
                    for exreg in extended_registers:
                        if op in extended_registers[exreg]:
                            parameter_set.add(exreg)

        if is_x86_32():
            nb_argument = len(parameter_set)
        else:
            nb_argument = max([function_parameters.index(p)+1 for p in parameter_set], default=0)

        args = []
        for i in range(nb_argument):
            _key, _values = gef.arch.get_ith_parameter(i, in_func=False)
            _values = RIGHT_ARROW.join(dereference_from(_values))
            args.append(f"{Color.colorify(_key, arg_key_color)} = {_values}")

        self.context_title("arguments (guessed)")
        gef_print(f"{function_name} (")
        if args:
            gef_print("   " + ",\n   ".join(args))
        gef_print(")")
        return

    def line_has_breakpoint(self, file_name: str, line_number: int, bp_locations: list[str]) -> bool:
        filename_line = f"{file_name}:{line_number}"
        return any(filename_line in loc for loc in bp_locations)

    def context_source(self) -> None:
        try:
            pc = gef.arch.pc
            symtabline = gdb.find_pc_line(pc)
            symtab = symtabline.symtab
            # we subtract one because the line number returned by gdb start at 1
            line_num = symtabline.line - 1
            if not symtab.is_valid():
                return

            fpath = pathlib.Path(symtab.fullname())
            lines = [curline.rstrip() for curline in fpath.read_text().splitlines()]

        except Exception:
            return

        file_base_name = os.path.basename(symtab.filename)
        breakpoints = gdb.breakpoints() or []
        bp_locations = [b.location for b in breakpoints if b.location and file_base_name in b.location]
        past_lines_color = gef.config["theme.old_context"]

        show_full_path_max = self["show_full_source_file_name_max_len"]
        show_basename_path_max = self["show_basename_source_file_name_max_len"]

        nb_line = self["nb_lines_code"]
        fn = symtab.filename
        if len(fn) > show_full_path_max:
            base = os.path.basename(fn)
            if len(base) > show_basename_path_max:
                base = base[-show_basename_path_max:]
            fn = fn[:15] + "[...]" + base
        title = f"source:{fn}+{line_num + 1}"
        cur_line_color = gef.config["theme.source_current_line"]
        self.context_title(title)
        show_extra_info = self["show_source_code_variable_values"]

        for i in range(line_num - nb_line + 1, line_num + nb_line):
            if i < 0:
                continue

            bp_prefix = Color.redify(BP_GLYPH) if self.line_has_breakpoint(file_base_name, i + 1, bp_locations) else " "

            if i < line_num:
                gef_print("{}{}".format(bp_prefix, Color.colorify(f"  {i + 1:4d}\t {lines[i]}", past_lines_color)))

            if i == line_num:
                prefix = f"{bp_prefix}{RIGHT_ARROW[1:]}{i + 1:4d}\t "
                leading = len(lines[i]) - len(lines[i].lstrip())
                if show_extra_info:
                    extra_info = self.get_pc_context_info(pc, lines[i])
                    if extra_info:
                        gef_print(f"{' ' * (len(prefix) + leading)}{extra_info}")
                gef_print(Color.colorify(f"{prefix}{lines[i]}", cur_line_color))

            if i > line_num:
                try:
                    gef_print(f"{bp_prefix}  {i + 1:4d}\t {lines[i]}")
                except IndexError:
                    break
        return

    def get_pc_context_info(self, pc: int, line: str) -> str:
        try:
            current_block = gdb.block_for_pc(pc)
            if not current_block or not current_block.is_valid(): return ""
            m = collections.OrderedDict()
            while current_block and not current_block.is_static:
                for sym in list(current_block):
                    symbol = sym.name
                    if not sym.is_function and re.search(fr"\W{symbol}\W", line):
                        val = gdb.parse_and_eval(symbol)
                        if val.type.code in (gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_ARRAY):
                            addr = int(val.address)
                            addrs = dereference_from(addr)
                            if len(addrs) > 2:
                                addrs = [addrs[0], "[...]", addrs[-1]]

                            f = f" {RIGHT_ARROW} "
                            val = f.join(addrs)
                        elif val.type.code == gdb.TYPE_CODE_INT:
                            val = hex(int(val))
                        else:
                            continue

                        if symbol not in m:
                            m[symbol] = val
                current_block = current_block.superblock

            if m:
                return "// " + ", ".join([f"{Color.yellowify(a)}={b}" for a, b in m.items()])
        except Exception:
            pass
        return ""

    def context_trace(self) -> None:
        self.context_title("trace")

        nb_backtrace = self["nb_lines_backtrace"]
        if nb_backtrace <= 0:
            return

        # backward compat for gdb (gdb < 7.10)
        if not hasattr(gdb, "FrameDecorator"):
            gdb.execute(f"backtrace {nb_backtrace:d}")
            return

        orig_frame: gdb.Frame = gdb.selected_frame()
        current_frame: gdb.Frame = gdb.newest_frame()
        frames = [current_frame,]
        while current_frame != orig_frame and current_frame:
            current_frame = current_frame.older()
            if not current_frame: break
            frames.append(current_frame)

        nb_backtrace_before = self["nb_lines_backtrace_before"]
        level = max(len(frames) - nb_backtrace_before - 1, 0)
        current_frame: gdb.Frame = frames[level]

        while current_frame:
            current_frame.select()
            if not current_frame.is_valid():
                continue

            pc = int(current_frame.pc())
            name = current_frame.name()
            items = []
            items.append(f"{pc:#x}")
            if name:
                frame_args = gdb.FrameDecorator.FrameDecorator(current_frame).frame_args() or [] # type: ignore
                symstr= ", ".join([f"{Color.yellowify(x.sym)}={x.sym.value(current_frame)!s}" for x in frame_args])
                m = f"{Color.greenify(name)}({symstr})"
                items.append(m)
            else:
                try:
                    insn = next(gef_disassemble(int(pc), 1))
                except gdb.MemoryError:
                    break

                # check if the gdb symbol table may know the address
                sym_found = gdb_get_location_from_symbol(pc)
                symbol = ""
                if sym_found:
                    sym_name, offset = sym_found
                    symbol = f" <{sym_name}+{offset:x}> "

                items.append(Color.redify(f"{symbol}{insn.mnemonic} {', '.join(insn.operands)}"))

            title = Color.colorify(f"#{level}", "bold green" if current_frame == orig_frame else "bold pink")
            gef_print(f"[{title}] {RIGHT_ARROW.join(items)}")
            older = current_frame.older()
            level += 1
            nb_backtrace -= 1
            if nb_backtrace == 0:
                break
            if not older:
                break
            current_frame = older

        orig_frame.select()
        return

    def context_threads(self) -> None:
        def reason() -> str:
            res = gdb.execute("info program", to_string=True)
            if not res:
                return "NOT RUNNING"

            for line in res.splitlines():
                line = line.strip()
                if line.startswith("It stopped with signal "):
                    return line.replace("It stopped with signal ", "").split(",", 1)[0]
                if line == "The program being debugged is not being run.":
                    return "NOT RUNNING"
                if line == "It stopped at a breakpoint that has since been deleted.":
                    return "TEMPORARY BREAKPOINT"
                if line.startswith("It stopped at breakpoint "):
                    return "BREAKPOINT"
                if line == "It stopped after being stepped.":
                    return "SINGLE STEP"

            return "STOPPED"

        self.context_title("threads")

        threads = gdb.selected_inferior().threads()[::-1]
        idx = self["nb_lines_threads"]
        if idx > 0:
            threads = threads[0:idx]

        if idx == 0:
            return

        if not threads:
            err("No thread selected")
            return

        selected_thread = gdb.selected_thread()
        selected_frame = gdb.selected_frame()

        for i, thread in enumerate(threads):
            line = f"[{Color.colorify(f'#{i:d}', 'bold green' if thread == selected_thread else 'bold pink')}] Id {thread.num:d}, "
            if thread.name:
                line += f"""Name: "{thread.name}", """
            if thread.is_running():
                line += Color.colorify("running", "bold green")
            elif thread.is_stopped():
                line += Color.colorify("stopped", "bold red")
                thread.switch()
                frame = gdb.selected_frame()
                frame_name = frame.name()

                # check if the gdb symbol table may know the address
                if not frame_name:
                    sym_found = gdb_get_location_from_symbol(int(frame.pc()))
                    if sym_found:
                        sym_name, offset = sym_found
                        frame_name = f"<{sym_name}+{offset:x}>"

                line += (f" {Color.colorify(f'{frame.pc():#x}', 'blue')} in "
                         f"{Color.colorify(frame_name or '??', 'bold yellow')} (), "
                         f"reason: {Color.colorify(reason(), 'bold pink')}")
            elif thread.is_exited():
                line += Color.colorify("exited", "bold yellow")
            gef_print(line)
            i += 1

        selected_thread.switch()
        selected_frame.select()
        return

    def context_additional_information(self) -> None:
        if not gef.ui.context_messages:
            return

        self.context_title("extra")
        for level, text in gef.ui.context_messages:
            if level == "error": err(text)
            elif level == "warn": warn(text)
            elif level == "success": ok(text)
            else: info(text)
        return

    def context_memory(self) -> None:
        for address, opt in sorted(gef.ui.watches.items()):
            sz, fmt = opt[0:2]
            self.context_title(f"memory:{address:#x}")
            if fmt == "pointers":
                gdb.execute(f"dereference -l {sz:d} {address:#x}")
            else:
                gdb.execute(f"hexdump {fmt} -s {sz:d} {address:#x}")

    @classmethod
    def update_registers(cls, _) -> None:
        for reg in gef.arch.all_registers:
            try:
                cls.old_registers[reg] = gef.arch.register(reg)
            except Exception:
                cls.old_registers[reg] = 0
        return

    def empty_extra_messages(self, _) -> None:
        gef.ui.context_messages.clear()
        return


@register
class MemoryCommand(GenericCommand):
    """Add or remove address ranges to the memory view."""
    _cmdline_ = "memory"
    _syntax_  = f"{_cmdline_} (watch|unwatch|reset|list)"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        self.usage()
        return


@register
class MemoryWatchCommand(GenericCommand):
    """Adds address ranges to the memory view."""
    _cmdline_ = "memory watch"
    _syntax_  = f"{_cmdline_} ADDRESS [SIZE] [(qword|dword|word|byte|pointers)]"
    _example_ = (f"\n{_cmdline_} 0x603000 0x100 byte"
                 f"\n{_cmdline_} $sp")

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) not in (1, 2, 3):
            self.usage()
            return

        address = parse_address(argv[0])
        size    = parse_address(argv[1]) if len(argv) > 1 else 0x10
        group   = "byte"

        if len(argv) == 3:
            group = argv[2].lower()
            if group not in ("qword", "dword", "word", "byte", "pointers"):
                warn(f"Unexpected grouping '{group}'")
                self.usage()
                return
        else:
            if gef.arch.ptrsize == 4:
                group = "dword"
            elif gef.arch.ptrsize == 8:
                group = "qword"

        gef.ui.watches[address] = (size, group)
        ok(f"Adding memwatch to {address:#x}")
        return


@register
class MemoryUnwatchCommand(GenericCommand):
    """Removes address ranges to the memory view."""
    _cmdline_ = "memory unwatch"
    _syntax_  = f"{_cmdline_} ADDRESS"
    _example_ = (f"\n{_cmdline_} 0x603000"
                 f"\n{_cmdline_} $sp")

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            self.usage()
            return

        address = parse_address(argv[0])
        res = gef.ui.watches.pop(address, None)
        if not res:
            warn(f"You weren't watching {address:#x}")
        else:
            ok(f"Removed memwatch of {address:#x}")
        return


@register
class MemoryWatchResetCommand(GenericCommand):
    """Removes all watchpoints."""
    _cmdline_ = "memory reset"
    _syntax_  = f"{_cmdline_}"

    @only_if_gdb_running
    def do_invoke(self, _: list[str]) -> None:
        gef.ui.watches.clear()
        ok("Memory watches cleared")
        return


@register
class MemoryWatchListCommand(GenericCommand):
    """Lists all watchpoints to display in context layout."""
    _cmdline_ = "memory list"
    _syntax_  = f"{_cmdline_}"

    @only_if_gdb_running
    def do_invoke(self, _: list[str]) -> None:
        if not gef.ui.watches:
            info("No memory watches")
            return

        info("Memory watches:")
        for address, opt in sorted(gef.ui.watches.items()):
            gef_print(f"- {address:#x} ({opt[0]}, {opt[1]})")
        return


@register
class HexdumpCommand(GenericCommand):
    """Display SIZE lines of hexdump from the memory location pointed by LOCATION."""

    _cmdline_ = "hexdump"
    _syntax_  = f"{_cmdline_} (qword|dword|word|byte) [LOCATION] [--size SIZE] [--reverse]"
    _example_ = f"{_cmdline_} byte $rsp --size 16 --reverse"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION, prefix=True)
        self["always_show_ascii"] = (False, "If true, hexdump will always display the ASCII dump")
        self.format: str | None = None
        self.__last_target = "$sp"
        return

    @only_if_gdb_running
    @parse_arguments({"address": "",}, {("--reverse", "-r"): False, ("--size", "-s"): 0})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        valid_formats = ["byte", "word", "dword", "qword"]
        if not self.format or self.format not in valid_formats:
            err("Invalid command")
            return

        args : argparse.Namespace = kwargs["arguments"]
        target = args.address or self.__last_target
        start_addr = parse_address(target)
        read_from = align_address(start_addr)

        if self.format == "byte":
            read_len = args.size or 0x40
            read_from += self.repeat_count * read_len
            mem = gef.memory.read(read_from, read_len)
            lines = hexdump(mem, base=read_from).splitlines()
        else:
            read_len = args.size or 0x10
            lines = self._hexdump(read_from, read_len, self.format, self.repeat_count * read_len)

        if args.reverse:
            lines.reverse()

        self.__last_target = target
        gef_print("\n".join(lines))
        return

    def _hexdump(self, start_addr: int, length: int, arrange_as: str, offset: int = 0) -> list[str]:
        endianness = gef.arch.endianness

        base_address_color = gef.config["theme.dereference_base_address"]
        show_ascii = gef.config["hexdump.always_show_ascii"]

        formats = {
            "qword": ("Q", 8),
            "dword": ("I", 4),
            "word": ("H", 2),
        }

        formatter, width = formats[arrange_as]
        fmt_str = f"{{base}}{VERTICAL_LINE}+{{offset:#06x}}   {{sym}}{{val:#0{width*2+2}x}}   {{text}}"
        fmt_pack = f"{endianness!s}{formatter}"
        lines = []

        i = 0
        text = ""
        while i < length:
            cur_addr = start_addr + (i + offset) * width
            sym = gdb_get_location_from_symbol(cur_addr)
            sym = f"<{sym[0]:s}+{sym[1]:04x}> " if sym else ""
            mem = gef.memory.read(cur_addr, width)
            val = struct.unpack(fmt_pack, mem)[0]
            if show_ascii:
                text = "".join([chr(b) if 0x20 <= b < 0x7F else "." for b in mem])
            lines.append(fmt_str.format(base=Color.colorify(format_address(cur_addr), base_address_color),
                                        offset=(i + offset) * width, sym=sym, val=val, text=text))
            i += 1

        return lines


@register
class HexdumpQwordCommand(HexdumpCommand):
    """Display SIZE lines of hexdump as QWORD from the memory location pointed by ADDRESS."""

    _cmdline_ = "hexdump qword"
    _syntax_  = f"{_cmdline_} [ADDRESS] [--size SIZE] [--reverse]"
    _example_ = f"{_cmdline_} qword $rsp -s 16 --reverse"

    def __init__(self) -> None:
        super().__init__()
        self.format = "qword"
        return


@register
class HexdumpDwordCommand(HexdumpCommand):
    """Display SIZE lines of hexdump as DWORD from the memory location pointed by ADDRESS."""

    _cmdline_ = "hexdump dword"
    _syntax_  = f"{_cmdline_} [ADDRESS] [--size SIZE] [--reverse]"
    _example_ = f"{_cmdline_} $esp -s 16 --reverse"

    def __init__(self) -> None:
        super().__init__()
        self.format = "dword"
        return


@register
class HexdumpWordCommand(HexdumpCommand):
    """Display SIZE lines of hexdump as WORD from the memory location pointed by ADDRESS."""

    _cmdline_ = "hexdump word"
    _syntax_  = f"{_cmdline_} [ADDRESS] [--size SIZE] [--reverse]"
    _example_ = f"{_cmdline_} $esp -s 16 --reverse"

    def __init__(self) -> None:
        super().__init__()
        self.format = "word"
        return


@register
class HexdumpByteCommand(HexdumpCommand):
    """Display SIZE lines of hexdump as BYTE from the memory location pointed by ADDRESS."""

    _cmdline_ = "hexdump byte"
    _syntax_  = f"{_cmdline_} [ADDRESS] [--size SIZE] [--reverse]"
    _example_ = f"{_cmdline_} $rsp -s 16"

    def __init__(self) -> None:
        super().__init__()
        self.format = "byte"
        return


@register
class PatchCommand(GenericCommand):
    """Write specified values to the specified address."""

    _cmdline_ = "patch"
    _syntax_  = (f"{_cmdline_} (qword|dword|word|byte) LOCATION VALUES\n"
                 f"{_cmdline_} string LOCATION \"double-escaped string\"")
    SUPPORTED_SIZES = {
        "qword": (8, "Q"),
        "dword": (4, "L"),
        "word": (2, "H"),
        "byte": (1, "B"),
    }

    def __init__(self) -> None:
        super().__init__(prefix=True, complete=gdb.COMPLETE_LOCATION)
        self.format: str | None = None
        return

    @only_if_gdb_running
    @parse_arguments({"location": "", "values": ["", ]}, {})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        if not self.format or self.format not in self.SUPPORTED_SIZES:
            self.usage()
            return

        if not args.location or not args.values:
            self.usage()
            return

        addr = align_address(parse_address(args.location))
        size, fcode = self.SUPPORTED_SIZES[self.format]
        values = args.values

        if size == 1:
            if values[0].startswith("$_gef"):
                var_name = values[0]
                try:
                    values = str(gdb.parse_and_eval(var_name)).lstrip("{").rstrip("}").replace(",","").split(" ")
                except Exception:
                    gef_print(f"Bad variable specified, check value with command: p {var_name}")
                    return

        d = str(gef.arch.endianness)
        for value in values:
            value = parse_address(value) & ((1 << size * 8) - 1)
            vstr = struct.pack(d + fcode, value)
            gef.memory.write(addr, vstr, length=size)
            addr += size
        return


@register
class PatchQwordCommand(PatchCommand):
    """Write specified QWORD to the specified address."""

    _cmdline_ = "patch qword"
    _syntax_  = f"{_cmdline_} LOCATION QWORD1 [QWORD2 [QWORD3..]]"
    _example_ = f"{_cmdline_} $rip 0x4141414141414141"

    def __init__(self) -> None:
        super().__init__()
        self.format = "qword"
        return


@register
class PatchDwordCommand(PatchCommand):
    """Write specified DWORD to the specified address."""

    _cmdline_ = "patch dword"
    _syntax_  = f"{_cmdline_} LOCATION DWORD1 [DWORD2 [DWORD3..]]"
    _example_ = f"{_cmdline_} $rip 0x41414141"

    def __init__(self) -> None:
        super().__init__()
        self.format = "dword"
        return


@register
class PatchWordCommand(PatchCommand):
    """Write specified WORD to the specified address."""

    _cmdline_ = "patch word"
    _syntax_  = f"{_cmdline_} LOCATION WORD1 [WORD2 [WORD3..]]"
    _example_ = f"{_cmdline_} $rip 0x4141"

    def __init__(self) -> None:
        super().__init__()
        self.format = "word"
        return


@register
class PatchByteCommand(PatchCommand):
    """Write specified BYTE to the specified address."""

    _cmdline_ = "patch byte"
    _syntax_  = f"{_cmdline_} LOCATION BYTE1 [BYTE2 [BYTE3..]]"
    _example_ = f"{_cmdline_} $pc 0x41 0x41 0x41 0x41 0x41"

    def __init__(self) -> None:
        super().__init__()
        self.format = "byte"
        return


@register
class PatchStringCommand(GenericCommand):
    """Write specified string to the specified memory location pointed by ADDRESS."""

    _cmdline_ = "patch string"
    _syntax_  = f"{_cmdline_} ADDRESS \"double backslash-escaped string\""
    _example_ = [
        f"{_cmdline_} $sp \"GEFROCKS\"",
        f"{_cmdline_} $sp \"\\\\x41\\\\x41\\\\x41\\\\x41\""
    ]

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        argc = len(argv)
        if argc != 2:
            self.usage()
            return

        location, msg = argv[0:2]
        addr = align_address(parse_address(location))

        try:
            msg_as_bytes = codecs.escape_decode(msg, "utf-8")[0]
            gef.memory.write(addr, msg_as_bytes, len(msg_as_bytes)) # type: ignore
        except (binascii.Error, gdb.error):
            err(f"Could not decode '\\xXX' encoded string \"{msg}\"")
        return


@lru_cache()
def dereference_from(address: int) -> list[str]:
    if not is_alive():
        return [format_address(address),]

    code_color = gef.config["theme.dereference_code"]
    string_color = gef.config["theme.dereference_string"]
    max_recursion = gef.config["dereference.max_recursion"] or 10
    addr = lookup_address(align_address(address))
    msg = [format_address(addr.value),]
    seen_addrs = set()

    while addr.section and max_recursion:
        if addr.value in seen_addrs:
            msg.append("[loop detected]")
            break
        seen_addrs.add(addr.value)

        max_recursion -= 1

        # Is this value a pointer or a value?
        # -- If it's a pointer, dereference
        deref = addr.dereference()
        if deref is None:
            # if here, dereferencing addr has triggered a MemoryError, no need to go further
            msg.append(str(addr))
            break

        new_addr = lookup_address(deref)
        if new_addr.valid:
            addr = new_addr
            msg.append(str(addr))
            continue

        # -- Otherwise try to parse the value
        if addr.section:
            if addr.section.is_executable() and addr.is_in_text_segment() and not is_ascii_string(addr.value):
                insn = gef_current_instruction(addr.value)
                insn_str = f"{insn.location} {insn.mnemonic} {', '.join(insn.operands)}"
                msg.append(Color.colorify(insn_str, code_color))
                break

            elif addr.section.permission & Permission.READ:
                if is_ascii_string(addr.value):
                    s = gef.memory.read_cstring(addr.value)
                    if len(s) < gef.arch.ptrsize:
                        txt = f'{format_address(deref)} ("{Color.colorify(s, string_color)}"?)'
                    elif len(s) > GEF_MAX_STRING_LENGTH:
                        txt = Color.colorify(f'"{s[:GEF_MAX_STRING_LENGTH]}[...]"', string_color)
                    else:
                        txt = Color.colorify(f'"{s}"', string_color)

                    msg.append(txt)
                    break

        # if not able to parse cleanly, simply display and break
        msg.append(format_address(deref))
        break

    return msg


@register
class DereferenceCommand(GenericCommand):
    """Dereference recursively from an address and display information. This acts like WinDBG `dps`
    command."""

    _cmdline_ = "dereference"
    _syntax_  = f"{_cmdline_} [-h] [--length LENGTH] [--reference REFERENCE] [address]"
    _aliases_ = ["telescope", ]
    _example_ = f"{_cmdline_} --length 20 --reference $sp+0x10 $sp"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        self["max_recursion"] = (7, "Maximum level of pointer recursion")
        return

    @staticmethod
    def pprint_dereferenced(addr: int, idx: int, base_offset: int = 0) -> str:
        base_address_color = gef.config["theme.dereference_base_address"]
        registers_color = gef.config["theme.dereference_register_value"]

        sep = f" {RIGHT_ARROW} "
        memalign = gef.arch.ptrsize

        offset = idx * memalign
        current_address = align_address(addr + offset)
        addrs = dereference_from(current_address)
        addr_l = format_address(int(addrs[0], 16))
        ma = (memalign*2 + 2)
        line = (
            f"{Color.colorify(addr_l, base_address_color)}{VERTICAL_LINE}"
            f"{base_offset+offset:+#07x}: {sep.join(addrs[1:]):{ma}s}"
        )

        register_hints = []

        for regname in gef.arch.all_registers:
            regvalue = gef.arch.register(regname)
            if current_address == regvalue:
                register_hints.append(regname)

        if register_hints:
            m = f"\t{LEFT_ARROW}{', '.join(list(register_hints))}"
            line += Color.colorify(m, registers_color)

        offset += memalign
        return line

    @only_if_gdb_running
    @parse_arguments({"address": "$sp"}, {("-r", "--reference"): "", ("-l", "--length"): 10})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        nb = args.length

        target = args.address
        target_addr = parse_address(target)

        reference = args.reference or target
        ref_addr = parse_address(reference)

        if process_lookup_address(target_addr) is None:
            err(f"Unmapped address: '{target}'")
            return

        if process_lookup_address(ref_addr) is None:
            err(f"Unmapped address: '{reference}'")
            return

        if gef.config["context.grow_stack_down"] is True:
            insnum_step = -1
            if nb > 0:
                from_insnum = nb * (self.repeat_count + 1) - 1
                to_insnum = self.repeat_count * nb - 1
            else:
                from_insnum = self.repeat_count * nb
                to_insnum = nb * (self.repeat_count + 1)
        else:
            insnum_step = 1
            if nb > 0:
                from_insnum = self.repeat_count * nb
                to_insnum = nb * (self.repeat_count + 1)
            else:
                from_insnum = nb * (self.repeat_count + 1) + 1
                to_insnum = (self.repeat_count * nb) + 1

        start_address = align_address(target_addr)
        base_offset = start_address - align_address(ref_addr)

        dereference_cmd = gef.gdb.commands["dereference"]
        assert isinstance(dereference_cmd, DereferenceCommand)
        for i in range(from_insnum, to_insnum, insnum_step):
            gef_print(dereference_cmd.pprint_dereferenced(start_address, i, base_offset))

        return


@register
class ASLRCommand(GenericCommand):
    """View/modify the ASLR setting of GDB. By default, GDB will disable ASLR when it starts the process. (i.e. not
    attached). This command allows to change that setting."""

    _cmdline_ = "aslr"
    _syntax_  = f"{_cmdline_} [(on|off)]"

    def do_invoke(self, argv: list[str]) -> None:
        argc = len(argv)

        if argc == 0:
            ret = gdb.execute("show disable-randomization", to_string=True) or ""
            i = ret.find("virtual address space is ")
            if i < 0:
                return

            msg = "ASLR is currently "
            if ret[i + 25:].strip() == "on.":
                msg += Color.redify("disabled")
            else:
                msg += Color.greenify("enabled")

            gef_print(msg)
            return

        elif argc == 1:
            if argv[0] == "on":
                info("Enabling ASLR")
                gdb.execute("set disable-randomization off")
                return
            elif argv[0] == "off":
                info("Disabling ASLR")
                gdb.execute("set disable-randomization on")
                return

            warn("Invalid command")

        self.usage()
        return


@register
class ResetCacheCommand(GenericCommand):
    """Reset cache of all stored data. This command is here for debugging and test purposes, GEF
    handles properly the cache reset under "normal" scenario."""

    _cmdline_ = "reset-cache"
    _syntax_  = _cmdline_

    def do_invoke(self, _: list[str]) -> None:
        reset_all_caches()
        return


@register
class VMMapCommand(GenericCommand):
    """Display a comprehensive layout of the virtual memory mapping. If a filter argument, GEF will
    filter out the mapping whose pathname do not match that filter."""

    _cmdline_ = "vmmap"
    _syntax_  = f"{_cmdline_} [FILTER]"
    _example_ = f"{_cmdline_} libc"

    @only_if_gdb_running
    @parse_arguments({"unknown_types": [""]}, {("--addr", "-a"): [""], ("--name", "-n"): [""]})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        vmmap = gef.memory.maps
        if not vmmap:
            err("No address mapping information found")
            return

        addrs: dict[str, int] = {x: parse_address(x) for x in args.addr}
        names: list[str] = [x for x in args.name]

        for arg in args.unknown_types:
            if not arg:
                continue

            if self.is_integer(arg):
                addr = int(arg, 0)
            else:
                addr = safe_parse_and_eval(arg)

            if addr is None:
                names.append(arg)
                warn(f"`{arg}` has no type specified. We guessed it was a name filter.")
            else:
                addrs[arg] = int(addr)
                warn(f"`{arg}` has no type specified. We guessed it was an address filter.")
            warn("You can use --name or --addr before the filter value for specifying its type manually.")
            gef_print()

        if not gef.config["gef.disable_color"]:
            self.show_legend()

        color = gef.config["theme.table_heading"]

        headers = ["Start", "End", "Offset", "Perm", "Path"]
        gef_print(Color.colorify("{:<{w}s}{:<{w}s}{:<{w}s}{:<4s} {:s}".format(*headers, w=gef.arch.ptrsize*2+3), color))

        last_printed_filter = None

        for entry in vmmap:
            names_filter = [f"name = '{x}'" for x in names if x in entry.path]
            addrs_filter = [f"addr = {self.format_addr_filter(arg, addr)}" for arg, addr in addrs.items()
                if entry.page_start <= addr < entry.page_end]
            filter_content = f"[{' & '.join([*names_filter, *addrs_filter])}]"

            if not names and not addrs:
                self.print_entry(entry)

            elif names_filter or addrs_filter:
                if filter_content != last_printed_filter:
                    gef_print() # skip a line between different filters
                    gef_print(Color.greenify(filter_content))
                    last_printed_filter = filter_content
                self.print_entry(entry)

        gef_print()
        return

    def format_addr_filter(self, arg: str, addr: int):
        return f"`{arg}`" if self.is_integer(arg) else f"`{arg}` ({addr:#x})"

    def print_entry(self, entry: Section) -> None:
        line_color = ""
        if entry.path == "[stack]":
            line_color = gef.config["theme.address_stack"]
        elif entry.path == "[heap]":
            line_color = gef.config["theme.address_heap"]
        elif entry.permission & Permission.READ and entry.permission & Permission.EXECUTE:
            line_color = gef.config["theme.address_code"]

        line_parts = [
            Color.colorify(format_address(entry.page_start), line_color),
            Color.colorify(format_address(entry.page_end), line_color),
            Color.colorify(format_address(entry.offset), line_color),
        ]
        if entry.permission == Permission.ALL:
            line_parts.append(Color.colorify(str(entry.permission), "underline " + line_color))
        else:
            line_parts.append(Color.colorify(str(entry.permission), line_color))

        line_parts.append(Color.colorify(entry.path, line_color))
        line = " ".join(line_parts)

        gef_print(line)
        return

    def show_legend(self) -> None:
        code_title = Color.colorify("Code", gef.config["theme.address_code"])
        stack_title = Color.colorify("Stack", gef.config["theme.address_stack"])
        heap_title = Color.colorify("Heap", gef.config["theme.address_heap"])
        gef_print(f"[ Legend:  {code_title} | {stack_title} | {heap_title} ]")
        return

    def is_integer(self, n: str) -> bool:
        try:
            int(n, 0)
        except ValueError:
            return False
        return True


@register
class XFilesCommand(GenericCommand):
    """Shows all libraries (and sections) loaded by binary. This command extends the GDB command
    `info files`, by retrieving more information from extra sources, and providing a better
    display. If an argument FILE is given, the output will grep information related to only that file.
    If an argument name is also given, the output will grep to the name within FILE."""

    _cmdline_ = "xfiles"
    _syntax_  = f"{_cmdline_} [FILE [NAME]]"
    _example_ = f"\n{_cmdline_} libc\n{_cmdline_} libc IO_vtables"

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        color = gef.config["theme.table_heading"]
        headers = ["Start", "End", "Name", "File"]
        gef_print(Color.colorify("{:<{w}s}{:<{w}s}{:<21s} {:s}".format(*headers, w=gef.arch.ptrsize*2+3), color))

        filter_by_file = argv[0] if argv and argv[0] else None
        filter_by_name = argv[1] if len(argv) > 1 and argv[1] else None

        for xfile in get_info_files():
            if filter_by_file:
                if filter_by_file not in xfile.filename:
                    continue
                if filter_by_name and filter_by_name not in xfile.name:
                    continue

            line_parts = [
                format_address(xfile.zone_start),
                format_address(xfile.zone_end),
                f"{xfile.name:<21s}",
                xfile.filename,
            ]
            gef_print(" ".join(line_parts))
        return


@register
class XAddressInfoCommand(GenericCommand):
    """Retrieve and display runtime information for the location(s) given as parameter."""

    _cmdline_ = "xinfo"
    _syntax_  = f"{_cmdline_} LOCATION"
    _example_ = f"{_cmdline_} $pc"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_LOCATION)
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            err("At least one valid address must be specified")
            self.usage()
            return

        for sym in argv:
            try:
                addr = align_address(parse_address(sym))
                gef_print(titlify(f"xinfo: {addr:#x}"))
                self.infos(addr)

            except gdb.error as gdb_err:
                err(f"{gdb_err}")
        return

    def infos(self, address: int) -> None:
        addr = lookup_address(address)
        if not addr.valid:
            warn(f"Cannot reach {address:#x} in memory space")
            return

        sect = addr.section
        info = addr.info

        if sect:
            gef_print(f"Page: {format_address(sect.page_start)} {RIGHT_ARROW} "
                      f"{format_address(sect.page_end)} (size={sect.page_end-sect.page_start:#x})"
                      f"\nPermissions: {sect.permission}"
                      f"\nPathname: {sect.path}"
                      f"\nOffset (from page): {addr.value-sect.page_start:#x}"
                      f"\nInode: {sect.inode}")

        if info:
            gef_print(f"Segment: {info.name} "
                      f"({format_address(info.zone_start)}-{format_address(info.zone_end)})"
                      f"\nOffset (from segment): {addr.value-info.zone_start:#x}")

        sym = gdb_get_location_from_symbol(address)
        if sym:
            name, offset = sym
            msg = f"Symbol: {name}"
            if offset:
                msg += f"+{offset:d}"
            gef_print(msg)

        return


@register
class XorMemoryCommand(GenericCommand):
    """XOR a block of memory. The command allows to simply display the result, or patch it
    runtime at runtime."""

    _cmdline_ = "xor-memory"
    _syntax_  = f"{_cmdline_} (display|patch) ADDRESS SIZE KEY"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    def do_invoke(self, _: list[str]) -> None:
        self.usage()
        return


@register
class XorMemoryDisplayCommand(GenericCommand):
    """Display a block of memory pointed by ADDRESS by xor-ing each byte with KEY. The key must be
    provided in hexadecimal format."""

    _cmdline_ = "xor-memory display"
    _syntax_  = f"{_cmdline_} ADDRESS SIZE KEY"
    _example_ = f"{_cmdline_} $sp 16 41414141"

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) != 3:
            self.usage()
            return

        address = parse_address(argv[0])
        length = int(argv[1], 0)
        key = argv[2]
        block = gef.memory.read(address, length)
        info(f"Displaying XOR-ing {address:#x}-{address + len(block):#x} with {key!r}")

        gef_print(titlify("Original block"))
        gef_print(hexdump(block, base=address))

        gef_print(titlify("XOR-ed block"))
        gef_print(hexdump(xor(block, key), base=address))
        return


@register
class XorMemoryPatchCommand(GenericCommand):
    """Patch a block of memory pointed by ADDRESS by xor-ing each byte with KEY. The key must be
    provided in hexadecimal format."""

    _cmdline_ = "xor-memory patch"
    _syntax_  = f"{_cmdline_} ADDRESS SIZE KEY"
    _example_ = f"{_cmdline_} $sp 16 41414141"

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) != 3:
            self.usage()
            return

        address = parse_address(argv[0])
        length = int(argv[1], 0)
        key = argv[2]
        block = gef.memory.read(address, length)
        info(f"Patching XOR-ing {address:#x}-{address + len(block):#x} with {key!r}")
        xored_block = xor(block, key)
        gef.memory.write(address, xored_block, length)
        return


@register
class TraceRunCommand(GenericCommand):
    """Create a runtime trace of all instructions executed from $pc to LOCATION specified. The
    trace is stored in a text file that can be next imported in IDA Pro to visualize the runtime
    path."""

    _cmdline_ = "trace-run"
    _syntax_  = f"{_cmdline_} LOCATION [MAX_CALL_DEPTH]"
    _example_ = f"{_cmdline_} 0x555555554610"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, complete=gdb.COMPLETE_LOCATION)
        self["max_tracing_recursion"] = (1, "Maximum depth of tracing")
        self["tracefile_prefix"] = ("./gef-trace-", "Specify the tracing output file prefix")
        return

    @only_if_gdb_running
    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) not in (1, 2):
            self.usage()
            return

        if len(argv) == 2 and argv[1].isdigit():
            depth = int(argv[1])
        else:
            depth = 1

        try:
            loc_start   = gef.arch.pc
            loc_end     = parse_address(argv[0])
        except gdb.error as e:
            err(f"Invalid location: {e}")
            return

        self.trace(loc_start, loc_end, depth)
        return

    def get_frames_size(self) -> int:
        n = 0
        f = gdb.newest_frame()
        while f:
            n += 1
            f = f.older()
        return n

    def trace(self, loc_start: int, loc_end: int, depth: int) -> None:
        info(f"Tracing from {loc_start:#x} to {loc_end:#x} (max depth={depth:d})")
        logfile = f"{self['tracefile_prefix']}{loc_start:#x}-{loc_end:#x}.txt"
        with RedirectOutputContext(to_file=logfile):
            hide_context()
            self.start_tracing(loc_start, loc_end, depth)
            unhide_context()
        ok(f"Done, logfile stored as '{logfile}'")
        info("Hint: import logfile with `ida_color_gdb_trace.py` script in IDA to visualize path")
        return

    def start_tracing(self, loc_start: int, loc_end: int, depth: int) -> None:
        loc_cur = loc_start
        frame_count_init = self.get_frames_size()

        gef_print("#",
                  f"# Execution tracing of {get_filepath()}",
                  f"# Start address: {format_address(loc_start)}",
                  f"# End address: {format_address(loc_end)}",
                  f"# Recursion level: {depth:d}",
                  "# automatically generated by gef.py",
                  "#\n", sep="\n")

        while loc_cur != loc_end:
            try:
                delta = self.get_frames_size() - frame_count_init

                if delta <= depth:
                    gdb.execute("stepi")
                else:
                    gdb.execute("finish")

                loc_cur = gef.arch.pc
                gdb.flush()

            except gdb.error as e:
                gef_print("#",
                          f"# Execution interrupted at address {format_address(loc_cur)}",
                          f"# Exception: {e}",
                          "#\n", sep="\n")
                break

        return


@register
class PatternCommand(GenericCommand):
    """Generate or Search a De Bruijn Sequence of unique substrings of length N
    and a total length of LENGTH. The default value of N is set to match the
    currently loaded architecture."""

    _cmdline_ = "pattern"
    _syntax_  = f"{_cmdline_} (create|search) ARGS"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        self["length"] = (1024, "Default length of a cyclic buffer to generate")
        return

    def do_invoke(self, _: list[str]) -> None:
        self.usage()
        return


@register
class PatternCreateCommand(GenericCommand):
    """Generate a De Bruijn Sequence of unique substrings of length N and a
    total length of LENGTH. The default value of N is set to match the currently
    loaded architecture."""

    _cmdline_ = "pattern create"
    _syntax_  = f"{_cmdline_} [-h] [-n N] [length]"
    _example_ = [
        f"{_cmdline_} 4096",
        f"{_cmdline_} -n 4 128"
    ]

    @parse_arguments({"length": 0}, {"-n": 0,})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        length = args.length or gef.config["pattern.length"]
        n = args.n or gef.arch.ptrsize
        info(f"Generating a pattern of {length:d} bytes (n={n:d})")
        pattern_str = gef_pystring(generate_cyclic_pattern(length, n))
        gef_print(pattern_str)
        ok(f"Saved as '{gef_convenience(pattern_str)}'")
        return


@register
class PatternSearchCommand(GenericCommand):
    """Search a De Bruijn Sequence of unique substrings of length N and a
    maximum total length of MAX_LENGTH. The default value of N is set to match
    the currently loaded architecture. The PATTERN argument can be a GDB symbol
    (such as a register name), a string or a hexadecimal value"""

    _cmdline_ = "pattern search"
    _syntax_  = f"{_cmdline_} [-h] [-n N] [--max-length MAX_LENGTH] [pattern]"
    _example_ = [f"{_cmdline_} $pc",
                 f"{_cmdline_} 0x61616164",
                 f"{_cmdline_} aaab"]
    _aliases_ = ["pattern offset"]

    @only_if_gdb_running
    @parse_arguments({"pattern": ""}, {("--period", "-n"): 0, ("--max-length", "-l"): 0})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args = kwargs["arguments"]
        if not args.pattern:
            warn("No pattern provided")
            return

        max_length = args.max_length or gef.config["pattern.length"]
        n = args.period or gef.arch.ptrsize
        if n not in (2, 4, 8) or n > gef.arch.ptrsize:
            err("Incorrect value for period")
            return
        self.search(args.pattern, max_length, n)
        return

    def search(self, pattern: str, size: int, period: int) -> None:
        pattern_be, pattern_le = None, None

        # 1. check if it's a symbol (like "$sp" or "0x1337")
        symbol = safe_parse_and_eval(pattern)
        if symbol:
            addr = int(abs(to_unsigned_long(symbol)))
            dereferenced_value = dereference(addr)
            if dereferenced_value:
                addr = int(abs(to_unsigned_long(dereferenced_value)))
            mask = (1<<(8 * period))-1
            addr &= mask
            pattern_le = addr.to_bytes(period, 'little')
            pattern_be = addr.to_bytes(period, 'big')
        else:
            # 2. assume it's a plain string
            pattern_be = gef_pybytes(pattern)
            pattern_le = gef_pybytes(pattern[::-1])

        info(f"Searching for '{pattern_le.hex()}'/'{pattern_be.hex()}' with period={period}")
        cyclic_pattern = generate_cyclic_pattern(size, period)
        off = cyclic_pattern.find(pattern_le)
        if off >= 0:
            ok(f"Found at offset {off:d} (little-endian search) "
               f"{Color.colorify('likely', 'bold red') if gef.arch.endianness == Endianness.LITTLE_ENDIAN else ''}")
            return

        off = cyclic_pattern.find(pattern_be)
        if off >= 0:
            ok(f"Found at offset {off:d} (big-endian search) "
               f"{Color.colorify('likely', 'bold green') if gef.arch.endianness == Endianness.BIG_ENDIAN else ''}")
            return

        err(f"Pattern '{pattern}' not found")
        return


@register
class ChecksecCommand(GenericCommand):
    """Checksec the security properties of the current executable or passed as argument. The
    command checks for the following protections:
    - PIE
    - NX
    - RelRO
    - Glibc Stack Canaries
    - Fortify Source"""

    _cmdline_ = "checksec"
    _syntax_  = f"{_cmdline_} [FILENAME]"
    _example_ = f"{_cmdline_} /bin/ls"

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_FILENAME)
        return

    def do_invoke(self, argv: list[str]) -> None:
        argc = len(argv)

        if argc == 0:
            filename = get_filepath()
            if filename is None:
                warn("No executable/library specified")
                return
        elif argc == 1:
            filename = os.path.realpath(os.path.expanduser(argv[0]))
            if not os.access(filename, os.R_OK):
                err("Invalid filename")
                return
        else:
            self.usage()
            return

        info(f"{self._cmdline_} for '{filename}'")
        self.print_security_properties(filename)
        return

    def print_security_properties(self, filename: str) -> None:
        sec = Elf(filename).checksec
        for prop in sec:
            if prop in ("Partial RelRO", "Full RelRO"): continue
            val = sec[prop]
            msg = Color.greenify(Color.boldify(TICK)) if val is True else Color.redify(Color.boldify(CROSS))
            if val and prop == "Canary" and is_alive():
                canary = gef.session.canary[0] if gef.session.canary else 0
                msg += f"(value: {canary:#x})"

            gef_print(f"{prop:<30s}: {msg}")

        if sec["Full RelRO"]:
            gef_print(f"{'RelRO':<30s}: {Color.greenify('Full')}")
        elif sec["Partial RelRO"]:
            gef_print(f"{'RelRO':<30s}: {Color.yellowify('Partial')}")
        else:
            gef_print(f"{'RelRO':<30s}: {Color.redify(Color.boldify(CROSS))}")
        return


@register
class GotCommand(GenericCommand):
    """Display current status of the got inside the process."""

    _cmdline_ = "got"
    _syntax_ = f"{_cmdline_} [FUNCTION_NAME ...] "
    _example_ = "got read printf exit"

    def __init__(self):
        super().__init__()
        self["function_resolved"] = ("green",
                                     "Line color of the got command output for resolved function")
        self["function_not_resolved"] = ("yellow",
                                         "Line color of the got command output for unresolved function")
        return

    def build_line(self, name: str, color: str, address_val: int, got_address: int) -> str:
        line = f"[{hex(address_val)}] "
        line += Color.colorify(f"{name} {RIGHT_ARROW} {hex(got_address)}", color)
        return line

    @only_if_gdb_running
    @parse_arguments({"symbols": [""]}, {"--all": False})
    def do_invoke(self, _: list[str], **kwargs: Any) -> None:
        args : argparse.Namespace = kwargs["arguments"]
        vmmap = gef.memory.maps
        mapfiles = [mapfile for mapfile in vmmap if
                    (args.all or mapfile.path == str(gef.session.file)) and
                    pathlib.Path(mapfile.realpath).is_file() and
                    mapfile.permission & Permission.EXECUTE]
        for mapfile in mapfiles:
            self.print_got_for(mapfile.path, mapfile.realpath, args.symbols)

    def print_got_for(self, file: str, realpath: str, argv: list[str]) -> None:
        readelf = gef.session.constants["readelf"]

        elf_file = realpath
        elf_virtual_path = file

        func_names_filter = argv if argv else []
        vmmap = gef.memory.maps
        base_address = min(x.page_start for x in vmmap if x.path == elf_virtual_path)
        end_address = max(x.page_end for x in vmmap if x.path == elf_virtual_path)

        # get the checksec output.
        checksec_status = Elf(elf_file).checksec
        relro_status = "Full RelRO"
        full_relro = checksec_status["Full RelRO"]
        pie = checksec_status["PIE"]  # if pie we will have offset instead of abs address.

        if not full_relro:
            relro_status = "Partial RelRO"
            partial_relro = checksec_status["Partial RelRO"]

            if not partial_relro:
                relro_status = "No RelRO"

        # retrieve jump slots using readelf
        lines = gef_execute_external([readelf, "--wide", "--relocs", elf_file], as_list=True)
        jmpslots = [line for line in lines if "JUMP" in line]

        gef_print(f"{titlify(file)}\n\nGOT protection: {relro_status} | GOT functions: {len(jmpslots)}\n ")

        for line in jmpslots:
            address, _, _, _, name = line.split()[:5]

            # if we have a filter let's skip the entries that are not requested.
            if func_names_filter:
                if not any(map(lambda x: x in name, func_names_filter)):
                    continue

            address_val = int(address, 16)

            # address_val is an offset from the base_address if we have PIE.
            if pie or is_remote_debug():
                address_val = base_address + address_val

            # read the address of the function.
            got_address = gef.memory.read_integer(address_val)

            # for the swag: different colors if the function has been resolved or not.
            if base_address < got_address < end_address:
                color = self["function_not_resolved"]
            else:
                color = self["function_resolved"]

            line = self.build_line(name, color, address_val, got_address)
            gef_print(line)
        return


@register
class HighlightCommand(GenericCommand):
    """Highlight user-defined text matches in GEF output universally."""
    _cmdline_ = "highlight"
    _syntax_ = f"{_cmdline_} (add|remove|list|clear)"
    _aliases_ = ["hl"]

    def __init__(self) -> None:
        super().__init__(prefix=True)
        self["regex"] = (False, "Enable regex highlighting")

    def do_invoke(self, _: list[str]) -> None:
        return self.usage()


@register
class HighlightListCommand(GenericCommand):
    """Show the current highlight table with matches to colors."""
    _cmdline_ = "highlight list"
    _aliases_ = ["highlight ls", "hll"]
    _syntax_ = _cmdline_

    def print_highlight_table(self) -> None:
        if not gef.ui.highlight_table:
            err("no matches found")
            return

        left_pad = max(map(len, gef.ui.highlight_table.keys()))
        for match, color in sorted(gef.ui.highlight_table.items()):
            print(f"{Color.colorify(match.ljust(left_pad), color)} {VERTICAL_LINE} "
                  f"{Color.colorify(color, color)}")
        return

    def do_invoke(self, _: list[str]) -> None:
        return self.print_highlight_table()


@register
class HighlightClearCommand(GenericCommand):
    """Clear the highlight table, remove all matches."""
    _cmdline_ = "highlight clear"
    _aliases_ = ["hlc"]
    _syntax_ = _cmdline_

    def do_invoke(self, _: list[str]) -> None:
        return gef.ui.highlight_table.clear()


@register
class HighlightAddCommand(GenericCommand):
    """Add a match to the highlight table."""
    _cmdline_ = "highlight add"
    _syntax_ = f"{_cmdline_} MATCH COLOR"
    _aliases_ = ["highlight set", "hla"]
    _example_ = f"{_cmdline_} 41414141 yellow"

    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) < 2:
            return self.usage()

        match, color = argv
        gef.ui.highlight_table[match] = color
        return


@register
class HighlightRemoveCommand(GenericCommand):
    """Remove a match in the highlight table."""
    _cmdline_ = "highlight remove"
    _syntax_ = f"{_cmdline_} MATCH"
    _aliases_ = [
        "highlight delete",
        "highlight del",
        "highlight unset",
        "highlight rm",
        "hlr",
    ]
    _example_ = f"{_cmdline_} remove 41414141"

    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            return self.usage()

        gef.ui.highlight_table.pop(argv[0], None)
        return


@register
class FormatStringSearchCommand(GenericCommand):
    """Exploitable format-string helper: this command will set up specific breakpoints
    at well-known dangerous functions (printf, snprintf, etc.), and check if the pointer
    holding the format string is writable, and therefore susceptible to format string
    attacks if an attacker can control its content."""
    _cmdline_ = "format-string-helper"
    _syntax_ = _cmdline_
    _aliases_ = ["fmtstr-helper",]

    def do_invoke(self, _: list[str]) -> None:
        dangerous_functions = {
            "printf": 0,
            "sprintf": 1,
            "fprintf": 1,
            "snprintf": 2,
            "vsnprintf": 2,
        }

        nb_installed_breaks = 0

        with RedirectOutputContext(to_file="/dev/null"):
            for function_name in dangerous_functions:
                argument_number = dangerous_functions[function_name]
                FormatStringBreakpoint(function_name, argument_number)
                nb_installed_breaks += 1

        ok(f"Enabled {nb_installed_breaks} FormatString "
           f"breakpoint{'s' if nb_installed_breaks > 1 else ''}")
        return


@register
class HeapAnalysisCommand(GenericCommand):
    """Heap vulnerability analysis helper: this command aims to track dynamic heap allocation
    done through malloc()/free() to provide some insights on possible heap vulnerabilities. The
    following vulnerabilities are checked:
    - NULL free
    - Use-after-Free
    - Double Free
    - Heap overlap"""
    _cmdline_ = "heap-analysis-helper"
    _syntax_ = _cmdline_

    def __init__(self) -> None:
        super().__init__(complete=gdb.COMPLETE_NONE)
        self["check_free_null"] = (False, "Break execution when a free(NULL) is encountered")
        self["check_double_free"] = (True, "Break execution when a double free is encountered")
        self["check_weird_free"] = (True, "Break execution when free() is called against a non-tracked pointer")
        self["check_uaf"] = (True, "Break execution when a possible Use-after-Free condition is found")
        self["check_heap_overlap"] = (True, "Break execution when a possible overlap in allocation is found")

        self.bp_malloc = None
        self.bp_calloc = None
        self.bp_free = None
        self.bp_realloc = None
        return

    @only_if_gdb_running
    @experimental_feature
    def do_invoke(self, argv: list[str]) -> None:
        if not argv:
            self.setup()
            return

        if argv[0] == "show":
            self.dump_tracked_allocations()
        return

    def setup(self) -> None:
        ok("Tracking malloc() & calloc()")
        self.bp_malloc = TraceMallocBreakpoint("__libc_malloc")
        self.bp_calloc = TraceMallocBreakpoint("__libc_calloc")
        ok("Tracking free()")
        self.bp_free = TraceFreeBreakpoint()
        ok("Tracking realloc()")
        self.bp_realloc = TraceReallocBreakpoint()

        ok("Disabling hardware watchpoints (this may increase the latency)")
        gdb.execute("set can-use-hw-watchpoints 0")

        info("Dynamic breakpoints correctly setup, "
             "GEF will break execution if a possible vulnerability is found.")
        warn(f"{Color.colorify('Note', 'bold underline yellow')}: "
             "The heap analysis slows down the execution noticeably.")

        # when inferior quits, we need to clean everything for a next execution
        gef_on_exit_hook(self.clean)
        return

    def dump_tracked_allocations(self) -> None:
        global gef

        if gef.session.heap_allocated_chunks:
            ok("Tracked as in-use chunks:")
            for addr, sz in gef.session.heap_allocated_chunks:
                gef_print(f"{CROSS} malloc({sz:d}) = {addr:#x}")
        else:
            ok("No malloc() chunk tracked")

        if gef.session.heap_freed_chunks:
            ok("Tracked as free-ed chunks:")
            for addr, sz in gef.session.heap_freed_chunks:
                gef_print(f"{TICK}  free({sz:d}) = {addr:#x}")
        else:
            ok("No free() chunk tracked")
        return

    def clean(self, _: "gdb.ExitedEvent") -> None:
        global gef

        ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - Cleaning up")
        for bp in [self.bp_malloc, self.bp_calloc, self.bp_free, self.bp_realloc]:
            if hasattr(bp, "retbp") and bp.retbp:
                try:
                    bp.retbp.delete()
                except RuntimeError:
                    # in some cases, gdb was found failing to correctly remove the retbp
                    # but they can be safely ignored since the debugging session is over
                    pass

            bp.delete()

        for wp in gef.session.heap_uaf_watchpoints:
            wp.delete()

        gef.session.heap_allocated_chunks = []
        gef.session.heap_freed_chunks = []
        gef.session.heap_uaf_watchpoints = []

        ok(f"{Color.colorify('Heap-Analysis', 'yellow bold')} - Re-enabling hardware watchpoints")
        gdb.execute("set can-use-hw-watchpoints 1")

        gef_on_exit_unhook(self.clean)
        return


#
# GDB Function declaration
#
@deprecated("")
def register_function(cls: Type["GenericFunction"]) -> Type["GenericFunction"]:
    """Decorator for registering a new convenience function to GDB."""
    return cls


class GenericFunction(gdb.Function):
    """This is an abstract class for invoking convenience functions, should not be instantiated."""

    _function_ : str
    _syntax_: str = ""
    _example_ : str = ""

    def __init__(self) -> None:
        super().__init__(self._function_)

    def invoke(self, *args: Any) -> int:
        if not is_alive():
            raise gdb.GdbError("No debugging session active")
        return self.do_invoke(args)

    def arg_to_long(self, args: Any, index: int, default: int = 0) -> int:
        try:
            addr = args[index]
            return int(addr) if addr.address is None else int(addr.address)
        except IndexError:
            return default

    def do_invoke(self, args: Any) -> int:
        raise NotImplementedError


@register
class StackOffsetFunction(GenericFunction):
    """Return the current stack base address plus an optional offset."""
    _function_ = "_stack"
    _syntax_   = f"${_function_}()"

    def do_invoke(self, args: list) -> int:
        base = get_section_base_address("[stack]")
        if not base:
            raise gdb.GdbError("Stack not found")

        return self.arg_to_long(args, 0) + base


@register
class HeapBaseFunction(GenericFunction):
    """Return the current heap base address plus an optional offset."""
    _function_ = "_heap"
    _syntax_   = f"${_function_}()"

    def do_invoke(self, args: list[str]) -> int:
        base = gef.heap.base_address
        if not base:
            base = get_section_base_address("[heap]")
            if not base:
                raise gdb.GdbError("Heap not found")
        return self.arg_to_long(args, 0) + base


@register
class SectionBaseFunction(GenericFunction):
    """Return the matching file's base address plus an optional offset.
    Defaults to current file. Note that quotes need to be escaped"""
    _function_ = "_base"
    _syntax_   = "$_base([filepath])"
    _example_  = "p $_base(\\\"/usr/lib/ld-2.33.so\\\")"

    def do_invoke(self, args: list) -> int:
        addr = 0
        try:
            name = args[0].string()
        except IndexError:
            assert gef.session.file
            name = gef.session.file.name
        except gdb.error:
            err(f"Invalid arg: {args[0]}")
            return 0

        try:
            base = get_section_base_address(name)
            if base:
                addr = int(base)
        except TypeError:
            err(f"Cannot find section {name}")
            return 0
        return addr


@register
class BssBaseFunction(GenericFunction):
    """Return the current bss base address plus the given offset."""
    _function_ = "_bss"
    _syntax_   = f"${_function_}([OFFSET])"
    _example_ = "deref $_bss(0x20)"

    def do_invoke(self, args: list) -> int:
        base = get_zone_base_address(".bss")
        if not base:
            raise gdb.GdbError("BSS not found")
        return self.arg_to_long(args, 0) + base


@register
class GotBaseFunction(GenericFunction):
    """Return the current GOT base address plus the given offset."""
    _function_ = "_got"
    _syntax_   = f"${_function_}([OFFSET])"
    _example_ = "deref $_got(0x20)"

    def do_invoke(self, args: list) -> int:
        base = get_zone_base_address(".got")
        if not base:
            raise gdb.GdbError("GOT not found")
        return base + self.arg_to_long(args, 0)


@register
class GefFunctionsCommand(GenericCommand):
    """List the convenience functions provided by GEF."""
    _cmdline_ = "functions"
    _syntax_ = _cmdline_

    def __init__(self) -> None:
        super().__init__()
        self.docs = []
        self.should_refresh = True
        return

    def __add__(self, function: GenericFunction):
        """Add function to documentation."""
        doc = getattr(function, "__doc__", "").lstrip()
        if not hasattr(function, "_syntax_"):
            raise ValueError("Function is invalid")
        syntax = getattr(function, "_syntax_").lstrip()
        msg = f"{Color.colorify(syntax, 'bold cyan')}\n {doc}"
        example = getattr(function, "_example_", "").strip()
        if example:
            msg += f"\n {Color.yellowify('Example:')} {example}"
        self.docs.append(msg)
        return self

    def __radd__(self, function: GenericFunction):
        return self.__add__(function)

    def __str__(self) -> str:
        if self.should_refresh:
            self.__rebuild()
        return self.__doc__ or ""

    def __rebuild(self) -> None:
        """Rebuild the documentation for functions."""
        for function in gef.gdb.functions.values():
            self += function

        self.command_size = len(gef.gdb.commands)
        _, cols = get_terminal_size()
        separator = HORIZONTAL_LINE*cols
        self.__doc__ = f"\n{separator}\n".join(sorted(self.docs))
        self.should_refresh = False
        return

    def do_invoke(self, argv) -> None:
        self.dont_repeat()
        gef_print(titlify("GEF - Convenience Functions"))
        gef_print("These functions can be used as arguments to other "
                  "commands to dynamically calculate values\n")
        gef_print(str(self))
        return


#
# GEF internal command classes
#
class GefCommand(gdb.Command):
    """GEF main command: view all new commands by typing `gef`."""

    _cmdline_ = "gef"
    _syntax_  = f"{_cmdline_} (missing|config|save|restore|set|run)"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, True)
        gef.config["gef.follow_child"] = GefSetting(True, bool, "Automatically set GDB to follow child when forking")
        gef.config["gef.readline_compat"] = GefSetting(False, bool, "Workaround for readline SOH/ETX issue (SEGV)")
        gef.config["gef.debug"] = GefSetting(False, bool, "Enable debug mode for gef")
        gef.config["gef.autosave_breakpoints_file"] = GefSetting("", str, "Automatically save and restore breakpoints")
        gef.config["gef.disable_target_remote_overwrite"] = GefSetting(False, bool, "Disable the overwrite of `target remote`")
        plugins_dir = GefSetting("", str, "Autoload additional GEF commands from external directory", hooks={"on_write": [GefSetting.no_spaces, ]})
        plugins_dir.add_hook("on_changed", [lambda _, new_val: GefSetting.must_exist(new_val), lambda _, new_val: self.load_extra_plugins(new_val), ])
        gef.config["gef.extra_plugins_dir"] = plugins_dir
        gef.config["gef.disable_color"] = GefSetting(False, bool, "Disable all colors in GEF")
        gef.config["gef.tempdir"] = GefSetting(GEF_TEMP_DIR, pathlib.Path, "Directory to use for temporary/cache content", hooks={"on_write": [GefSetting.no_spaces, GefSetting.create_folder_tree]})
        gef.config["gef.show_deprecation_warnings"] = GefSetting(True, bool, "Toggle the display of the `deprecated` warnings")
        gef.config["gef.buffer"] = GefSetting(True, bool, "Internally buffer command output until completion")
        gef.config["gef.bruteforce_main_arena"] = GefSetting(False, bool, "Allow bruteforcing main_arena symbol if everything else fails")
        gef.config["gef.libc_version"] = GefSetting("", str, "Specify libc version when auto-detection fails")
        gef.config["gef.main_arena_offset"] = GefSetting("", str, "Offset from libc base address to main_arena symbol (int or hex). Set to empty string to disable.")
        gef.config["gef.propagate_debug_exception"] = GefSetting(False, bool, "If true, when debug mode is enabled, Python exceptions will be propagated all the way.")

        self.commands : dict[str, GenericCommand] = {}
        self.functions : dict[str, GenericFunction] = {}
        self.missing: dict[str, Exception] = {}
        return

    @property
    @deprecated()
    def loaded_commands(self) -> list[tuple[str, Type[GenericCommand], Any]]:
        raise ObsoleteException("Obsolete loaded_commands")

    @property
    @deprecated()
    def loaded_functions(self) -> list[Type[GenericFunction]]:
        raise ObsoleteException("Obsolete loaded_functions")

    @property
    @deprecated()
    def missing_commands(self) -> dict[str, Exception]:
        raise ObsoleteException("Obsolete missing_commands")

    def setup(self) -> None:
        self.load()

        GefHelpCommand()
        GefConfigCommand()
        GefSaveCommand()
        GefMissingCommand()
        GefSetCommand()
        GefRunCommand()
        GefInstallExtraScriptCommand()

        # At this point, commands (incl. extras) are loaded with default settings.
        # Load custom settings from config file if any
        GefRestoreCommand()
        return

    def load_extra_plugins(self, extra_plugins_dir: pathlib.Path | None = None) -> int:
        """Load the plugins from the gef-extras setting. Returns the number of new plugins added."""
        def load_plugin(fpath: pathlib.Path) -> bool:
            try:
                dbg(f"Loading '{fpath}'")
                gdb.execute(f"source {fpath}")
            except AlreadyRegisteredException:
                pass
            except Exception as e:
                warn(f"Exception while loading {fpath}: {str(e)}")
                return False
            return True

        def load_plugins_from_directory(plugin_directory: pathlib.Path):
            nb_added = -1
            nb_initial = len(__registered_commands__)
            start_time = time.perf_counter()
            for entry in plugin_directory.glob("**/*.py"):
                load_plugin(entry)

            try:
                nb_added = len(__registered_commands__) - nb_initial
                if nb_added > 0:
                    self.load()
                    nb_failed = len(__registered_commands__) - len(self.commands)
                    load_time = time.perf_counter() - start_time
                    ok(f"{Color.colorify(str(nb_added), 'bold green')} extra commands added " \
                       f"in {load_time:.2f} seconds")
                    if nb_failed != 0:
                        warn(f"{Color.colorify(str(nb_failed), 'bold light_gray')} extra commands/functions failed to be added. "
                        "Check `gef missing` to know why")
            except gdb.error as e:
                err(f"failed: {e}")
            return nb_added

        directory = extra_plugins_dir or gef.config["gef.extra_plugins_dir"]
        if not directory:
            return 0
        directory = pathlib.Path(directory).expanduser().absolute()
        if not directory.exists():
            return 0
        dbg(f"Loading extra plugins from directory={directory}")
        return load_plugins_from_directory(directory)

    @property
    def loaded_command_names(self) -> Iterable[str]:
        print("obsolete loaded_command_names")
        return self.commands.keys()

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        gdb.execute("gef help")
        return

    def add_context_layout_mapping(self, current_pane_name: str, display_pane_function: Callable, pane_title_function: Callable, condition: Callable | None) -> None:
        """Add a new context layout mapping."""
        context = self.commands["context"]
        assert isinstance(context, ContextCommand)

        # overload the printing of pane title
        context.layout_mapping[current_pane_name] = (display_pane_function, pane_title_function, condition)

    def add_context_pane(self, pane_name: str, display_pane_function: Callable, pane_title_function: Callable, condition: Callable | None) -> None:
        """Add a new context pane to ContextCommand."""
        # assure users can toggle the new context
        corrected_settings_name: str = pane_name.replace(" ", "_")
        if corrected_settings_name in gef.config["context.layout"]:
            warn(f"Duplicate name for `{pane_name}` (`{corrected_settings_name}`), skipping")
            return

        gef.config["context.layout"] += f" {corrected_settings_name}"
        self.add_context_layout_mapping(corrected_settings_name, display_pane_function, pane_title_function, condition)

    def load(self) -> None:
        """Load all the commands and functions defined by GEF into GDB."""
        current_commands = set(self.commands.keys())
        new_commands = set(x._cmdline_ for x in __registered_commands__) - current_commands
        current_functions = set(self.functions.keys())
        new_functions = set(x._function_ for x in __registered_functions__) - current_functions
        self.missing.clear()
        self.__load_time_ms = time.time()* 1000

        # load all new functions
        for name in sorted(new_functions):
            for function_cls in __registered_functions__:
                if function_cls._function_ == name:
                    assert issubclass(function_cls, GenericFunction)
                    self.functions[name] = function_cls()
                    break

        # load all new commands
        for name in sorted(new_commands):
            try:
                for command_cls in __registered_commands__:
                    if command_cls._cmdline_ == name:
                        assert issubclass(command_cls, GenericCommand)
                        command_instance = command_cls()

                        # create the aliases if any
                        if hasattr(command_instance, "_aliases_"):
                            aliases = getattr(command_instance, "_aliases_")
                            for alias in aliases:
                                GefAlias(alias, name)

                        self.commands[name] = command_instance
                        break

            except Exception as reason:
                self.missing[name] = reason

        self.__load_time_ms = (time.time()* 1000) - self.__load_time_ms
        return


    def show_banner(self) -> None:
        gef_print(f"{Color.greenify('GEF')} for {gef.session.os} ready, "
                  f"type `{Color.colorify('gef', 'underline yellow')}' to start, "
                  f"`{Color.colorify('gef config', 'underline pink')}' to configure")

        ver = f"{sys.version_info.major:d}.{sys.version_info.minor:d}"
        gef_print(f"{Color.colorify(str(len(self.commands)), 'bold green')} commands loaded "
                    f"and {Color.colorify(str(len(self.functions)), 'bold blue')} functions added for "
                    f"GDB {Color.colorify(gdb.VERSION, 'bold yellow')} in {self.__load_time_ms:.2f}ms "
                    f"using Python engine {Color.colorify(ver, 'bold red')}")

        nb_missing = len(self.missing)
        if nb_missing:
                warn(f"{Color.colorify(str(nb_missing), 'bold red')} "
                    f"command{'s' if nb_missing > 1 else ''} could not be loaded, "
                    f"run `{Color.colorify('gef missing', 'underline pink')}` to know why.")
        return


class GefHelpCommand(gdb.Command):
    """GEF help sub-command."""
    _cmdline_ = "gef help"
    _syntax_  = _cmdline_

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False)
        self.docs = []
        self.should_refresh = True
        self.command_size = 0
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        gef_print(titlify("GEF - GDB Enhanced Features"))
        gef_print(str(self))
        return

    def __rebuild(self) -> None:
        """Rebuild the documentation."""
        for name, cmd in gef.gdb.commands.items():
            self += (name, cmd)

        self.command_size = len(gef.gdb.commands)
        _, cols = get_terminal_size()
        separator = HORIZONTAL_LINE*cols
        self.__doc__ = f"\n{separator}\n".join(sorted(self.docs))
        self.should_refresh = False
        return

    def __add__(self, command: tuple[str, GenericCommand]):
        """Add command to GEF documentation."""
        cmd, class_obj = command
        if " " in cmd:
            # do not print subcommands in gef help
            return self
        doc = getattr(class_obj, "__doc__", "").lstrip()
        aliases = f"Aliases: {', '.join(class_obj._aliases_)}" if hasattr(class_obj, "_aliases_") else ""
        msg = f"{Color.colorify(cmd, 'bold red')}\n{doc}\n{aliases}"
        self.docs.append(msg)
        return self

    def __radd__(self, command: tuple[str, GenericCommand]):
        return self.__add__(command)

    def __str__(self) -> str:
        """Lazily regenerate the `gef help` object if it was modified"""
        # quick check in case the docs have changed
        if self.should_refresh or self.command_size != len(gef.gdb.commands):
            self.__rebuild()
        return self.__doc__ or ""


class GefConfigCommand(gdb.Command):
    """GEF configuration sub-command
    This command will help set/view GEF settings for the current debugging session.
    It is possible to make those changes permanent by running `gef save` (refer
    to this command help), and/or restore previously saved settings by running
    `gef restore` (refer help).
    """
    _cmdline_ = "gef config"
    _syntax_  = f"{_cmdline_} [setting_name] [setting_value]"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_NONE, prefix=False)
        return

    def invoke(self, args: str, from_tty: bool) -> None:
        self.dont_repeat()
        argv = gdb.string_to_argv(args)
        argc = len(argv)

        if not (0 <= argc <= 2):
            err("Invalid number of arguments")
            return

        if argc == 0:
            gef_print(titlify("GEF configuration settings"))
            self.print_settings()
            return

        if argc == 1:
            prefix = argv[0]
            names = [x for x in gef.config.keys() if x.startswith(prefix)]
            if names:
                if len(names) == 1:
                    gef_print(titlify(f"GEF configuration setting: {names[0]}"))
                    self.print_setting(names[0], verbose=True)
                else:
                    gef_print(titlify(f"GEF configuration settings matching '{argv[0]}'"))
                    for name in names: self.print_setting(name)
            return

        if not is_debug():
            try:
                self.set_setting(argv)
            except (ValueError, KeyError) as e:
                err(str(e))
        else:
            # Let exceptions (if any) propagate
            self.set_setting(argv)
        return

    def print_setting(self, plugin_name: str, verbose: bool = False) -> None:
        res = gef.config.raw_entry(plugin_name)
        string_color = gef.config["theme.dereference_string"]
        misc_color = gef.config["theme.dereference_base_address"]

        if not res:
            return

        _setting = Color.colorify(plugin_name, "green")
        _type = res.type.__name__
        if _type == "str":
            _value = f'"{Color.colorify(res.value, string_color)}"'
        else:
            _value = Color.colorify(res.value, misc_color)

        gef_print(f"{_setting} ({_type}) = {_value}")

        if verbose:
            gef_print(Color.colorify("\nDescription:", "bold underline"))
            gef_print(f"\t{res.description}")
        return

    def print_settings(self) -> None:
        for x in sorted(gef.config):
            self.print_setting(x)
        return

    def set_setting(self, argv: list[str]) -> bool:
        global gef
        key, new_value = argv

        if "." not in key:
            err("Invalid command format")
            return False

        loaded_commands = list( gef.gdb.commands.keys()) + ["gef"]
        plugin_name = key.split(".", 1)[0]
        if plugin_name not in loaded_commands:
            err(f"Unknown plugin '{plugin_name}'")
            return False

        if key not in gef.config:
            dbg(f"'{key}' is not a valid configuration setting")
            return False

        _type = gef.config.raw_entry(key).type

        # Attempt to parse specific values for known types
        if _type is bool:
            if new_value.upper() in ("TRUE", "T", "1"):
                _newval = True
            elif new_value.upper() in ("FALSE", "F", "0"):
                _newval = False
            else:
                raise ValueError(f"Cannot parse '{new_value}' as bool")
        else:
            _newval = _type(new_value)

        gef.config[key] = _newval

        reset_all_caches()
        return True

    def complete(self, text: str, word: str) -> list[str]:
        settings = sorted(gef.config)

        if text == "":
            # no prefix: example: `gef config TAB`
            return [s for s in settings if word in s]

        if "." not in text:
            # if looking for possible prefix
            return [s for s in settings if s.startswith(text.strip())]

        # finally, look for possible values for given prefix
        return [s.split(".", 1)[1] for s in settings if s.startswith(text.strip())]


class GefSaveCommand(gdb.Command):
    """GEF save sub-command.
    Saves the current configuration of GEF to disk (by default in file '~/.gef.rc')."""
    _cmdline_ = "gef save"
    _syntax_  = _cmdline_

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False)
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        cfg = configparser.RawConfigParser()
        old_sect = None

        # save the configuration
        for key in sorted(gef.config):
            sect, optname = key.split(".", 1)
            value = gef.config[key]

            if old_sect != sect:
                cfg.add_section(sect)
                old_sect = sect

            cfg.set(sect, optname, value)

        # save the aliases
        cfg.add_section("aliases")
        for alias in gef.session.aliases:
            cfg.set("aliases", alias.alias, alias.command)

        with GEF_RC.open("w") as fd:
            cfg.write(fd)

        ok(f"Configuration saved to '{GEF_RC}'")
        return


class GefRestoreCommand(gdb.Command):
    """GEF restore sub-command.
    Loads settings from file '~/.gef.rc' and apply them to the configuration of GEF."""
    _cmdline_ = "gef restore"
    _syntax_  = _cmdline_

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False)
        self.reload(True)
        return

    def invoke(self, args: str, from_tty: bool) -> None:
        self.dont_repeat()
        if GEF_RC.is_file():
            quiet = (args.lower() == "quiet")
            self.reload(quiet)
        return

    def reload(self, quiet: bool):
        cfg = configparser.ConfigParser()
        cfg.read(GEF_RC)

        for section in cfg.sections():
            if section == "aliases":
                # load the aliases
                for key in cfg.options(section):
                    try:
                        GefAlias(key, cfg.get(section, key))
                    except Exception as e:
                        dbg(f"GefAlias() raised exception {e}")
                continue

            # load the other options
            for optname in cfg.options(section):
                key = f"{section}.{optname}"
                try:
                    setting = gef.config.raw_entry(key)
                except Exception:
                    continue
                new_value = cfg.get(section, optname)
                if setting.type is bool:
                    new_value = True if new_value.upper() in ("TRUE", "T", "1") else False
                setting.value = setting.type(new_value)

        if not quiet:
            ok(f"Configuration from '{Color.colorify(str(GEF_RC), 'bold blue')}' restored")
        return


class GefMissingCommand(gdb.Command):
    """GEF missing sub-command
    Display the GEF commands that could not be loaded, along with the reason of why
    they could not be loaded.
    """
    _cmdline_ = "gef missing"
    _syntax_  = _cmdline_

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False)
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        missing_commands: dict[str, Exception] = gef.gdb.missing
        if not missing_commands:
            ok("No missing command")
            return

        for cmd, exc in missing_commands.items():
            warn(f"Missing `{cmd}`: reason: {str(exc)})")
        return


class GefSetCommand(gdb.Command):
    """Override GDB set commands with the context from GEF."""
    _cmdline_ = "gef set"
    _syntax_  = f"{_cmdline_} [GDB_SET_ARGUMENTS]"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_SYMBOL, False)
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        args = args.split()
        cmd = ["set", args[0],]
        for p in args[1:]:
            if p.startswith("$_gef"):
                c = gdb.parse_and_eval(p)
                cmd.append(c.string())
            else:
                cmd.append(p)

        gdb.execute(" ".join(cmd))
        return


class GefRunCommand(gdb.Command):
    """Override GDB run commands with the context from GEF.
    Simple wrapper for GDB run command to use arguments set from `gef set args`."""
    _cmdline_ = "gef run"
    _syntax_  = f"{_cmdline_} [GDB_RUN_ARGUMENTS]"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_FILENAME, False)
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()
        if is_alive():
            gdb.execute("continue")
            return

        argv = args.split()
        gdb.execute(f"gef set args {' '.join(argv)}")
        gdb.execute("run")
        return


class GefAlias(gdb.Command):
    """Simple aliasing wrapper because GDB doesn't do what it should."""

    def __init__(self, alias: str, command: str, completer_class: int = gdb.COMPLETE_NONE, command_class: int = gdb.COMMAND_NONE) -> None:
        p = command.split()
        if not p:
            return

        if any(x for x in gef.session.aliases if x.alias == alias):
            return

        self.command = command
        self.alias = alias
        c = command.split()[0]
        r = self.lookup_command(c)
        self.__doc__ = f"Alias for '{Color.greenify(command)}'"
        if r is not None:
            _instance = r[1]
            self.__doc__ += f": {_instance.__doc__}"

            if hasattr(_instance,  "complete"):
                self.complete = _instance.complete

        super().__init__(alias, command_class, completer_class=completer_class)
        gef.session.aliases.append(self)
        return

    def __repr__(self) -> str:
        return f"GefAlias(from={self.alias}, to={self.command})"

    def __str__(self) -> str:
        return f"GefAlias(from={self.alias}, to={self.command})"

    def invoke(self, args: Any, from_tty: bool) -> None:
        gdb.execute(f"{self.command} {args}", from_tty=from_tty)
        return

    def lookup_command(self, cmd: str) -> tuple[str, GenericCommand] | None:
        global gef
        for _name, _instance in gef.gdb.commands.items():
            if cmd == _name:
                return _name, _instance

        return None


@register
class AliasesCommand(GenericCommand):
    """Base command to add, remove, or list aliases."""

    _cmdline_ = "aliases"
    _syntax_  = f"{_cmdline_} (add|rm|ls)"

    def __init__(self) -> None:
        super().__init__(prefix=True)
        return

    def do_invoke(self, _: list[str]) -> None:
        self.usage()
        return


@register
class AliasesAddCommand(AliasesCommand):
    """Command to add aliases."""

    _cmdline_ = "aliases add"
    _syntax_  = f"{_cmdline_} [ALIAS] [COMMAND]"
    _example_ = f"{_cmdline_} scope telescope"

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, argv: list[str]) -> None:
        if len(argv) < 2:
            self.usage()
            return
        GefAlias(argv[0], " ".join(argv[1:]))
        return


@register
class AliasesRmCommand(AliasesCommand):
    """Command to remove aliases."""

    _cmdline_ = "aliases rm"
    _syntax_ = f"{_cmdline_} [ALIAS]"

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, argv: list[str]) -> None:
        global gef
        if len(argv) != 1:
            self.usage()
            return
        try:
            alias_to_remove = next(filter(lambda x: x.alias == argv[0], gef.session.aliases))
            gef.session.aliases.remove(alias_to_remove)
        except (ValueError, StopIteration):
            err(f"{argv[0]} not found in aliases.")
            return
        gef_print("You must reload GEF for alias removals to apply.")
        return


@register
class AliasesListCommand(AliasesCommand):
    """Command to list aliases."""

    _cmdline_ = "aliases ls"
    _syntax_ = _cmdline_

    def __init__(self) -> None:
        super().__init__()
        return

    def do_invoke(self, _: list[str]) -> None:
        ok("Aliases defined:")
        for a in gef.session.aliases:
            gef_print(f"{a.alias:30s} {RIGHT_ARROW} {a.command}")
        return


class GefTmuxSetup(gdb.Command):
    """Setup a comfortable tmux debugging environment."""

    def __init__(self) -> None:
        super().__init__("tmux-setup", gdb.COMMAND_NONE, gdb.COMPLETE_NONE)
        GefAlias("screen-setup", "tmux-setup")
        return

    def invoke(self, args: Any, from_tty: bool) -> None:
        self.dont_repeat()

        tmux = os.getenv("TMUX")
        if tmux:
            self.tmux_setup()
            return

        screen = os.getenv("TERM")
        if screen is not None and screen == "screen":
            self.screen_setup()
            return

        warn("Not in a tmux/screen session")
        return

    def tmux_setup(self) -> None:
        """Prepare the tmux environment by vertically splitting the current pane, and
        forcing the context to be redirected there."""
        tmux = which("tmux")
        ok("tmux session found, splitting window...")

        pane, pty = subprocess.check_output([tmux, "splitw", "-h", '-F#{session_name}:#{window_index}.#{pane_index}-#{pane_tty}', "-P"]).decode().strip().split("-")
        atexit.register(lambda : subprocess.run([tmux, "kill-pane", "-t", pane]))
        # clear the screen and let it wait for input forever
        gdb.execute(f"!'{tmux}' send-keys -t {pane} 'clear ; cat' C-m")
        gdb.execute(f"!'{tmux}' select-pane -L")

        ok(f"Setting `context.redirect` to '{pty}'...")
        gdb.execute(f"gef config context.redirect {pty}")
        ok("Done!")
        return

    def screen_setup(self) -> None:
        """Hackish equivalent of the tmux_setup() function for screen."""
        screen = which("screen")
        sty = os.getenv("STY")
        ok("screen session found, splitting window...")
        fd_script, script_path = tempfile.mkstemp()
        fd_tty, tty_path = tempfile.mkstemp()
        os.close(fd_tty)

        with os.fdopen(fd_script, "w") as f:
            f.write("startup_message off\n")
            f.write("split -v\n")
            f.write("focus right\n")
            f.write(f"screen bash -c 'tty > {tty_path}; clear; cat'\n")
            f.write("focus left\n")

        gdb.execute(f"!'{screen}' -r '{sty}' -m -d -X source {script_path}")
        # artificial delay to make sure `tty_path` is populated
        time.sleep(0.25)
        with open(tty_path, "r") as f:
            pty = f.read().strip()
        ok(f"Setting `context.redirect` to '{pty}'...")
        gdb.execute(f"gef config context.redirect {pty}")
        ok("Done!")
        os.unlink(script_path)
        os.unlink(tty_path)
        return


class GefInstallExtraScriptCommand(gdb.Command):
    """`gef install` command: installs one or more scripts from the `gef-extras` script repo. Note that the command
    doesn't check for external dependencies the script(s) might require."""
    _cmdline_ = "gef install"
    _syntax_  = f"{_cmdline_} SCRIPTNAME [SCRIPTNAME [SCRIPTNAME...]]"

    def __init__(self) -> None:
        super().__init__(self._cmdline_, gdb.COMMAND_SUPPORT, gdb.COMPLETE_NONE, False)
        self.branch = gef.config.get("gef.extras_default_branch", GEF_EXTRAS_DEFAULT_BRANCH)
        return

    def invoke(self, argv: str, from_tty: bool) -> None:
        self.dont_repeat()
        if not argv:
            err("No script name provided")
            return

        args = argv.split()

        if "--list" in args or "-l" in args:
            subprocess.run(["xdg-open", f"https://github.com/hugsy/gef-extras/{self.branch}/"])
            return

        self.dirpath = gef.config["gef.tempdir"].expanduser().absolute()
        if not self.dirpath.is_dir():
            err("'gef.tempdir' is not a valid directory")
            return

        for script in args:
            script = script.lower()
            if not self.__install_extras_script(script):
                warn(f"Failed to install '{script}', skipping...")
        return


    def __install_extras_script(self, script: str) -> bool:
        fpath = self.dirpath / f"{script}.py"
        if not fpath.exists():
            url = f"https://raw.githubusercontent.com/hugsy/gef-extras/{self.branch}/scripts/{script}.py"
            info(f"Searching for '{script}.py' in `gef-extras@{self.branch}`...")
            data = http_get(url)
            if not data:
                warn("Not found")
                return False

            with fpath.open("wb") as fd:
                fd.write(data)
                fd.flush()

        old_command_set = set(gef.gdb.commands)
        gdb.execute(f"source {fpath}")
        new_command_set = set(gef.gdb.commands)
        new_commands = [f"`{c[0]}`" for c in (new_command_set - old_command_set)]
        ok(f"Installed file '{fpath}', new command(s) available: {', '.join(new_commands)}")
        return True


#
# GEF internal  classes
#

def __gef_prompt__(current_prompt: Callable[[Callable], str]) -> str:
    """GEF custom prompt function."""
    if gef.config["gef.readline_compat"] is True: return GEF_PROMPT
    if gef.config["gef.disable_color"] is True: return GEF_PROMPT
    prompt = gef.session.remote.mode.prompt_string() if gef.session.remote else ""
    prompt += "(core) " if is_target_coredump() else ""
    prompt += GEF_PROMPT_ON if is_alive() else GEF_PROMPT_OFF
    return prompt


class GefManager(metaclass=abc.ABCMeta):
    def reset_caches(self) -> None:
        """Reset the LRU-cached attributes"""
        for attr in dir(self):
            try:
                obj = getattr(self, attr)
                if not hasattr(obj, "cache_clear"):
                    continue
                obj.cache_clear()
            except Exception:
                # we're resetting the cache here, we don't care if (or which) exception triggers
                continue
        return


class GefMemoryManager(GefManager):
    """Class that manages memory access for gef."""
    def __init__(self) -> None:
        self.reset_caches()
        return

    def reset_caches(self) -> None:
        super().reset_caches()
        self.__maps: list[Section] | None = None
        return

    def write(self, address: int, buffer: ByteString, length: int | None = None) -> None:
        """Write `buffer` at address `address`."""
        length = length or len(buffer)
        gdb.selected_inferior().write_memory(address, buffer, length)

    def read(self, addr: int, length: int = 0x10) -> bytes:
        """Return a `length` long byte array with the copy of the process memory at `addr`."""
        return gdb.selected_inferior().read_memory(addr, length).tobytes()

    def read_integer(self, addr: int) -> int:
        """Return an integer read from memory."""
        sz = gef.arch.ptrsize
        mem = self.read(addr, sz)
        unpack = u32 if sz == 4 else u64
        return unpack(mem)

    def read_cstring(self,
                     address: int,
                     max_length: int = GEF_MAX_STRING_LENGTH,
                     encoding: str | None = None) -> str:
        """Return a C-string read from memory."""
        encoding = encoding or "unicode-escape"
        length = min(address | (DEFAULT_PAGE_SIZE-1), max_length+1)

        try:
            res_bytes = self.read(address, length)
        except gdb.error:
            current_address = address
            res_bytes = b""
            while len(res_bytes) < length:
                try:
                    # Calculate how many bytes there are until next page
                    next_page = current_address + DEFAULT_PAGE_SIZE
                    page_mask = ~(DEFAULT_PAGE_SIZE - 1)
                    size = (next_page & page_mask) - current_address

                    # Read until the end of the current page
                    res_bytes += self.read(current_address, size)

                    current_address += size
                except gdb.error:
                    if not res_bytes:
                        err(f"Can't read memory at '{address:#x}'")
                        return ""
                    break
        try:
            with warnings.catch_warnings():
                # ignore DeprecationWarnings (see #735)
                warnings.simplefilter("ignore")
                res = res_bytes.decode(encoding, "strict")
        except UnicodeDecodeError:
            # latin-1 as fallback due to its single-byte to glyph mapping
            res = res_bytes.decode("latin-1", "replace")

        res = res.split("\x00", 1)[0]
        ustr = res.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
        if max_length and len(res) > max_length:
            return f"{ustr[:max_length]}[...]"
        return ustr

    def read_ascii_string(self, address: int) -> str | None:
        """Read an ASCII string from memory"""
        cstr = self.read_cstring(address)
        if isinstance(cstr, str) and cstr and all(x in string.printable for x in cstr):
            return cstr
        return None

    @property
    def maps(self) -> list[Section]:
        if not self.__maps:
            maps = self.__parse_maps()
            if not maps:
                raise RuntimeError("Failed to determine memory layout")
            self.__maps = maps
        return self.__maps

    def __parse_maps(self) -> list[Section] | None:
        """Return the mapped memory sections. If the current arch has its maps
        method defined, then defer to that to generated maps, otherwise, try to
        figure it out from procfs, then info sections, then monitor info
        mem."""
        if gef.arch.maps is not None:
            return list(gef.arch.maps())

        # Coredumps are the only case where `maintenance info sections` collected more
        # info than `info proc sections`.so use this unconditionally. See #1154

        if is_target_coredump():
            return list(self.parse_gdb_maintenance_info_sections())

        try:
            return list(self.parse_gdb_info_proc_maps())
        except Exception:
            pass

        try:
            return list(self.parse_procfs_maps())
        except Exception:
            pass

        try:
            return list(self.parse_monitor_info_mem())
        except Exception:
            pass

        raise RuntimeError("Failed to get memory layout")

    @staticmethod
    def parse_procfs_maps() -> Generator[Section, None, None]:
        """Get the memory mapping from procfs."""
        procfs_mapfile = gef.session.maps
        if not procfs_mapfile:
            is_remote = gef.session.remote is not None
            raise FileNotFoundError(f"Missing {'remote ' if is_remote else ''}procfs map file")

        with procfs_mapfile.open("r") as fd:
            for line in fd:
                line = line.strip()
                addr, perm, off, _, rest = line.split(" ", 4)
                rest = rest.split(" ", 1)
                if len(rest) == 1:
                    inode = rest[0]
                    pathname = ""
                else:
                    inode = rest[0]
                    pathname = rest[1].lstrip()

                addr_start, addr_end = parse_string_range(addr)
                off = int(off, 16)
                perm = Permission.from_process_maps(perm)
                inode = int(inode)
                yield Section(page_start=addr_start,
                              page_end=addr_end,
                              offset=off,
                              permission=perm,
                              inode=inode,
                              path=pathname)
        return

    @staticmethod
    def parse_gdb_info_proc_maps() -> Generator[Section, None, None]:
        """Get the memory mapping from GDB's command `info proc mappings`."""
        if GDB_VERSION < (11, 0):
            raise AttributeError("Disregarding old format")

        output = (gdb.execute("info proc mappings", to_string=True) or "")
        if not output:
            raise AttributeError

        start_idx = output.find("Start Addr")
        if start_idx == -1:
            raise AttributeError

        output = output[start_idx:]
        lines = output.splitlines()
        if len(lines) < 2:
            raise AttributeError

        # The function assumes the following output format (as of GDB 11+) for `info proc mappings`:
        # - live process (incl. remote)
        # ```
        #           Start Addr           End Addr       Size     Offset  Perms  objfile
        #       0x555555554000     0x555555558000     0x4000        0x0  r--p   /usr/bin/ls
        #       0x555555558000     0x55555556c000    0x14000     0x4000  r-xp   /usr/bin/ls
        # [...]
        # ```
        # or
        # - coredump & rr
        # ```
        #          Start Addr           End Addr       Size     Offset objfile
        #      0x555555554000     0x555555558000     0x4000        0x0 /usr/bin/ls
        #      0x555555558000     0x55555556c000    0x14000     0x4000 /usr/bin/ls
        # ```
        # In the latter case the 'Perms' header is missing, so mock the Permission to `rwx` so
        # `dereference` will still work.

        mock_permission = all(map(lambda x: x.strip() != "Perms", lines[0].split()))
        for line in lines[1:]:
            if not line:
                break

            parts = [x.strip() for x in line.split()]
            addr_start, addr_end, _, offset = [int(x, 16) for x in parts[0:4]]
            if mock_permission:
                perm = Permission(7)
                path = " ".join(parts[4:]) if len(parts) >= 4 else ""
            else:
                perm = Permission.from_process_maps(parts[4])
                path = " ".join(parts[5:]) if len(parts) >= 5 else ""
            yield Section(
                page_start=addr_start,
                page_end=addr_end,
                offset=offset,
                permission=perm,
                path=path,
            )
        return

    @staticmethod
    def parse_monitor_info_mem() -> Generator[Section, None, None]:
        """Get the memory mapping from GDB's command `monitor info mem`
        This can raise an exception, which the memory manager takes to mean
        that this method does not work to get a map.
        """
        stream = StringIO(gdb.execute("monitor info mem", to_string=True))

        for line in stream:
            try:
                ranges, off, perms = line.split()
                off = int(off, 16)
                start, end = [int(s, 16) for s in ranges.split("-")]
            except ValueError:
                continue

            perm = Permission.from_monitor_info_mem(perms)
            yield Section(page_start=start,
                          page_end=end,
                          offset=off,
                          permission=perm)

    @staticmethod
    def parse_gdb_maintenance_info_sections() -> Generator[Section, None, None]:
        """Get the memory mapping from GDB's command `maintenance info sections` (limited info). In some cases (i.e. coredumps),
        the memory info collected by `info proc sections` is insufficient."""
        stream = StringIO(gdb.execute("maintenance info sections", to_string=True))

        for line in stream:
            if not line:
                break

            try:
                parts = line.split()
                addr_start, addr_end = [int(x, 16) for x in parts[1].split("->")]
                off = int(parts[3][:-1], 16)
                path = parts[4]
                perm = Permission.NONE
                if "DATA" in parts[5:]:
                    perm |= Permission.READ | Permission.WRITE
                if "CODE" in parts[5:]:
                    perm |= Permission.READ | Permission.EXECUTE
                yield Section(
                    page_start=addr_start,
                    page_end=addr_end,
                    offset=off,
                    permission=perm,
                    path=path,
                )

            except IndexError:
                continue
            except ValueError:
                continue

    @staticmethod
    def parse_info_mem():
        """Get the memory mapping from GDB's command `info mem`. This can be
        provided by certain gdbserver implementations."""
        for line in StringIO(gdb.execute("info mem", to_string=True)):
            # Using memory regions provided by the target.
            # Num Enb Low Addr   High Addr  Attrs
            # 0   y   0x10000000 0x10200000 flash blocksize 0x1000 nocache
            # 1   y   0x20000000 0x20042000 rw nocache
            _, en, start, end, *attrs = line.split()
            if en != "y":
                continue

            if "flash" in attrs:
                perm = Permission.from_info_mem("r")
            else:
                perm = Permission.from_info_mem("rw")
            yield Section(page_start=int(start, 0),
                          page_end=int(end, 0),
                          permission=perm)

    def append(self, section: Section):
        if not self.maps:
            raise AttributeError("No mapping defined")
        if not isinstance(section, Section):
            raise TypeError("section has an invalid type")

        assert self.__maps
        for s in self.__maps:
            if section.overlaps(s):
                raise RuntimeError(f"{section} overlaps {s}")
        self.__maps.append(section)
        return self

    def __iadd__(self, section: Section):
        return self.append(section)


class GefHeapManager(GefManager):
    """Class managing session heap."""
    def __init__(self) -> None:
        self.reset_caches()
        return

    def reset_caches(self) -> None:
        self.__libc_main_arena: GlibcArena | None = None
        self.__libc_selected_arena: GlibcArena | None = None
        self.__heap_base = None
        return

    @property
    def main_arena(self) -> GlibcArena | None:
        if not self.__libc_main_arena:
            try:
                __main_arena_addr = GefHeapManager.find_main_arena_addr()
                self.__libc_main_arena = GlibcArena(f"*{__main_arena_addr:#x}")
                # the initialization of `main_arena` also defined `selected_arena`, so
                # by default, `main_arena` == `selected_arena`
                self.selected_arena = self.__libc_main_arena
            except Exception:
                # the search for arena can fail when the session is not started
                pass
        return self.__libc_main_arena

    @main_arena.setter
    def main_arena(self, value: GlibcArena) -> None:
        self.__libc_main_arena = value
        return

    @staticmethod
    @lru_cache()
    def find_main_arena_addr() -> int:
        assert gef.libc.version
        """A helper function to find the glibc `main_arena` address, either from
        symbol, from its offset from `__malloc_hook` or by brute force."""
        # Before anything else, use libc offset from config if available
        if gef.config["gef.main_arena_offset"]:
            try:
                libc_base = get_section_base_address("libc")
                offset: int = gef.config["gef.main_arena_offset"]
                if libc_base:
                    dbg(f"Using main_arena_offset={offset:#x} from config")
                    addr = libc_base + offset

                    # Verify the found address before returning
                    if GlibcArena.verify(addr):
                        return addr
            except gdb.error:
                pass

        # First, try to find `main_arena` symbol directly
        try:
            return parse_address(f"&{LIBC_HEAP_MAIN_ARENA_DEFAULT_NAME}")
        except gdb.error:
            pass

        # Second, try to find it by offset from `__malloc_hook`
        if gef.libc.version < (2, 34):
            try:
                malloc_hook_addr = parse_address("(void *)&__malloc_hook")

                struct_size = ctypes.sizeof(GlibcArena.malloc_state_t())

                if is_x86():
                    addr = align_address_to_size(malloc_hook_addr + gef.arch.ptrsize, 0x20)
                elif is_arch(Elf.Abi.AARCH64):
                    addr = malloc_hook_addr - gef.arch.ptrsize*2 - struct_size
                elif is_arch(Elf.Abi.ARM):
                    addr = malloc_hook_addr - gef.arch.ptrsize - struct_size
                else:
                    addr = None

                # Verify the found address before returning
                if addr and GlibcArena.verify(addr):
                    return addr

            except gdb.error:
                pass

        # Last resort, try to find it via brute force if enabled in settings
        if gef.config["gef.bruteforce_main_arena"]:
            alignment = 0x8
            try:
                dbg("Trying to bruteforce main_arena address")
                # setup search_range for `main_arena` to `.data` of glibc
                def search_filter(zone: Zone) -> bool:
                    return "libc" in pathlib.Path(zone.filename).name and zone.name == ".data"

                for dotdata in list(filter(search_filter, get_info_files())):
                    search_range = range(dotdata.zone_start, dotdata.zone_end, alignment)
                    # find first possible candidate
                    for addr in search_range:
                        if GlibcArena.verify(addr):
                            dbg(f"Found candidate at {addr:#x}")
                            return addr
                    dbg("Bruteforce not successful")
            except Exception:
                pass

        # Nothing helped
        err_msg = f"Cannot find main_arena for {gef.arch.arch}. You might want to set a manually found libc offset "
        if not gef.config["gef.bruteforce_main_arena"]:
            err_msg += "or allow bruteforcing "
        err_msg += "through the GEF config."
        raise OSError(err_msg)

    @property
    def selected_arena(self) -> GlibcArena | None:
        if not self.__libc_selected_arena:
            # `selected_arena` must default to `main_arena`
            self.__libc_selected_arena = self.main_arena
        return self.__libc_selected_arena

    @selected_arena.setter
    def selected_arena(self, value: GlibcArena) -> None:
        self.__libc_selected_arena = value
        return

    @property
    def arenas(self) -> list | Iterator[GlibcArena]:
        if not self.main_arena:
            return []
        return iter(self.main_arena)

    @property
    def base_address(self) -> int | None:
        if not self.__heap_base:
            base = 0
            try:
                base = parse_address("mp_->sbrk_base")
                base = self.malloc_align_address(base)
            except gdb.error:
                # missing symbol, try again
                base = 0
            if not base:
                base = get_section_base_address("[heap]")
            self.__heap_base = base
        return self.__heap_base

    @property
    def chunks(self) -> list | Iterator:
        if not self.base_address:
            return []
        return iter(GlibcChunk(self.base_address, from_base=True))

    @property
    def min_chunk_size(self) -> int:
        return 4 * gef.arch.ptrsize

    @property
    def malloc_alignment(self) -> int:
        assert gef.libc.version
        __default_malloc_alignment = 0x10
        if gef.libc.version >= (2, 26) and is_x86_32():
            # Special case introduced in Glibc 2.26:
            # https://elixir.bootlin.com/glibc/glibc-2.26/source/sysdeps/i386/malloc-alignment.h#L22
            return __default_malloc_alignment
        # Generic case:
        # https://elixir.bootlin.com/glibc/glibc-2.26/source/sysdeps/generic/malloc-alignment.h#L22
        return 2 * gef.arch.ptrsize

    def csize2tidx(self, size: int) -> int:
        return abs((size - self.min_chunk_size + self.malloc_alignment - 1)) // self.malloc_alignment

    def tidx2size(self, idx: int) -> int:
        return idx * self.malloc_alignment + self.min_chunk_size

    def malloc_align_address(self, address: int) -> int:
        """Align addresses according to glibc's MALLOC_ALIGNMENT. See also Issue #689 on Github"""
        def ceil(n: float) -> int:
            return int(-1 * n // 1 * -1)
        malloc_alignment = self.malloc_alignment
        return malloc_alignment * ceil((address / malloc_alignment))


class GefSetting:
    """Basic class for storing gef settings as objects"""

    def __init__(self, value: Any, cls: type | None = None, description: str | None = None, hooks: dict[str, list[Callable]] | None = None)  -> None:
        self.value = value
        self.type = cls or type(value)
        self.description = description or ""
        self.hooks: dict[str, list[Callable]] = collections.defaultdict(list)
        if not hooks:
            hooks = {"on_read": [], "on_write": [], "on_changed": []}

        for access, funcs in hooks.items():
            self.add_hook(access, funcs)
        return

    def __str__(self) -> str:
        return f"Setting(type={self.type.__name__}, value='{self.value}', desc='{self.description[:10]}...', " \
                f"read_hooks={len(self.hooks['on_read'])}, write_hooks={len(self.hooks['on_write'])}, "\
                f"changed_hooks={len(self.hooks['on_changed'])})"

    def add_hook(self, access: str, funcs: list[Callable]):
        if access not in ("on_read", "on_write", "on_changed"):
            raise ValueError("invalid access type")
        for func in funcs:
            if not callable(func):
                raise ValueError("hook is not callable")
            self.hooks[access].append(func)
        return self

    @staticmethod
    def no_spaces(value: pathlib.Path):
        if " " in str(value):
            raise ValidationError("setting cannot contain spaces")

    @staticmethod
    def must_exist(value: pathlib.Path):
        if not value or not pathlib.Path(value).expanduser().absolute().exists():
            raise ValidationError("specified path must exist")

    @staticmethod
    def create_folder_tree(value: pathlib.Path):
        value.mkdir(0o755, exist_ok=True, parents=True)


class GefSettingsManager(dict):
    """
    GefSettings acts as a dict where the global settings are stored and can be read, written or deleted as any other dict.
    For instance, to read a specific command setting: `gef.config[mycommand.mysetting]`
    """
    def __getitem__(self, name: str) -> Any:
        setting : GefSetting = super().__getitem__(name)
        self.__invoke_read_hooks(setting)
        return setting.value

    def __setitem__(self, name: str, value: Any) -> None:
        # check if the key exists
        if super().__contains__(name):
            # if so, update its value directly
            setting = super().__getitem__(name)
            if not isinstance(setting, GefSetting): raise TypeError
            new_value = setting.type(value)
            dbg(f"in __invoke_changed_hooks(\"{name}\"), setting.value={setting.value} -> new_value={new_value}, changing={bool(setting.value != new_value)}")
            self.__invoke_changed_hooks(setting, new_value)
            self.__invoke_write_hooks(setting, new_value)
            setting.value = new_value
            return

        # if not, assert `value` is a GefSetting, then insert it
        if not isinstance(value, GefSetting): raise TypeError("Invalid argument")
        if not value.type: raise TypeError("Invalid type")
        if not value.description: raise AttributeError("Invalid description")
        setting = value
        value = setting.value
        self.__invoke_write_hooks(setting, value)
        super().__setitem__(name, setting)
        return

    def __delitem__(self, name: str) -> None:
        return super().__delitem__(name)

    def raw_entry(self, name: str) -> GefSetting:
        return super().__getitem__(name)

    def __invoke_read_hooks(self, setting: GefSetting) -> None:
        for callback in setting.hooks["on_read"]:
            callback()
        return

    def __invoke_changed_hooks(self, setting: GefSetting, new_value: Any) -> None:
        old_value = setting.value
        if old_value == new_value:
            return
        for callback in setting.hooks["on_changed"]:
            callback(old_value, new_value)

    def __invoke_write_hooks(self, setting: GefSetting, new_value: Any) -> None:
        for callback in setting.hooks["on_write"]:
            callback(new_value)


class GefSessionManager(GefManager):
    """Class managing the runtime properties of GEF. """
    def __init__(self) -> None:
        self.reset_caches()
        self.remote: "GefRemoteSessionManager | None" = None
        self.remote_initializing: bool = False
        self.qemu_mode: bool = False
        self.coredump_mode: bool | None = None
        self.convenience_vars_index: int = 0
        self.heap_allocated_chunks: list[tuple[int, int]] = []
        self.heap_freed_chunks: list[tuple[int, int]] = []
        self.heap_uaf_watchpoints: list[UafWatchpoint] = []
        self.pie_breakpoints: dict[int, PieVirtualBreakpoint] = {}
        self.pie_counter: int = 1
        self.aliases: list[GefAlias] = []
        self.modules: list[FileFormat] = []
        self.constants = {} # a dict for runtime constants (like 3rd party file paths)
        for constant in ("python3", "readelf", "nm", "file", "ps"):
            self.constants[constant] = which(constant)
        return

    def reset_caches(self) -> None:
        super().reset_caches()
        self._auxiliary_vector = None
        self._pagesize = None
        self._os = None
        self._pid = None
        self._file = None
        self._maps: pathlib.Path | None = None
        self._root: pathlib.Path | None = None
        return

    def __str__(self) -> str:
        _type = "Local" if self.remote is None else f"Remote/{self.remote.mode}"
        return f"Session(type={_type}, pid={self.pid or 'Not running'}, os='{self.os}')"

    def __repr__(self) -> str:
        return str(self)

    @property
    def auxiliary_vector(self) -> dict[str, int] | None:
        if not is_alive():
            return None
        if is_qemu_system():
            return None
        if not self._auxiliary_vector:
            auxiliary_vector = {}
            auxv_info = gdb.execute("info auxv", to_string=True) or ""
            if not auxv_info or "failed" in auxv_info:
                err("Failed to query auxiliary variables")
                return None
            for line in auxv_info.splitlines():
                line = line.split('"')[0].strip()  # remove the ending string (if any)
                line = line.split()  # split the string by whitespace(s)
                if len(line) < 4:
                    continue
                __av_type = line[1]
                __av_value = line[-1]
                auxiliary_vector[__av_type] = int(__av_value, base=0)
            self._auxiliary_vector = auxiliary_vector
        return self._auxiliary_vector

    @property
    def os(self) -> str:
        """Return the current OS."""
        if not self._os:
            self._os = platform.system().lower()
        return self._os

    @property
    def pid(self) -> int:
        """Return the PID of the target process."""
        if not self._pid:
            pid = gdb.selected_inferior().pid if not self.qemu_mode else gdb.selected_thread().ptid[1]
            if not pid:
                raise RuntimeError("cannot retrieve PID for target process")
            self._pid = pid
        return self._pid

    @property
    def file(self) -> pathlib.Path | None:
        """Return a Path object of the target process."""
        if self.remote is not None:
            return self.remote.file
        progspace = gdb.current_progspace()
        assert progspace
        fpath: str = progspace.filename or ""
        if fpath and not self._file:
            self._file = pathlib.Path(fpath).expanduser()
        return self._file

    @property
    def cwd(self) -> pathlib.Path | None:
        if self.remote is not None:
            return self.remote.root
        return self.file.parent if self.file else None

    @property
    def pagesize(self) -> int:
        """Get the system page size"""
        auxval = self.auxiliary_vector
        if not auxval:
            return DEFAULT_PAGE_SIZE
        self._pagesize = auxval["AT_PAGESZ"]
        return self._pagesize

    @property
    def canary(self) -> tuple[int, int] | None:
        """Return a tuple of the canary address and value, read from the canonical
        location if supported by the architecture. Otherwise, read from the auxiliary
        vector."""
        try:
            canary_location = gef.arch.canary_address()
            canary = gef.memory.read_integer(canary_location)
        except (NotImplementedError, gdb.error):
            # Fall back to `AT_RANDOM`, which is the original source
            # of the canary value but not the canonical location
            return self.original_canary
        return canary, canary_location

    @property
    def original_canary(self) -> tuple[int, int] | None:
        """Return a tuple of the initial canary address and value, read from the
        auxiliary vector."""
        auxval = self.auxiliary_vector
        if not auxval:
            return None
        canary_location = auxval["AT_RANDOM"]
        canary = gef.memory.read_integer(canary_location)
        canary &= ~0xFF
        return canary, canary_location

    @property
    def maps(self) -> pathlib.Path | None:
        """Returns the Path to the procfs entry for the memory mapping."""
        if not is_alive():
            return None
        if not self._maps:
            if self.remote is not None:
                self._maps = self.remote.maps
            else:
                self._maps = pathlib.Path(f"/proc/{self.pid}/maps")
        return self._maps

    @property
    def root(self) -> pathlib.Path | None:
        """Returns the path to the process's root directory."""
        if not is_alive():
            return None
        if not self._root:
            self._root = pathlib.Path(f"/proc/{self.pid}/root")
        return self._root


class GefRemoteSessionManager(GefSessionManager):
    """Class for managing remote sessions with GEF. It will create a temporary environment
    designed to clone the remote one."""

    class RemoteMode(enum.IntEnum):
        GDBSERVER = 0
        QEMU = 1
        RR = 2

        def __str__(self):
            return self.name

        def __repr__(self):
            return f"RemoteMode = {str(self)} ({int(self)})"

        def prompt_string(self) -> str:
            match self:
                case GefRemoteSessionManager.RemoteMode.QEMU:
                    return Color.boldify("(qemu) ")
                case GefRemoteSessionManager.RemoteMode.RR:
                    return Color.boldify("(rr) ")
                case GefRemoteSessionManager.RemoteMode.GDBSERVER:
                    return Color.boldify("(remote) ")
            raise AttributeError("Unknown value")

    def __init__(self, host: str, port: int, pid: int =-1, qemu: pathlib.Path | None = None) -> None:
        super().__init__()
        self.__host = host
        self.__port = port
        self.__local_root_fd = tempfile.TemporaryDirectory()
        self.__local_root_path = pathlib.Path(self.__local_root_fd.name)
        self.__qemu = qemu
        if pid > 0:
            self._pid = pid

        if self.__qemu is not None:
            self._mode = GefRemoteSessionManager.RemoteMode.QEMU
        elif os.environ.get("GDB_UNDER_RR", None) == "1":
            self._mode =  GefRemoteSessionManager.RemoteMode.RR
        else:
            self._mode =  GefRemoteSessionManager.RemoteMode.GDBSERVER

    def close(self) -> None:
        self.__local_root_fd.cleanup()
        try:
            gef_on_new_unhook(self.remote_objfile_event_handler)
            gef_on_new_hook(new_objfile_handler)
        except Exception as e:
            warn(f"Exception while restoring local context: {str(e)}")
            raise

    def __str__(self) -> str:
        return f"RemoteSession(target='{self.target}', local='{self.root}', pid={self.pid}, mode={self.mode})"

    def __repr__(self) -> str:
        return str(self)

    @property
    def target(self) -> str:
        return f"{self.__host}:{self.__port}"

    @property
    def root(self) -> pathlib.Path:
        return self.__local_root_path.absolute()

    @property
    def file(self) -> pathlib.Path:
        """Path to the file being debugged as seen by the remote endpoint."""
        if not self._file:
            progspace = gdb.current_progspace()
            if not progspace:
                raise RuntimeError("No session started")
            filename = progspace.filename
            if not filename:
                raise RuntimeError("No session started")
            start_idx = len("target:") if filename.startswith("target:") else 0
            self._file = pathlib.Path(filename[start_idx:])
        return self._file

    @property
    def lfile(self) -> pathlib.Path:
        """Local path to the file being debugged."""
        return self.root / str(self.file).lstrip("/")

    @property
    def maps(self) -> pathlib.Path:
        if not self._maps:
            self._maps = self.root / f"proc/{self.pid}/maps"
        return self._maps

    @property
    def mode(self) -> RemoteMode:
        return self._mode

    def sync(self, src: str, dst: str | None = None) -> bool:
        """Copy the `src` into the temporary chroot. If `dst` is provided, that path will be
        used instead of `src`."""
        if not dst:
            dst = src
        tgt = self.root / dst.lstrip("/")
        if tgt.exists():
            return True
        tgt.parent.mkdir(parents=True, exist_ok=True)
        dbg(f"[remote] downloading '{src}' -> '{tgt}'")
        gdb.execute(f"remote get '{src}' '{tgt.absolute()}'")
        return tgt.exists()

    def connect(self, pid: int) -> bool:
        """Connect to remote target. If in extended mode, also attach to the given PID."""
        # before anything, register our new hook to download files from the remote target
        dbg("[remote] Installing new objfile handlers")
        try:
            gef_on_new_unhook(new_objfile_handler)
        except SystemError:
            # the default objfile handler might already have been removed, ignore failure
            pass

        gef_on_new_hook(self.remote_objfile_event_handler)

        # then attempt to connect
        is_extended_mode = (pid > -1)
        dbg(f"[remote] Enabling extended remote: {bool(is_extended_mode)}")
        try:
            with DisableContextOutputContext():
                cmd = f"target {'extended-' if is_extended_mode else ''}remote {self.target}"
                dbg(f"[remote] Executing '{cmd}'")
                gdb.execute(cmd)
                if is_extended_mode:
                    gdb.execute(f"attach {pid:d}")
            return True
        except Exception as e:
            err(f"Failed to connect to {self.target}: {e}")

        # a failure will trigger the cleanup, deleting our hook anyway
        return False

    def setup(self) -> bool:
        # setup remote adequately depending on remote or qemu mode
        match self.mode:
            case GefRemoteSessionManager.RemoteMode.QEMU:
                dbg(f"Setting up as qemu session, target={self.__qemu}")
                self.__setup_qemu()
            case GefRemoteSessionManager.RemoteMode.RR:
                dbg("Setting up as rr session")
                self.__setup_rr()
            case GefRemoteSessionManager.RemoteMode.GDBSERVER:
                dbg("Setting up as remote session")
                self.__setup_remote()
            case _:
                raise ValueError

        # refresh gef to consider the binary
        reset_all_caches()
        gef.binary = Elf(self.lfile)
        reset_architecture()
        return True

    def __setup_qemu(self) -> bool:
        # setup emulated file in the chroot
        assert self.__qemu
        target = self.root / str(self.__qemu.parent).lstrip("/")
        target.mkdir(parents=True, exist_ok=False)
        shutil.copy2(self.__qemu, target)
        self._file = self.__qemu
        assert self.lfile.exists()

        # create a procfs
        procfs = self.root / f"proc/{self.pid}/"
        procfs.mkdir(parents=True, exist_ok=True)

        ## /proc/pid/cmdline
        cmdline = procfs / "cmdline"
        if not cmdline.exists():
            with cmdline.open("w") as fd:
                fd.write("")

        ## /proc/pid/environ
        environ = procfs / "environ"
        if not environ.exists():
            with environ.open("wb") as fd:
                fd.write(b"PATH=/bin\x00HOME=/tmp\x00")

        ## /proc/pid/maps
        maps = procfs / "maps"
        if not maps.exists():
            with maps.open("w") as fd:
                fname = self.file.absolute()
                mem_range = "00000000-ffffffff" if is_32bit() else "0000000000000000-ffffffffffffffff"
                fd.write(f"{mem_range} rwxp 00000000 00:00 0                    {fname}\n")
        return True

    def __setup_remote(self) -> bool:
        # get the file
        fpath = f"/proc/{self.pid}/exe"
        if not self.sync(fpath, str(self.file)):
            err(f"'{fpath}' could not be fetched on the remote system.")
            return False

        # pseudo procfs
        for _file in ("maps", "environ", "cmdline"):
            fpath = f"/proc/{self.pid}/{_file}"
            if not self.sync(fpath):
                err(f"'{fpath}' could not be fetched on the remote system.")
                return False

        return True

    def __setup_rr(self) -> bool:
        #
        # Simply override the local root path, the binary must exist
        # on the host.
        #
        self.__local_root_path = pathlib.Path("/")
        return True

    def remote_objfile_event_handler(self, evt: "gdb.NewObjFileEvent") -> None:
        dbg(f"[remote] in remote_objfile_handler({evt.new_objfile.filename if evt else 'None'}))")
        if not evt or not evt.new_objfile.filename:
            return
        if not evt.new_objfile.filename.startswith("target:") and not evt.new_objfile.filename.startswith("/"):
            warn(f"[remote] skipping '{evt.new_objfile.filename}'")
            return
        if evt.new_objfile.filename.startswith("target:"):
            src: str = evt.new_objfile.filename[len("target:"):]
            if not self.sync(src):
                raise FileNotFoundError(f"Failed to sync '{src}'")
        return


class GefUiManager(GefManager):
    """Class managing UI settings."""
    def __init__(self) -> None:
        self.redirect_fd : TextIOWrapper | None = None
        self.context_hidden = False
        self.stream_buffer : StringIO | None = None
        self.highlight_table: dict[str, str] = {}
        self.watches: dict[int, tuple[int, str]] = {}
        self.context_messages: list[tuple[str, str]] = []
        return


class GefLibcManager(GefManager):
    """Class managing everything libc-related (except heap)."""
    PATTERN_LIBC_VERSION_MEMORY = re.compile(rb"glibc (\d+)\.(\d+)")
    PATTERN_LIBC_VERSION_FILENAME = re.compile(r"libc6?[-_](\d+)\.(\d+)\.so")

    def __init__(self) -> None:
        self._version : tuple[int, int] | None = None
        self._patch: int | None = None
        self._release: str | None = None
        return

    def __str__(self) -> str:
        return f"Libc(version='{self.version}')"

    @property
    def version(self) -> tuple[int, int] | None:
        if not is_alive():
            return None

        if not self._version:
            self._version = GefLibcManager.find_libc_version()

        # Whenever auto-detection fails, try use the user-provided version.
        if self._version == (0, 0):
            if gef.config["gef.libc_version"]:
                ver = [int(v) for v in gef.config["gef.libc_version"].split(".", 1)]
                assert len(ver) >= 2
                self._version = ver[0], ver[1]

        return self._version

    @staticmethod
    @lru_cache()
    def find_libc_version() -> tuple[int, int]:
        """Attempt to determine the libc version. This operation can be long."""
        libc_sections = (m for m in gef.memory.maps if "libc" in m.path and m.permission & Permission.READ)
        for section in libc_sections:
            # Try to determine from the filepath
            match = re.search(GefLibcManager.PATTERN_LIBC_VERSION_FILENAME, section.path)
            if match:
                return int(match.group(1)), int(match.group(2))

            # Try to determine from memory
            try:
                mem = gef.memory.read(section.page_start, section.size)
                match = re.search(GefLibcManager.PATTERN_LIBC_VERSION_MEMORY, mem)
                if match:
                    return int(match.group(1)), int(match.group(2))
            except gdb.MemoryError:
                continue

        return 0, 0


class Gef:
    """The GEF root class, which serves as a entrypoint for all the debugging session attributes (architecture,
    memory, settings, etc.)."""
    binary: FileFormat | None
    arch: Architecture
    config : GefSettingsManager
    ui: GefUiManager
    libc: GefLibcManager
    memory : GefMemoryManager
    heap : GefHeapManager
    session : GefSessionManager
    gdb: GefCommand

    def __init__(self) -> None:
        self.binary: FileFormat | None = None
        self.arch: Architecture = GenericArchitecture() # see PR #516, will be reset by `new_objfile_handler`
        self.arch_reason: str = "This is the default architecture"
        self.config = GefSettingsManager()
        self.ui = GefUiManager()
        self.libc = GefLibcManager()
        return

    def __str__(self) -> str:
        return f"Gef(binary='{self.binary or 'None'}', arch={self.arch})"

    def reinitialize_managers(self) -> None:
        """Reinitialize the managers. Avoid calling this function directly, using `pi reset()` is preferred"""
        self.memory = GefMemoryManager()
        self.heap = GefHeapManager()
        self.session = GefSessionManager()
        return

    def setup(self) -> None:
        """Setup initialize the runtime setup, which may require for the `gef` to be not None."""
        self.reinitialize_managers()
        self.gdb = GefCommand()
        self.gdb.setup()
        gdb.execute(f"save gdb-index '{self.config['gef.tempdir']}'")
        return

    def reset_caches(self) -> None:
        """Recursively clean the cache of all the managers. Avoid calling this function directly, using `reset-cache`
        is preferred"""
        for mgr in (self.memory, self.heap, self.session, self.arch):
            mgr.reset_caches()
        return


def target_remote_posthook():
    if gef.session.remote_initializing:
        return

    gef.session.remote = GefRemoteSessionManager("", 0)
    if not gef.session.remote.setup():
        raise EnvironmentError(f"Failed to create a proper environment for {gef.session.remote}")

if __name__ == "__main__":
    if sys.version_info[0] == 2:
        err("GEF has dropped Python2 support for GDB when it reached EOL on 2020/01/01.")
        err("If you require GEF for GDB+Python2, use https://github.com/hugsy/gef-legacy.")
        exit(1)

    if GDB_VERSION < GDB_MIN_VERSION or PYTHON_VERSION < PYTHON_MIN_VERSION:
        err("You're using an old version of GDB. GEF will not work correctly. "
            f"Consider updating to GDB {'.'.join(map(str, GDB_MIN_VERSION))} or higher "
            f"(with Python {'.'.join(map(str, PYTHON_MIN_VERSION))} or higher).")
        exit(1)

    # setup config
    gdb_initial_settings = (
        "set confirm off",
        "set verbose off",
        "set pagination off",
        "set print elements 0",
        "set history save on",
        f"set history filename {os.getenv('GDBHISTFILE', '~/.gdb_history')}",
        "set output-radix 0x10",
        "set print pretty on",
        "set disassembly-flavor intel",
        "handle SIGALRM print nopass",
    )
    for cmd in gdb_initial_settings:
        try:
            gdb.execute(cmd)
        except gdb.error:
            pass

    # load GEF, set up the managers and load the plugins, functions,
    gef = Gef()
    reset()
    assert isinstance(gef, Gef)
    gef.gdb.load()
    gef.gdb.show_banner()

    # load config
    if gef.gdb.load_extra_plugins():
        # reload settings
        gdb.execute("gef restore")

    # setup gdb prompt
    gdb.prompt_hook = __gef_prompt__

    # gdb events configuration
    gef_on_continue_hook(continue_handler)
    gef_on_stop_hook(hook_stop_handler)
    gef_on_new_hook(new_objfile_handler)
    gef_on_exit_hook(exit_handler)
    gef_on_memchanged_hook(memchanged_handler)
    gef_on_regchanged_hook(regchanged_handler)

    progspace = gdb.current_progspace()
    if progspace and progspace.filename:
        # if here, we are sourcing gef from a gdb session already attached, force call to new_objfile (see issue #278)
        new_objfile_handler(None)

    GefTmuxSetup()

    if GDB_VERSION > (9, 0):
        disable_tr_overwrite_setting = "gef.disable_target_remote_overwrite"

        if not gef.config[disable_tr_overwrite_setting]:
            warnmsg = ("Using `target remote` with GEF should work in most cases, "
                       "but use `gef-remote` if you can. You can disable the "
                       "overwrite of the `target remote` command by toggling "
                       f"`{disable_tr_overwrite_setting}` in the config.")
            hook = f"""
                define target hookpost-{{}}
                pi target_remote_posthook()
                context
                pi if calling_function() != "connect": warn("{warnmsg}")
                end
            """

            # Register a post-hook for `target remote` that initialize the remote session
            gdb.execute(hook.format("remote"))
            gdb.execute(hook.format("extended-remote"))
        else:
            errmsg = ("Using `target remote` does not work, use `gef-remote` "
                      f"instead. You can toggle `{disable_tr_overwrite_setting}` "
                      "if this is not desired.")
            hook = f"""pi if calling_function() != "connect": err("{errmsg}")"""
            gdb.execute(f"define target hook-remote\n{hook}\nend")
            gdb.execute(f"define target hook-extended-remote\n{hook}\nend")

    # restore saved breakpoints (if any)
    bkp_fpath = pathlib.Path(gef.config["gef.autosave_breakpoints_file"]).expanduser().absolute()
    if bkp_fpath.is_file():
        gdb.execute(f"source {bkp_fpath}")

    # Add a `source` post hook to force gef to recheck the registered plugins and
    # eventually load the missing one(s)
    gdb.execute("define hookpost-source\npi gef.gdb.load()\nend")
