Skip to content

Commit 46866ad

Browse files
committed
Redesign MMIO support to be pickle-friendly
1 parent 2f604f4 commit 46866ad

File tree

3 files changed

+144
-76
lines changed

3 files changed

+144
-76
lines changed

qiling/hw/hw.py

Lines changed: 107 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,68 @@
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6-
import ctypes
6+
from functools import cached_property
7+
from typing import Any, Dict, List, Optional, Tuple
78

8-
from qiling.core import Qiling
9+
from qiling import Qiling
910
from qiling.hw.peripheral import QlPeripheral
1011
from qiling.utils import ql_get_module_function
1112
from qiling.exception import QlErrorModuleFunctionNotFound
1213

1314

15+
# should adhere to the QlMmioHandler interface, but not extend it directly to
16+
# avoid potential pickling issues
17+
class QlPripheralHandler:
18+
def __init__(self, hwman: "QlHwManager", base: int, size: int, label: str) -> None:
19+
self._hwman = hwman
20+
self._base = base
21+
self._size = size
22+
self._label = label
23+
24+
def __getstate__(self):
25+
state = self.__dict__.copy()
26+
del state['_hwman'] # remove non-pickleable reference
27+
28+
return state
29+
30+
@cached_property
31+
def _mmio(self) -> bytearray:
32+
"""Get memory buffer used to back non-mapped hardware mmio regions.
33+
"""
34+
35+
return bytearray(self._size)
36+
37+
def read(self, ql: Qiling, offset: int, size: int) -> int:
38+
address = self._base + offset
39+
hardware = self._hwman.find(address)
40+
41+
if hardware:
42+
return hardware.read(address - hardware.base, size)
43+
44+
else:
45+
ql.log.debug('[%s] read non-mapped hardware [%#010x]', self._label, address)
46+
return int.from_bytes(self._mmio[offset:offset + size], byteorder='little')
47+
48+
def write(self, ql: Qiling, offset: int, size: int, value: int) -> None:
49+
address = self._base + offset
50+
hardware = self._hwman.find(address)
51+
52+
if hardware:
53+
hardware.write(address - hardware.base, size, value)
54+
55+
else:
56+
ql.log.debug('[%s] write non-mapped hardware [%#010x] = %#010x', self._label, address, value)
57+
self._mmio[offset:offset + size] = value.to_bytes(size, 'little')
58+
59+
1460
class QlHwManager:
1561
def __init__(self, ql: Qiling):
1662
self.ql = ql
1763

18-
self.entity = {}
19-
self.region = {}
20-
21-
self.stepable = {}
64+
self.entity: Dict[str, QlPeripheral] = {}
65+
self.region: Dict[str, List[Tuple[int, int]]] = {}
2266

