Skip to content

Commit 77395f1

Browse files
committed
test: add verify_flash_algos.py
Not yet ready for primetime since it reports too many false positives.
1 parent f82abb2 commit 77395f1

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed

test/verify_flash_algos.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (c) 2021 Chris Reed
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
import argparse
19+
from importlib import import_module
20+
from pathlib import Path
21+
from typing import (Any, Dict, Iterator, List)
22+
23+
import pyocd
24+
from pyocd.core.memory_map import MemoryRange
25+
26+
class FlashAlgoVerifyFailure(Exception):
27+
pass
28+
29+
FlashAlgoDict = Dict[str, Any]
30+
31+
def range_str(range: MemoryRange) -> str:
32+
return f"[{range.start:#010x}..{range.end:#010x}]"
33+
34+
class FlashAlgoVerifier:
35+
36+
REQUIRED_ENTRY_POINTS = (
37+
'pc_init',
38+
'pc_program_page',
39+
'pc_erase_sector',
40+
)
41+
42+
MINIMUM_STACK_SIZE = 256
43+
44+
def __init__(self, module_name: str, name: str, algo: FlashAlgoDict) -> None:
45+
# print(f"Examining: {module_name}.{name}")
46+
self.module_name = module_name
47+
self.name = name
48+
self.algo = algo
49+
50+
# Get layout values.
51+
try:
52+
self.load_addr = algo['load_address']
53+
self.instr_size = len(algo['instructions']) * 4
54+
self.instr_top = self.load_addr + self.instr_size
55+
self.instr_range = MemoryRange(start=self.load_addr, length=self.instr_size)
56+
self.static_base = algo['static_base']
57+
self.stack_top = algo['begin_stack']
58+
self.page_buffers = sorted(algo.get('page_buffers', [algo['begin_data']]))
59+
except KeyError as err:
60+
raise FlashAlgoVerifyFailure(f"flash algo dict missing required key: {err}") from None
61+
62+
# Compute page size
63+
try:
64+
self.page_size = algo['page_size']
65+
except KeyError as err:
66+
if len(self.page_buffers) > 1:
67+
self.page_size = self.page_buffers[1] - self.page_buffers[0]
68+
else:
69+
print(f"Warning: page_size key is not available and unable to compute page size for {self.module_name}.{self.name}")
70+
self.page_size = 128
71+
72+
# Collect entry points.
73+
self.entry_points = {
74+
k: v
75+
for k, v in algo.items()
76+
if k.startswith('pc_')
77+
}
78+
if not all((n in self.entry_points) for n in self.REQUIRED_ENTRY_POINTS):
79+
raise FlashAlgoVerifyFailure("flash algo dict missing required entry point")
80+
81+
def verify(self) -> None:
82+
# Entry points must be within instructions.
83+
for name, addr in self.entry_points.items():
84+
is_disabled = (addr in (0, None))
85+
86+
# Make sure required entry points are not disabled.
87+
if (name in self.REQUIRED_ENTRY_POINTS) and is_disabled:
88+
raise FlashAlgoVerifyFailure("required entry point '{name}' is disabled (value {addr})")
89+
90+
# Verify entry points are either disabled or reside within the loaded address range.
91+
if not (is_disabled or self.instr_range.contains_address(addr)):
92+
raise FlashAlgoVerifyFailure(f"entry point '{name}' not within instructions {range_str(self.instr_range)}")
93+
94+
# Static base should be within the instructions, since the instructions are supposed to contain
95+
# both rw and zi ready for loading.
96+
if not self.instr_range.contains_address(self.static_base):
97+
raise FlashAlgoVerifyFailure(f"static base {self.static_base:#010x} not within instructions {range_str(self.instr_range)}")
98+
99+
# Verify stack basics.
100+
if self.instr_range.contains_address(self.stack_top):
101+
raise FlashAlgoVerifyFailure(f"stack top {self.stack_top:#010x} is within instructions {range_str(self.instr_range)}")
102+
103+
buffers_top = self.page_buffers[-1] + self.page_size
104+
105+
# Compute max stack size.
106+
if self.stack_top > self.instr_top and self.stack_top <= self.page_buffers[0]:
107+
stack_size = self.stack_top - self.instr_top
108+
elif self.stack_top > buffers_top:
109+
stack_size = self.stack_top - buffers_top
110+
else:
111+
stack_size = 0
112+
print(f"Warning: unable to compute stack size for {self.module_name}.{self.name}")
113+
114+
stack_range = MemoryRange(start=(self.stack_top - stack_size), length=stack_size)
115+
116+
# Minimum stack size.
117+
if (stack_size != 0) and (stack_size < self.MINIMUM_STACK_SIZE):
118+
raise FlashAlgoVerifyFailure(f"stack size {stack_size} is below minimum {self.MINIMUM_STACK_SIZE}")
119+
120+
# Page buffers.
121+
for base_addr in self.page_buffers:
122+
buffer_range = MemoryRange(start=base_addr, length=self.page_size)
123+
124+
if buffer_range.intersects_range(self.instr_range):
125+
raise FlashAlgoVerifyFailure(f"buffer {base_addr:#010x} overlaps instructsion {range_str(self.instr_range)}")
126+
if buffer_range.intersects_range(stack_range):
127+
raise FlashAlgoVerifyFailure(f"buffer {range_str(buffer_range)} overlaps stack {range_str(stack_range)}")
128+
129+
130+
def collect_modules(dotted_path: str, dir_path: Path) -> Iterator[str]:
131+
"""@brief Yield dotted names of all modules contained within the given package."""
132+
for entry in sorted(dir_path.iterdir(), key=lambda v: v.name):
133+
# Primitive tests for modules and packages.
134+
is_subpackage = (entry.is_dir() and (entry / "__init__.py").exists())
135+
is_module = entry.suffix == ".py"
136+
module_name = dotted_path + '.' + entry.stem
137+
138+
# Yield this module's name.
139+
if is_module:
140+
yield module_name
141+
# Recursively yield modules from valid sub-packages.
142+
elif is_subpackage:
143+
for name in collect_modules(module_name, entry):
144+
yield name
145+
146+
147+
def is_algo_dict(n: str, o: Any) -> bool:
148+
"""@brief Test whether a dict contains a flash algo."""
149+
return (isinstance(o, Dict)
150+
and (n != '__builtins__')
151+
and 'instructions' in o
152+
and 'pc_program_page' in o)
153+
154+
155+
def main() -> None:
156+
parser = argparse.ArgumentParser(description="Flash algo verifier")
157+
parser.add_argument("module", nargs='*', help="pyOCD module name containing flash algos to verify")
158+
args = parser.parse_args()
159+
160+
pyocd_path = Path(pyocd.__file__).parent.resolve()
161+
# print(f"pyocd package path: {pyocd_path}")
162+
163+
if not args.module:
164+
target_module_names = collect_modules('pyocd.target.builtin', pyocd_path / 'target' / 'builtin')
165+
else:
166+
target_module_names = args.module
167+
168+
for module_name in target_module_names:
169+
# print(f"Importing: {module_name}")
170+
module = import_module(module_name)
171+
172+
# Scan for algo dictionaries in the module. This assumes they are defined at the module level,
173+
# which is the case for all current targets.
174+
algos_iter = ((n, v) for n, v in module.__dict__.items() if is_algo_dict(n, v))
175+
176+
for name, algo in algos_iter:
177+
try:
178+
FlashAlgoVerifier(module_name, name, algo).verify()
179+
except FlashAlgoVerifyFailure as err:
180+
print(f"Error: {module_name}.{name}: {err}")
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)