Skip to content

Commit

Permalink
Support additional API on Python 3 bindings (#2016)
Browse files Browse the repository at this point in the history
* Styling and commets fixes

* Add errno API support

* Improve OOP approach by adjusting the way reg types are selected

* Leverage new approach to deduplicate reg_read and reg_write code

* Adjust reg_read_batch

* Add support for reg_write_batch

* Adjust x86 MSR accessors

* Turn asserts into descriptive exceptions

* Improve comments and styling

* Fix ARM memcpy neon regression test

* Modify canonicals import

* Introduce ARM CP reg accessors
  • Loading branch information
elicn authored Oct 6, 2024
1 parent 05e29b4 commit ac4872b
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 222 deletions.
81 changes: 53 additions & 28 deletions bindings/python/unicorn/unicorn_py3/arch/arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"""
# @author elicn

from typing import Any, Tuple
from typing import Tuple, Type

import ctypes

# traditional unicorn imports
from unicorn import arm_const as const

# newly introduced unicorn imports
from ..unicorn import Uc
from ..unicorn import Uc, check_maxbits
from .types import UcTupledReg, UcReg128

ARMCPReg = Tuple[int, int, int, int, int, int, int, int]
Expand Down Expand Up @@ -40,46 +40,71 @@ class UcAArch32(Uc):
"""Unicorn subclass for ARM architecture.
"""

REG_RANGE_CP = (const.UC_ARM_REG_CP_REG,)

REG_RANGE_Q = range(const.UC_ARM_REG_Q0, const.UC_ARM_REG_Q15 + 1)

@staticmethod
def __select_reg_class(reg_id: int):
"""Select class for special architectural registers.
@classmethod
def _select_reg_class(cls, reg_id: int) -> Type:
"""Select the appropriate class for the specified architectural register.
"""

reg_class = (
(UcAArch32.REG_RANGE_Q, UcReg128),
(UcAArch32.REG_RANGE_CP, UcRegCP),
(UcAArch32.REG_RANGE_Q, UcReg128)
)

return next((cls for rng, cls in reg_class if reg_id in rng), None)
return next((c for rng, c in reg_class if reg_id in rng), cls._DEFAULT_REGTYPE)

def reg_read(self, reg_id: int, aux: Any = None):
# select register class for special cases
reg_cls = UcAArch32.__select_reg_class(reg_id)
# to learn more about accessing aarch32 coprocessor registers, refer to:
# https://developer.arm.com/documentation/ddi0601/latest/AArch32-Registers

if reg_cls is None:
if reg_id == const.UC_ARM_REG_CP_REG:
return self._reg_read(reg_id, UcRegCP, *aux)
def cpr_read(self, coproc: int, opc1: int, crn: int, crm: int, opc2: int, el: int, is_64: bool) -> int:
"""Read a coprocessor register value.
else:
# fallback to default reading method
return super().reg_read(reg_id, aux)
Args:
coproc : coprocessor to access, value varies between 0 and 15
opc1 : opcode 1, value varies between 0 and 7
crn : coprocessor register to access (CRn), value varies between 0 and 15
crm : additional coprocessor register to access (CRm), value varies between 0 and 15
opc2 : opcode 2, value varies between 0 and 7
el : the exception level the coprocessor register belongs to, value varies between 0 and 3
is_64 : indicates whether this is a 64-bit register
return self._reg_read(reg_id, reg_cls)
Returns: value of coprocessor register
"""

def reg_write(self, reg_id: int, value) -> None:
# select register class for special cases
reg_cls = UcAArch32.__select_reg_class(reg_id)
assert check_maxbits(coproc, 4)
assert check_maxbits(opc1, 3)
assert check_maxbits(crn, 4)
assert check_maxbits(crm, 4)
assert check_maxbits(opc2, 3)
assert check_maxbits(el, 2) # note that unicorn currently supports only EL0 and EL1

return self.reg_read(const.UC_ARM_REG_CP_REG, (coproc, int(is_64), el, crn, crm, opc1, opc2))

def cpr_write(self, coproc: int, opc1: int, crn: int, crm: int, opc2: int, el: int, is_64: bool, value: int) -> None:
"""Write a coprocessor register value.
Args:
coproc : coprocessor to access, value varies between 0 and 15
opc1 : opcode 1, value varies between 0 and 7
crn : coprocessor register to access (CRn), value varies between 0 and 15
crm : additional coprocessor register to access (CRm), value varies between 0 and 15
opc2 : opcode 2, value varies between 0 and 7
el : the exception level the coprocessor register belongs to, value varies between 0 and 3
is_64 : indicates whether this is a 64-bit register
value : value to write
"""

if reg_cls is None:
if reg_id == const.UC_ARM_REG_CP_REG:
self._reg_write(reg_id, UcRegCP, value)
assert check_maxbits(coproc, 4)
assert check_maxbits(opc1, 3)
assert check_maxbits(crn, 4)
assert check_maxbits(crm, 4)
assert check_maxbits(opc2, 3)
assert check_maxbits(el, 2) # note that unicorn currently supports only EL0 and EL1

else:
# fallback to default writing method
super().reg_write(reg_id, value)
self.reg_write(const.UC_ARM_REG_CP_REG, (coproc, int(is_64), el, crn, crm, opc1, opc2, value))

else:
self._reg_write(reg_id, reg_cls, value)

__all__ = ['UcRegCP', 'UcAArch32']
77 changes: 48 additions & 29 deletions bindings/python/unicorn/unicorn_py3/arch/arm64.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""
# @author elicn

from typing import Any, Callable, NamedTuple, Tuple
from typing import Any, Callable, NamedTuple, Tuple, Type

import ctypes

Expand All @@ -11,7 +11,7 @@
from unicorn.unicorn_const import UC_ERR_ARG, UC_HOOK_INSN

# newly introduced unicorn imports
from ..unicorn import Uc, UcError, uccallback
from ..unicorn import Uc, UcError, uccallback, check_maxbits
from .types import uc_engine, UcTupledReg, UcReg128

ARM64CPReg = Tuple[int, int, int, int, int, int]
Expand Down Expand Up @@ -41,6 +41,8 @@ class UcAArch64(Uc):
"""Unicorn subclass for ARM64 architecture.
"""

REG_RANGE_CP = (const.UC_ARM64_REG_CP_REG,)

REG_RANGE_Q = range(const.UC_ARM64_REG_Q0, const.UC_ARM64_REG_Q31 + 1)
REG_RANGE_V = range(const.UC_ARM64_REG_V0, const.UC_ARM64_REG_V31 + 1)

Expand Down Expand Up @@ -85,45 +87,62 @@ class CpReg(NamedTuple):

return getattr(self, '_Uc__do_hook_add')(htype, fptr, begin, end, insn)

@staticmethod
def __select_reg_class(reg_id: int):
"""Select class for special architectural registers.
@classmethod
def _select_reg_class(cls, reg_id: int) -> Type:
"""Select the appropriate class for the specified architectural register.
"""

reg_class = (
(UcAArch64.REG_RANGE_Q, UcReg128),
(UcAArch64.REG_RANGE_V, UcReg128)
(UcAArch64.REG_RANGE_CP, UcRegCP64),
(UcAArch64.REG_RANGE_Q, UcReg128),
(UcAArch64.REG_RANGE_V, UcReg128)
)

return next((cls for rng, cls in reg_class if reg_id in rng), None)
return next((c for rng, c in reg_class if reg_id in rng), cls._DEFAULT_REGTYPE)

# to learn more about accessing aarch64 coprocessor registers, refer to:
# https://developer.arm.com/documentation/ddi0601/latest/AArch64-Registers

def cpr_read(self, op0: int, op1: int, crn: int, crm: int, op2: int) -> int:
"""Read a coprocessor register value.
def reg_read(self, reg_id: int, aux: Any = None):
# select register class for special cases
reg_cls = UcAArch64.__select_reg_class(reg_id)
Args:
op0 : opcode 0, value varies between 0 and 3
op1 : opcode 1, value varies between 0 and 7
crn : coprocessor register to access (CRn), value varies between 0 and 15
crm : additional coprocessor register to access (CRm), value varies between 0 and 15
op2 : opcode 2, value varies between 0 and 7
if reg_cls is None:
if reg_id == const.UC_ARM64_REG_CP_REG:
return self._reg_read(reg_id, UcRegCP64, *aux)
Returns: value of coprocessor register
"""

assert check_maxbits(op0, 2)
assert check_maxbits(op1, 3)
assert check_maxbits(crn, 4)
assert check_maxbits(crm, 4)
assert check_maxbits(op2, 3)

else:
# fallback to default reading method
return super().reg_read(reg_id, aux)
return self.reg_read(const.UC_ARM64_REG_CP_REG, (crn, crm, op0, op1, op2))

return self._reg_read(reg_id, reg_cls)
def cpr_write(self, op0: int, op1: int, crn: int, crm: int, op2: int, value: int) -> None:
"""Write a coprocessor register value.
def reg_write(self, reg_id: int, value) -> None:
# select register class for special cases
reg_cls = UcAArch64.__select_reg_class(reg_id)
Args:
op0 : opcode 0, value varies between 0 and 3
op1 : opcode 1, value varies between 0 and 7
crn : coprocessor register to access (CRn), value varies between 0 and 15
crm : additional coprocessor register to access (CRm), value varies between 0 and 15
op2 : opcode 2, value varies between 0 and 7
value : value to write
"""

if reg_cls is None:
if reg_id == const.UC_ARM64_REG_CP_REG:
self._reg_write(reg_id, UcRegCP64, value)
assert check_maxbits(op0, 2)
assert check_maxbits(op1, 3)
assert check_maxbits(crn, 4)
assert check_maxbits(crm, 4)
assert check_maxbits(op2, 3)

else:
# fallback to default writing method
super().reg_write(reg_id, value)
self.reg_write(const.UC_ARM64_REG_CP_REG, (crn, crm, op0, op1, op2, value))

else:
self._reg_write(reg_id, reg_cls, value)

__all__ = ['UcRegCP64', 'UcAArch64']
65 changes: 21 additions & 44 deletions bindings/python/unicorn/unicorn_py3/arch/intel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""
# @author elicn

from typing import Any, Callable, Sequence, Tuple
from typing import Any, Callable, Tuple, Type

import ctypes

Expand Down Expand Up @@ -64,6 +64,8 @@ class UcIntel(Uc):
"""Unicorn subclass for Intel architecture.
"""

REG_RANGE_MSR = (const.UC_X86_REG_MSR,)

REG_RANGE_MMR = (
const.UC_X86_REG_IDTR,
const.UC_X86_REG_GDTR,
Expand Down Expand Up @@ -127,67 +129,42 @@ def __hook_insn_cpuid_cb(uc: Uc, key: int) -> int:

return getattr(self, '_Uc__do_hook_add')(htype, fptr, begin, end, insn)

@staticmethod
def __select_reg_class(reg_id: int):
"""Select class for special architectural registers.
@classmethod
def _select_reg_class(cls, reg_id: int) -> Type:
"""Select the appropriate class for the specified architectural register.
"""

reg_class = (
(UcIntel.REG_RANGE_MSR, UcRegMSR),
(UcIntel.REG_RANGE_MMR, UcRegMMR),
(UcIntel.REG_RANGE_FP, UcRegFPR),
(UcIntel.REG_RANGE_XMM, UcReg128),
(UcIntel.REG_RANGE_YMM, UcReg256),
(UcIntel.REG_RANGE_ZMM, UcReg512)
)

return next((cls for rng, cls in reg_class if reg_id in rng), None)

def reg_read(self, reg_id: int, aux: Any = None):
# select register class for special cases
reg_cls = UcIntel.__select_reg_class(reg_id)

if reg_cls is None:
# backward compatibility: msr read through reg_read
if reg_id == const.UC_X86_REG_MSR:
if type(aux) is not int:
raise UcError(UC_ERR_ARG)

value = self.msr_read(aux)

else:
value = super().reg_read(reg_id, aux)
else:
value = self._reg_read(reg_id, reg_cls)

return value
return next((c for rng, c in reg_class if reg_id in rng), cls._DEFAULT_REGTYPE)

def reg_write(self, reg_id: int, value) -> None:
# select register class for special cases
reg_cls = UcIntel.__select_reg_class(reg_id)
def msr_read(self, msr_id: int) -> int:
"""Read a model-specific register.
if reg_cls is None:
# backward compatibility: msr write through reg_write
if reg_id == const.UC_X86_REG_MSR:
if type(value) is not tuple or len(value) != 2:
raise UcError(UC_ERR_ARG)
Args:
msr_id: MSR index
self.msr_write(*value)
return
Returns: MSR value
"""

super().reg_write(reg_id, value)
else:
self._reg_write(reg_id, reg_cls, value)

def msr_read(self, msr_id: int) -> int:
return self._reg_read(const.UC_X86_REG_MSR, UcRegMSR, msr_id)
return self.reg_read(const.UC_X86_REG_MSR, msr_id)

def msr_write(self, msr_id: int, value: int) -> None:
self._reg_write(const.UC_X86_REG_MSR, UcRegMSR, (msr_id, value))
"""Write to a model-specific register.
def reg_read_batch(self, reg_ids: Sequence[int]) -> Tuple:
reg_types = [UcIntel.__select_reg_class(rid) or self._DEFAULT_REGTYPE for rid in reg_ids]
Args:
msr_id: MSR index
value: new MSR value
"""

return self._reg_read_batch(reg_ids, reg_types)
self.reg_write(const.UC_X86_REG_MSR, (msr_id, value))


__all__ = ['UcRegMMR', 'UcRegMSR', 'UcRegFPR', 'UcIntel']
15 changes: 10 additions & 5 deletions bindings/python/unicorn/unicorn_py3/arch/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# @author elicn

from abc import abstractmethod
from typing import Generic, Tuple, TypeVar
from typing import Any, Generic, Tuple, TypeVar

import ctypes

Expand All @@ -24,15 +24,15 @@ class UcReg(ctypes.Structure):

@property
@abstractmethod
def value(self):
def value(self) -> Any:
"""Get register value.
"""

pass

@classmethod
@abstractmethod
def from_value(cls, value):
def from_value(cls, value) -> 'UcReg':
"""Create a register instance from a given value.
"""

Expand All @@ -52,7 +52,11 @@ def value(self) -> VT:

@classmethod
def from_value(cls, value: VT):
assert type(value) is tuple and len(value) == len(cls._fields_)
if not isinstance(value, tuple):
raise TypeError(f'got {type(value).__name__} while expecting a tuple')

if len(value) != len(cls._fields_):
raise TypeError(f'got {len(value)} elements while expecting {len(cls._fields_)}')

return cls(*value)

Expand All @@ -72,7 +76,8 @@ def value(self) -> int:

@classmethod
def from_value(cls, value: int):
assert type(value) is int
if not isinstance(value, int):
raise TypeError(f'got {type(value).__name__} while expecting an integer')

mask = (1 << 64) - 1
size = cls._fields_[0][1]._length_
Expand Down
Loading

0 comments on commit ac4872b

Please sign in to comment.