Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support additional API on Python 3 bindings #2016

Merged
merged 12 commits into from
Oct 6, 2024
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)
Copy link
Member

@wtdcode wtdcode Oct 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make cpr_read/write raise an UC_ERR_ARG? Terminating the whole process without a chance to catch seems too aggressive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking, exceptions are thrown when the arguments normally make sense but there is a specific problem this time (e.g. opening a file which happens to be inaccessible, etc.). In these cases the user may catch the exception and choose how to handle it.

Here the assertions come to block an invalid usage with incorrect arguments: it is not a call that may sometimes work and sometimes not -- rather it is invalid. There is no way to "handle" such case other than re-writing that piece of code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I also forget AssertionError can be caught so let it go.

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:
Copy link
Member

@wtdcode wtdcode Oct 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems the same as above, which should raise an exception, but maybe next time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not following on this one. What should raise an exception? It marks this line:

return self.reg_read(const.UC_X86_REG_MSR, msr_id)

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
Loading