23-
def create(self, label: str, struct: str=None, base: int=None, kwargs: dict={}) -> "QlPeripheral":
67+
def create(self, label: str, struct: Optional[str] = None, base: Optional[int] = None, kwargs: Optional[Dict[str, Any]] = None) -> QlPeripheral:
2468
""" Create the peripheral accroding the label and envs.
2569
2670
struct: Structure of the peripheral. Use defualt ql structure if not provide.
@@ -30,88 +74,76 @@ def create(self, label: str, struct: str=None, base: int=None, kwargs: dict={})
3074
if struct is None:
3175
struct, base, kwargs = self.load_env(label.upper())
3276

77+
if kwargs is None:
78+
kwargs = {}
79+
3380
try:
34-
3581
entity = ql_get_module_function('qiling.hw', struct)(self.ql, label, **kwargs)
36-
37-
self.entity[label] = entity
38-
if hasattr(entity, 'step'):
39-
self.stepable[label] = entity
4082

41-
self.region[label] = [(lbound + base, rbound + base) for (lbound, rbound) in entity.region]
83+
except QlErrorModuleFunctionNotFound:
84+
self.ql.log.warning(f'could not create {struct}({label}): implementation not found')
4285

86+
else:
87+
assert isinstance(entity, QlPeripheral)
88+
assert isinstance(base, int)
89+
90+
self.entity[label] = entity
91+
self.region[label] = [(lbound + base, rbound + base) for (lbound, rbound) in entity.region]
4392

4493
return entity
45-
except QlErrorModuleFunctionNotFound:
46-
self.ql.log.debug(f'The {struct}({label}) has not been implemented')
4794

48-
def delete(self, label: str):
95+
# FIXME: what should we do if struct is not implemented? is it OK to return None , or we fail?
96+
97+
def delete(self, label: str) -> None:
4998
""" Remove the peripheral
5099
"""
100+
51101
if label in self.entity:
52-
self.entity.pop(label)
53-
self.region.pop(label)
54-
if label in self.stepable:
55-
self.stepable.pop(label)
102+
del self.entity[label]
103+
104+
if label in self.region:
105+
del self.region[label]
56106

57-
def load_env(self, label: str):
107+
def load_env(self, label: str) -> Tuple[str, int, Dict[str, Any]]:
58108
""" Get peripheral information (structure, base address, initialization list) from env.
59109
60110
Args:
61111
label (str): Peripheral Label
62-
112+
63113
"""
64114
args = self.ql.env[label]
65-
115+
66116
return args['struct'], args['base'], args.get("kwargs", {})
67117

68118
def load_all(self):
69119
for label, args in self.ql.env.items():
70120
if args['type'] == 'peripheral':
71121
self.create(label.lower(), args['struct'], args['base'], args.get("kwargs", {}))
72122

73-
def find(self, address: int):
123+
# TODO: this is wasteful. device mapping is known at creation time. at least we could cache lru entries
124+
def find(self, address: int) -> Optional[QlPeripheral]:
74125
""" Find the peripheral at `address`
75126
"""
76-
127+
77128
for label in self.entity.keys():
78129
for lbound, rbound in self.region[label]:
79130
if lbound <= address < rbound:
80131
return self.entity[label]
81132

133+
return None
134+
82135
def step(self):
83-
""" Update all peripheral's state
136+
""" Update all peripheral's state
84137
"""
85-
for entity in self.stepable.values():
86-
entity.step()
87-
88-
def setup_mmio(self, begin, size, info=""):
89-
mmio = ctypes.create_string_buffer(size)
90-
91-
def mmio_read_cb(ql, offset, size):
92-
address = begin + offset
93-
hardware = self.find(address)
94-
95-
if hardware:
96-
return hardware.read(address - hardware.base, size)
97-
else:
98-
ql.log.debug('%s Read non-mapped hardware [0x%08x]' % (info, address))
99-
100-
buf = ctypes.create_string_buffer(size)
101-
ctypes.memmove(buf, ctypes.addressof(mmio) + offset, size)
102-
return int.from_bytes(buf.raw, byteorder='little')
103-
104-
def mmio_write_cb(ql, offset, size, value):
105-
address = begin + offset
106-
hardware = self.find(address)
107-
108-
if hardware:
109-
hardware.write(address - hardware.base, size, value)
110-
else:
111-
ql.log.debug('%s Write non-mapped hardware [0x%08x] = 0x%08x' % (info, address, value))
112-
ctypes.memmove(ctypes.addressof(mmio) + offset, (value).to_bytes(size, 'little'), size)
113-
114-
self.ql.mem.map_mmio(begin, size, mmio_read_cb, mmio_write_cb, info=info)
138+
139+
for ent in self.entity.values():
140+
if hasattr(ent, 'step'):
141+
ent.step()
142+
143+
def setup_mmio(self, begin: int, size: int, info: str) -> None:
144+
dev = QlPripheralHandler(self, begin, size, info)
145+
146+
self.ql.mem.map_mmio(begin, size, dev, info)
115147

116148
def show_info(self):
117149
self.ql.log.info(f'{"Start":8s} {"End":8s} {"Label":8s} {"Class"}')
@@ -131,8 +163,25 @@ def __getattr__(self, key):
131163
return self.entity.get(key)
132164

133165
def save(self):
134-
return {label : entity.save() for label, entity in self.entity.items()}
166+
return {
167+
'entity': {label: entity.save() for label, entity in self.entity.items()},
168+
'region': self.region
169+
}
135170

136171
def restore(self, saved_state):
137-
for label, data in saved_state.items():
172+
entity = saved_state['entity']
173+
assert isinstance(entity, dict)
174+
175+
region = saved_state['region']
176+
assert isinstance(region, dict)
177+
178+
for label, data in entity.items():
138179
self.entity[label].restore(data)
180+
181+
self.region = region
182+
183+
# a dirty hack to rehydrate non-pickleable hwman
184+
# a proper fix would require a deeper refactoring to how peripherals are created and managed
185+
for ph in self.ql.mem.mmio_cbs.values():
186+
if isinstance(ph, QlPripheralHandler):
187+
setattr(ph, '_hwman', self)

qiling/loader/mcu.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,20 +114,23 @@ def load_env(self):
114114
base = args['base']
115115
self.ql.mem.map(base, size, info=f'[{name}]')
116116

117-
if memtype == 'remap':
118-
size = args['size']
119-
base = args['base']
120-
alias = args['alias']
121-
self.ql.hw.setup_remap(alias, base, size, info=f'[{name}]')
117+
# elif memtype == 'remap':
118+
# size = args['size']
119+
# base = args['base']
120+
# alias = args['alias']
121+
# self.ql.hw.setup_remap(alias, base, size, info=f'[{name}]')
122122

123-
if memtype == 'mmio':
123+
elif memtype == 'mmio':
124124
size = args['size']
125125
base = args['base']
126-
self.ql.hw.setup_mmio(base, size, info=f'[{name}]')
126+
self.ql.hw.setup_mmio(base, size, name)
127127

128-
if memtype == 'core':
128+
elif memtype == 'core':
129129
self.ql.hw.create(name.lower())
130130

