Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion archinfo/arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,11 +863,14 @@ class ArchNotFound(Exception):
pass


def arch_from_id(ident: str, endness=Endness.ANY, bits="") -> Arch:
def arch_from_id(ident: str, endness: str = Endness.ANY, bits: int | str = "") -> Arch:
"""
Take our best guess at the arch referred to by the given identifier, and return an instance of its class.

You may optionally provide the ``endness`` and ``bits`` parameters (strings) to help this function out.

An identifier that no registered architecture claims at the requested width and endness is
looked up among the sleigh languages pypcode ships, so an ArchPcode may come back.
"""
if bits == 64 or (isinstance(bits, str) and "64" in bits):
bits = 64
Expand Down Expand Up @@ -914,6 +917,15 @@ def arch_from_id(ident: str, endness=Endness.ANY, bits="") -> Arch:
cls = acls
break
if not cls:
# Nothing registered matches, so fall back to the sleigh languages that pypcode ships. They
# cannot be registered in arch_id_map because register_arch builds an architecture from an
# endness, while ArchPcode is built from a language. The import is delayed because
# arch_pcode imports this module.
from .arch_pcode import ArchPcode # pylint: disable=import-outside-toplevel

pcode_arch = ArchPcode.from_id(ident, endness, bits)
if pcode_arch is not None:
return pcode_arch
raise ArchNotFound(
f"Can't find architecture info for architecture {ident} with {repr(bits)} bits and {endness} endness"
)
Expand Down
103 changes: 101 additions & 2 deletions archinfo/arch_pcode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
from typing import Union

from .arch import Arch, Endness, Register
Expand All @@ -16,6 +17,17 @@

log = logging.getLogger(__name__)

_SLEIGH_ENDNESS = {"little": Endness.LE, "big": Endness.BE}


def _normalize_arch_name(name: str) -> str:
"""
Reduce an architecture name to a form that can be compared across the conventions used by sleigh
language definitions and by the loaders that call :func:`archinfo.arch_from_id`, e.g. ``PA-RISC``,
``pa-risc:BE:32:default`` and ``EM_PARISC`` all reduce to ``parisc``.
"""
return re.sub(r"[^a-z0-9]", "", name.lower())


class ArchPcode(Arch):
"""
Expand All @@ -36,7 +48,7 @@ def __init__(self, language: Union["pypcode.ArchLanguage", str]):
self.pcode_id = language.id
self.description = language.description
self.bits = int(language.size)
self.endness = {"little": Endness.LE, "big": Endness.BE}[language.endian]
self.endness = _SLEIGH_ENDNESS[language.endian]
self.instruction_endness = self.endness
self.sizeof = (
{"short": 16, "int": 32, "long": 64, "long long": 64}
Expand Down Expand Up @@ -120,7 +132,9 @@ def find_matching_cid(language, desired):
sp_bits = self.bits
if "sp" in archinfo_regs:
sp_bits = archinfo_regs["sp"].size * 8
self.initial_sp = (0x8000 << (sp_bits - 16)) - 1
# start the stack at the top of the lower half of the stack pointer's range. The stack pointer can be
# narrower than the address space; the 8051, for instance, has a one-byte SP on a 16-bit address space.
self.initial_sp = (1 << (sp_bits - 1)) - 1
self.linux_name = "" # FIXME
self.triplet = "" # FIXME

Expand Down Expand Up @@ -151,6 +165,91 @@ def pcode_arch(self) -> "ArchPcode":
"""
return self

@classmethod
def from_id(cls, ident: str, endness: str = Endness.ANY, bits: int | str = "") -> "ArchPcode | None":
"""
Take our best guess at the sleigh language referred to by the given identifier, and return an
ArchPcode for it.

ArchPcode cannot be registered with :func:`archinfo.register_arch`, because that builds an
architecture from an endness while a p-code architecture is built from a language, so
:func:`archinfo.arch_from_id` calls this instead to reach the languages pypcode ships.

:param ident: The identifier to resolve, e.g. ``pa-risc:BE:32:default`` or ``EM_PARISC``.
:param endness: The endness to require. Anything that is not a concrete byte order, such as
Endness.ANY or Endness.UNSURE, accepts either.
:param bits: The bit width to require, if it is known.
:return: An ArchPcode for the best matching language, or None if none matches.
"""
if not _has_pypcode:
return None

try:
required_bits = int(bits) if bits else None
except ValueError:
# arch_from_id hands us whatever it could not read a width out of, and no language
# matches a width that is not a number.
return None
required_endness = endness if endness in (Endness.LE, Endness.BE, Endness.ME) else None

languages = [language for arch in pypcode.Arch.enumerate() for language in arch.languages]
normalized_ident = _normalize_arch_name(ident)

# A full language id names one language and nothing else, so it overrides the hints.
for language in languages:
if _normalize_arch_name(language.id) == normalized_ident:
return cls(language)

# Otherwise the identifier is whatever form the loader read out of a header, such as
# pyelftools' EM_PARISC or pefile's IMAGE_FILE_MACHINE_LOONGARCH64, so compare the whole
# identifier and each of its words against the processor names sleigh knows.
names = {normalized_ident}
for word in re.split(r"[^0-9a-zA-Z]+", ident):
word = _normalize_arch_name(word)
names.add(word)
# Architecture names often carry their width, e.g. loongarch64 for Loongarch.
names.add(word.rstrip("0123456789"))
names.discard("")

candidates = []
for language in languages:
if not names & cls._processor_names(language):
continue
if required_bits is not None and int(language.size) != required_bits:
continue
language_endness = _SLEIGH_ENDNESS.get(language.endian)
if language_endness is None:
continue
if required_endness is not None and language_endness != required_endness:
continue
candidates.append(language)

