275 lines
8.4 KiB
Python
275 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import functools
|
|
import sys
|
|
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING, Any, Type, Optional
|
|
|
|
from qiling.const import QL_ENDIAN
|
|
|
|
if TYPE_CHECKING:
|
|
from qiling.os.memory import QlMemoryManager
|
|
|
|
|
|
# the cache decorator is needed here not only for performance purposes, but also to make sure
|
|
# the *same* class type is returned every time rather than creating another one with the same
|
|
# name and properties.
|
|
#
|
|
# TODO: work around the missing functools.cache decorator on Python versions earlier than 3.9
|
|
cache = functools.cache if sys.version_info >= (3, 9) else functools.lru_cache(maxsize=2)
|
|
|
|
|
|
class BaseStruct(ctypes.Structure):
|
|
"""An abstract class for C structures.
|
|
|
|
Refrain from subclassing it directly as it does not take the emulated architecture
|
|
properties into account. Subclass `BaseStructEL` or `BaseStructEB` instead.
|
|
"""
|
|
|
|
def save_to(self, mem: QlMemoryManager, address: int) -> None:
|
|
"""Store structure contents to a specified memory address.
|
|
|
|
Args:
|
|
mem: memory manager instance
|
|
address: destination address
|
|
"""
|
|
|
|
data = bytes(self)
|
|
|
|
mem.write(address, data)
|
|
|
|
@classmethod
|
|
def load_from(cls, mem: QlMemoryManager, address: int):
|
|
"""Construct and populate a structure from saved contents.
|
|
|
|
Args:
|
|
mem: memory manager instance
|
|
address: source address
|
|
|
|
Returns: populated structure instance
|
|
"""
|
|
|
|
data = mem.read(address, cls.sizeof())
|
|
|
|
return cls.from_buffer(data)
|
|
|
|
@classmethod
|
|
def volatile_ref(cls, mem: QlMemoryManager, address: int):
|
|
"""Refer to a memory location as a volatile structure variable.
|
|
|
|
Args:
|
|
mem : memory manager instance
|
|
address : bind address
|
|
|
|
Example:
|
|
>>> class Point(BaseStruct):
|
|
... _fields_ = [
|
|
... ('x', ctypes.c_uint32),
|
|
... ('y', ctypes.c_uint32)
|
|
... ]
|
|
|
|
>>> # bind a volatile Point structure to address `ptr`
|
|
>>> p = Point.volatile_ref(ql.mem, ptr)
|
|
... if p.x > 10: # x value is read directly from memory
|
|
... p.x = 10 # x value is written directly to memory
|
|
... # y value in memory remains unchanged
|
|
>>>
|
|
"""
|
|
|
|
# map all structure field names to their types
|
|
_fields = dict((fname, ftype) for fname, ftype, *_ in cls._fields_)
|
|
|
|
class VolatileStructRef(cls):
|
|
"""Turn a BaseStruct subclass into a volatile structure.
|
|
|
|
Field values are never cached: when retrieving a field's value, its value
|
|
is read from memory and when setting a field's value, its value is flushed
|
|
to memory.
|
|
|
|
This is useful to make sure a structure's fields are alway synced with memory.
|
|
"""
|
|
|
|
def __getattribute__(self, name: str) -> Any:
|
|
# accessing a structure field?
|
|
if name in _fields:
|
|
field = cls.__dict__[name]
|
|
ftype = _fields[name]
|
|
|
|
if issubclass(ftype, BaseStruct):
|
|
fvalue = ftype.volatile_ref(mem, address + field.offset)
|
|
|
|
else:
|
|
# load field's bytes from memory and tranform them into a value
|
|
data = mem.read(address + field.offset, field.size)
|
|
fvalue = ftype.from_buffer(data)
|
|
|
|
if hasattr(fvalue, 'value'):
|
|
fvalue = fvalue.value
|
|
|
|
# set the value to the structure in order to maintain consistency with ctypes.Structure
|
|
super().__setattr__(name, fvalue)
|
|
return fvalue
|
|
|
|
# return attribute value
|
|
return super().__getattribute__(name)
|
|
|
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
# accessing a structure field?
|
|
if name in _fields:
|
|
field = cls.__dict__[name]
|
|
ftype = _fields[name]
|
|
|
|
# transform value into field bytes and write them to memory
|
|
fvalue = ftype(*value) if hasattr(ftype, '_length_') else ftype(value)
|
|
data = bytes(fvalue)
|
|
|
|
mem.write(address + field.offset, data)
|
|
|
|
# proceed to set the value to the structure in order to maintain consistency with ctypes.Structure
|
|
|
|
# set attribute value
|
|
super().__setattr__(name, value)
|
|
|
|
return VolatileStructRef()
|
|
|
|
@classmethod
|
|
@contextmanager
|
|
def ref(cls, mem: QlMemoryManager, address: int):
|
|
"""A structure context manager to facilitate updating structure contents.
|
|
|
|
On context enter, a structure is created and populated from the specified memory
|
|
address. All changes to structure content are written back to memory on context
|
|
exit. If the structure content has not changed, no memory writes occur.
|
|
|
|
Args:
|
|
mem : memory manager instance
|
|
address : bind address
|
|
|
|
Example:
|
|
>>> class Point(BaseStruct):
|
|
... _fields_ = [
|
|
... ('x', ctypes.c_uint32),
|
|
... ('y', ctypes.c_uint32)
|
|
... ]
|
|
|
|
>>> # bind a Point structure to address `ptr`
|
|
>>> with Point.ref(ql.mem, ptr) as p:
|
|
... p.x = 10
|
|
... p.y = 20
|
|
>>> # p data has changed and will be written back to `ptr`
|
|
|
|
>>> # bind a Point structure to address `ptr`
|
|
>>> with Point.ref(ql.mem, ptr) as p:
|
|
... print(f'saved coordinates: {p.x}, {p.y}')
|
|
>>> # p data has not changed and nothing will be written back
|
|
"""
|
|
|
|
instance = cls.load_from(mem, address)
|
|
orig_data = hash(bytes(instance))
|
|
|
|
try:
|
|
yield instance
|
|
finally:
|
|
curr_data = hash(bytes(instance))
|
|
|
|
if curr_data != orig_data:
|
|
instance.save_to(mem, address)
|
|
|
|
@classmethod
|
|
def sizeof(cls) -> int:
|
|
"""Get structure size in bytes.
|
|
"""
|
|
|
|
return ctypes.sizeof(cls)
|
|
|
|
@classmethod
|
|
def offsetof(cls, fname: str) -> int:
|
|
"""Get field offset within the structure.
|
|
|
|
Args:
|
|
fname: field name
|
|
|
|
Returns: field offset in bytes
|
|
Raises: `AttributeError` if the specified field does not exist
|
|
"""
|
|
|
|
return getattr(cls, fname).offset
|
|
|
|
@classmethod
|
|
def memberat(cls, offset: int) -> Optional[str]:
|
|
"""Get the member name at a given offset.
|
|
|
|
Args:
|
|
offset: field offset within the structure
|
|
|
|
Returns: field name, or None if no field starts at the specified offset
|
|
"""
|
|
|
|
return next((fname for fname, *_ in cls._fields_ if cls.offsetof(fname) == offset), None)
|
|
|
|
|
|
class BaseStructEL(BaseStruct, ctypes.LittleEndianStructure):
|
|
"""Little Endian structure base class.
|
|
"""
|
|
pass
|
|
|
|
|
|
class BaseStructEB(BaseStruct, ctypes.BigEndianStructure):
|
|
"""Big Endian structure base class.
|
|
"""
|
|
pass
|
|
|
|
|
|
@cache
|
|
def get_aligned_struct(archbits: int, endian: QL_ENDIAN = QL_ENDIAN.EL) -> Type[BaseStruct]:
|
|
"""Provide an aligned version of BaseStruct based on the emulated
|
|
architecture properties.
|
|
|
|
Args:
|
|
archbits: required alignment in bits
|
|
"""
|
|
|
|
Struct = {
|
|
QL_ENDIAN.EL: BaseStructEL,
|
|
QL_ENDIAN.EB: BaseStructEB
|
|
}[endian]
|
|
|
|
class AlignedStruct(Struct):
|
|
_pack_ = archbits // 8
|
|
|
|
return AlignedStruct
|
|
|
|
|
|
@cache
|
|
def get_aligned_union(archbits: int):
|
|
"""Provide an aligned union class based on the emulated architecture
|
|
properties. This class does not inherit the special BaseStruct methods.
|
|
|
|
FIXME: ctypes.Union endianess cannot be set arbitrarily, rather it depends
|
|
on the hosting system. ctypes.LittleEndianUnion and ctypes.BigEndianUnion
|
|
are available only starting from Python 3.11
|
|
|
|
Args:
|
|
archbits: required alignment in bits
|
|
"""
|
|
|
|
class AlignedUnion(ctypes.Union):
|
|
_pack_ = archbits // 8
|
|
|
|
return AlignedUnion
|
|
|
|
|
|
def get_native_type(archbits: int) -> Type[ctypes._SimpleCData]:
|
|
"""Select a ctypes integer type whose size matches the emulated
|
|
architecture native size.
|
|
"""
|
|
|
|
__type = {
|
|
32: ctypes.c_uint32,
|
|
64: ctypes.c_uint64
|
|
}
|
|
|
|
return __type[archbits]
|