131+
else:
132+
self.ql.log.error(f'Unknown memory type "{memtype}" for {name}')
133+
131134
def run(self):
132135
self.load_profile()
133136
self.load_env()

qiling/os/memory.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import bisect
77
import os
88
import re
9-
from typing import Any, Callable, Iterator, List, Mapping, Optional, Pattern, Sequence, Tuple, Union
9+
from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Pattern, Protocol, Sequence, Tuple, Union
1010

1111
from unicorn import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_PROT_ALL
1212

@@ -20,6 +20,22 @@
2020
MmioWriteCallback = Callable[[Qiling, int, int, int], None]
2121

2222

23+
class QlMmioHandler(Protocol):
24+
"""A simple MMIO handler boilerplate that can be used to implement memory mapped devices.
25+
26+
This should be extended to implement mapped devices state machines. Note that the read and write
27+
methods are optional, where their existance indicates whether the device supports the corresponding
28+
operation. That is, an unimplemented method means the corresponding operation will be silently
29+
dropped.
30+
"""
31+
32+
def read(self, ql: Qiling, offset: int, size: int) -> int:
33+
...
34+
35+
def write(self, ql: Qiling, offset: int, size: int, value: int) -> None:
36+
...
37+
38+
2339
class QlMemoryManager:
2440
"""
2541
some ideas and code from:
@@ -29,7 +45,7 @@ class QlMemoryManager:
2945
def __init__(self, ql: Qiling, pagesize: int = 0x1000):
3046
self.ql = ql
3147
self.map_info: List[MapInfoEntry] = []
32-
self.mmio_cbs = {}
48+
self.mmio_cbs: Dict[Tuple[int, int], QlMmioHandler] = {}
3349

3450
bit_stuff = {
3551
64: (1 << 64) - 1,
@@ -272,7 +288,7 @@ def save(self):
272288

273289
for lbound, ubound, perm, label, is_mmio in self.map_info:
274290
if is_mmio:
275-
mem_dict['mmio'].append((lbound, ubound, perm, label, *self.mmio_cbs[(lbound, ubound)]))
291+
mem_dict['mmio'].append((lbound, ubound, perm, label, self.mmio_cbs[(lbound, ubound)]))
276292
else:
277293
data = self.read(lbound, ubound - lbound)
278294
mem_dict['ram'].append((lbound, ubound, perm, label, bytes(data)))
@@ -294,12 +310,12 @@ def restore(self, mem_dict):
294310
self.ql.log.debug(f'writing {len(data):#x} bytes at {lbound:#08x}')
295311
self.write(lbound, data)
296312

297-
for lbound, ubound, perms, label, read_cb, write_cb in mem_dict['mmio']:
313+
for lbound, ubound, perms, label, handler in mem_dict['mmio']:
298314
self.ql.log.debug(f"restoring mmio range: {lbound:#08x} {ubound:#08x} {label}")
299315

300316
size = ubound - lbound
301317
if not self.is_mapped(lbound, size):
302-
self.map_mmio(lbound, size, read_cb, write_cb, info=label)
318+
self.map_mmio(lbound, size, handler, label)
303319

304320
def read(self, addr: int, size: int) -> bytearray:
305321
"""Read bytes from memory.
@@ -619,15 +635,15 @@ def map(self, addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str
619635
self.ql.uc.mem_map(addr, size, perms)
620636
self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', is_mmio=False)
621637

622-
def map_mmio(self, addr: int, size: int, read_cb: Optional[MmioReadCallback], write_cb: Optional[MmioWriteCallback], info: str = '[mmio]'):
638+
def map_mmio(self, addr: int, size: int, handler: QlMmioHandler, info: str = '[mmio]'):
623639
# TODO: mmio memory overlap with ram? Is that possible?
624640
# TODO: Can read_cb or write_cb be None? How uc handle that access?
625641
prot = UC_PROT_NONE
626642

627-
if read_cb:
643+
if hasattr(handler, 'read'):
628644
prot |= UC_PROT_READ
629645

630-
if write_cb:
646+
if hasattr(handler, 'write'):
631647
prot |= UC_PROT_WRITE
632648

633649
# generic mmio read wrapper
@@ -642,10 +658,10 @@ def __mmio_write(uc, offset: int, size: int, value: int, user_data: MmioWriteCal
642658

643659
cb(self.ql, offset, size, value)
644660

645-
self.ql.uc.mmio_map(addr, size, __mmio_read, read_cb, __mmio_write, write_cb)
661+
self.ql.uc.mmio_map(addr, size, __mmio_read, handler.read, __mmio_write, handler.write)
646662
self.add_mapinfo(addr, addr + size, prot, info, is_mmio=True)
647663

648-
self.mmio_cbs[(addr, addr + size)] = (read_cb, write_cb)
664+
self.mmio_cbs[(addr, addr + size)] = handler
649665

650666

651667
class Chunk:

0 commit comments

Comments
 (0)