if not candidates:
return None

# Prefer the default variant, then the narrowest, then the default endness, and fall back on
# the id, so that an underspecified identifier always resolves to the same language.
def rank(language: "pypcode.ArchLanguage"):
return (
language.variant != "default",
int(language.size),
_SLEIGH_ENDNESS[language.endian] != cls.default_endness,
language.id,
)

return cls(min(candidates, key=rank))

@staticmethod
def _processor_names(language: "pypcode.ArchLanguage") -> set[str]:
"""
Return the normalized names of the processor a sleigh language describes. The processor field
and the language id do not always agree, e.g. ``z8401x:LE:16:default`` is ``Z80``.
"""
return {
_normalize_arch_name(language.id.split(":")[0]),
_normalize_arch_name(language.processor),
}

@staticmethod
def _get_language_by_id(lang_id) -> "pypcode.ArchLanguage":
if not _has_pypcode:
Expand Down
72 changes: 71 additions & 1 deletion tests/test_pcode.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# pylint:disable=missing-class-docstring,no-self-use
import pickle
import unittest
from unittest.mock import patch

from archinfo import ArchError, ArchPcode, ArchS390X, Endness, arch_from_id
from archinfo import ArchError, ArchNotFound, ArchPcode, ArchS390X, Endness, arch_from_id

try:
import pypcode
Expand All @@ -17,10 +18,79 @@ def test_arch_68000(self):
assert arch.instruction_endness == Endness.BE
assert arch.bits == 32

def test_arch_with_narrow_stack_pointer(self):
"""The 8051's stack pointer is one byte wide, narrower than its 16-bit address space."""
arch = ArchPcode("8051:BE:16:default")
assert arch.registers["sp"][1] == 1
assert arch.initial_sp == 0x7F

# architectures with a stack pointer at least 16 bits wide keep the initial stack pointer they always had
assert ArchPcode("z80:LE:16:default").initial_sp == 0x7FFF
assert ArchPcode("68000:BE:32:default").initial_sp == 0x7FFFFFFF
assert ArchPcode("x86:LE:64:default").initial_sp == 0x7FFFFFFFFFFFFFFF

def test_arch_bad_langid(self):
with self.assertRaises(ArchError):
ArchPcode("invalid")

def test_arch_from_id_by_processor_name(self):
# pypcode is the only definition of PA-RISC that archinfo has, and the ELF loader asks for it
# by pyelftools' machine name, with the endness and class read out of the header.
arch = arch_from_id("EM_PARISC", "be", 32)
assert isinstance(arch, ArchPcode)
assert arch.pcode_id == "pa-risc:BE:32:default"
assert arch.bits == 32
assert arch.memory_endness == Endness.BE

# where a processor has a language per byte order, the header's byte order picks one
assert arch_from_id("EM_XTENSA", "be", 32).pcode_id == "Xtensa:BE:32:default"
assert arch_from_id("EM_XTENSA", "le", 32).pcode_id == "Xtensa:LE:32:default"

# the PE loader asks by pefile's machine name, which carries its width instead
assert arch_from_id("IMAGE_FILE_MACHINE_LOONGARCH64").pcode_id == "Loongarch:LE:64:lp64d"

def test_arch_from_id_by_language_id(self):
# A full language id names one language, whatever the hints say.
arch = arch_from_id("Xtensa:BE:32:default")
assert isinstance(arch, ArchPcode)
assert arch.pcode_id == "Xtensa:BE:32:default"

def test_arch_from_id_underspecified(self):
assert arch_from_id("sparc").pcode_id == "sparc:BE:32:default"
assert arch_from_id("sparc", bits=64).pcode_id == "sparc:BE:64:default"
assert arch_from_id("xtensa").pcode_id == "Xtensa:LE:32:default"

def test_arch_from_id_with_a_narrow_stack_pointer(self):
# Resolving reaches every language, including the ones whose stack pointer is narrower than
# their address space.
assert arch_from_id("8051").pcode_id == "8051:BE:16:default"

def test_arch_from_id_prefers_registered_arches(self):
for ident in ["x86", "amd64", "arm", "aarch64", "mips32", "mips64", "ppc32", "ppc64", "s390x", "riscv64"]:
assert not isinstance(arch_from_id(ident), ArchPcode)

def test_arch_from_id_without_a_language(self):
# pypcode has no little-endian SPARC, and nothing at all for these architectures.
with self.assertRaises(ArchNotFound):
arch_from_id("EM_SPARC", "le", 32)
with self.assertRaises(ArchNotFound):
arch_from_id("EM_IA_64", "le", 64)
with self.assertRaises(ArchNotFound):
arch_from_id("DEC Alpha", "le", 64)
with self.assertRaises(ArchNotFound):
arch_from_id("EM_VAX", "le", 32)
# a width that is not a number is still an architecture we cannot find, not a crash
with self.assertRaises(ArchNotFound):
arch_from_id("EM_PARISC", "be", "wide")

def test_arch_from_id_without_pypcode(self):
# archinfo installed without the pcode extra has no language to reach, and answers exactly
# as it did before p-code architectures were reachable at all.
with patch("archinfo.arch_pcode._has_pypcode", False):
assert arch_from_id("x86").name == "X86"
with self.assertRaises(ArchNotFound):
arch_from_id("EM_PARISC", "be", 32)

def test_pickle(self):
arch = ArchPcode("68000:BE:32:default")
pickle.dumps(arch)
Expand Down
Loading