Fit Nordic Full KISS with lossless assets and firmware lifetime trimming
This commit is contained in:
1 parent
2369a3d28c
commit
b4bec19e88
51 files changed
+7739
-17
No files matched your search
@@ -0,0 +1,27 @@
|
||||
/* Host-only instruction measurements for the pinned Unicorn 2.1.4 checker.
|
||||
* Match the Python callback exactly: read SP before every ARM instruction. */
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef int (*mesh_reg_read)(void *, int, void *);
|
||||
struct mesh_arm_measurement {
|
||||
mesh_reg_read read_register;
|
||||
int sp_register, error;
|
||||
uint64_t minimum_sp, instructions;
|
||||
};
|
||||
|
||||
size_t mesh_arm_measurement_size(void) { return sizeof(struct mesh_arm_measurement); }
|
||||
|
||||
void mesh_arm_measure_instruction(void *engine, uint64_t address,
|
||||
uint32_t size, void *user)
|
||||
{
|
||||
struct mesh_arm_measurement *state = (struct mesh_arm_measurement *) user;
|
||||
uint32_t sp = 0;
|
||||
int error;
|
||||
(void) address;
|
||||
(void) size;
|
||||
error = state->read_register(engine, state->sp_register, &sp);
|
||||
if (error) state->error = error;
|
||||
if (sp < state->minimum_sp) state->minimum_sp = sp;
|
||||
++state->instructions;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Measure every ARM instruction with a C callback, or the identical Python one.
|
||||
|
||||
The optional native callback avoids billions of crossings into Python while
|
||||
keeping instruction/stack coverage. It uses the reviewed private handle API of
|
||||
pinned Unicorn 2.1.4. Other versions or hosts without a C compiler use Python.
|
||||
No Unicorn headers, linker development package, or network access are needed.
|
||||
"""
|
||||
import ctypes
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unicorn
|
||||
from unicorn import UC_HOOK_CODE
|
||||
from unicorn import arm_const as arm
|
||||
|
||||
|
||||
class _NativeState(ctypes.Structure):
|
||||
_fields_ = [('read_register', ctypes.c_void_p), ('sp_register', ctypes.c_int),
|
||||
('error', ctypes.c_int), ('minimum_sp', ctypes.c_uint64),
|
||||
('instructions', ctypes.c_uint64)]
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _native_library():
|
||||
compiler = shutil.which('cc') or shutil.which('gcc') or shutil.which('clang')
|
||||
if not compiler:
|
||||
raise RuntimeError('no host C compiler; use the Python instruction callback')
|
||||
directory = tempfile.TemporaryDirectory(prefix='mesh-arm-instruction-meter-')
|
||||
library = Path(directory.name) / 'meter.so'
|
||||
result = subprocess.run([compiler, '-std=c11', '-O2', '-shared', '-fPIC',
|
||||
str(Path(__file__).with_suffix('.c')), '-o', str(library)],
|
||||
capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError('native callback compilation failed: ' + result.stderr)
|
||||
loaded = ctypes.CDLL(str(library))
|
||||
loaded.mesh_arm_measurement_size.restype = ctypes.c_size_t
|
||||
if loaded.mesh_arm_measurement_size() != ctypes.sizeof(_NativeState):
|
||||
raise RuntimeError('native/Python measurement layout mismatch')
|
||||
return loaded, directory
|
||||
|
||||
|
||||
class ArmInstructionMeter:
|
||||
def __init__(self, machine, prefer_native=True):
|
||||
self.backend = 'python'
|
||||
self._state = {'minimum_sp': 0, 'instructions': 0}
|
||||
self._native = None
|
||||
if prefer_native:
|
||||
try:
|
||||
self._install_native(machine)
|
||||
self.backend = 'native'
|
||||
return
|
||||
except (RuntimeError, OSError, AttributeError) as error:
|
||||
print('ARM meter Python fallback: ' + str(error), file=sys.stderr)
|
||||
self.handle = machine.hook_add(UC_HOOK_CODE, self._measure_python)
|
||||
|
||||
def _install_native(self, machine):
|
||||
if unicorn.__version__ != '2.1.4':
|
||||
raise RuntimeError('native callback requires reviewed Unicorn 2.1.4 bindings')
|
||||
from unicorn.unicorn_py3 import unicorn as bindings
|
||||
library, directory = _native_library()
|
||||
state = _NativeState(ctypes.cast(bindings.uclib.uc_reg_read, ctypes.c_void_p),
|
||||
arm.UC_ARM_REG_SP, 0, 0, 0)
|
||||
callback = bindings.HOOK_CODE_CFUNC(('mesh_arm_measure_instruction', library))
|
||||
handle = bindings.uc_hook_h()
|
||||
result = bindings.uclib.uc_hook_add(machine._uch, ctypes.byref(handle), UC_HOOK_CODE,
|
||||
callback, ctypes.byref(state), ctypes.c_uint64(1), ctypes.c_uint64(0))
|
||||
if result:
|
||||
raise RuntimeError('native Unicorn hook registration failed: ' + str(result))
|
||||
# Keep the C callback, its context, loaded code and temporary file alive
|
||||
# for at least the lifetime of the registered machine hook.
|
||||
machine._callbacks[handle.value] = (callback, state, library, directory)
|
||||
self._native, self.handle = state, handle.value
|
||||
|
||||
def _measure_python(self, machine, address, size, data):
|
||||
self._state['minimum_sp'] = min(self._state['minimum_sp'], machine.reg_read(arm.UC_ARM_REG_SP))
|
||||
self._state['instructions'] += 1
|
||||
|
||||
def reset(self, top):
|
||||
if self._native is not None:
|
||||
self._native.minimum_sp, self._native.instructions, self._native.error = top, 0, 0
|
||||
else:
|
||||
self._state.update(minimum_sp=top, instructions=0)
|
||||
|
||||
def snapshot(self):
|
||||
if self._native is not None:
|
||||
if self._native.error:
|
||||
raise RuntimeError('Unicorn SP read failed: ' + str(self._native.error))
|
||||
return {'minimum_sp': self._native.minimum_sp,
|
||||
'instructions': self._native.instructions}
|
||||
return dict(self._state)
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check canonical Reader replies and stack using the linked RAK Full ARM ELF.
|
||||
|
||||
Requires unicorn==2.1.4. Every first page and all pages of the three longest
|
||||
Requires unicorn==2.1.4. A host C compiler accelerates the identical per-
|
||||
instruction measurement callback; otherwise Python is used. Every first page and all pages of the three longest
|
||||
verses are compared with the independently ASCII-normalized pinned source.
|
||||
--every-page reconstructs every verse. Only replyVerse and its callee frames
|
||||
are measured; the fixed caller reserve is not a physical high-water reading
|
||||
@@ -14,7 +15,8 @@ from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from firmware_elf import FirmwareElf
|
||||
from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS, UC_HOOK_CODE
|
||||
from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS
|
||||
from arm_instruction_meter import ArmInstructionMeter
|
||||
from unicorn import arm_const as arm
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -41,11 +43,7 @@ def check(path, every_page=False):
|
||||
uc.mem_write(section[3], elf.section_bytes(section))
|
||||
stop, top, reply = 0x1000, 0x2003e000, 0x20030000
|
||||
uc.mem_write(stop, b'\0\xbe')
|
||||
state = {'minimum_sp': top, 'instructions': 0}
|
||||
def instruction(machine, address, size, data):
|
||||
state['minimum_sp'] = min(state['minimum_sp'], machine.reg_read(arm.UC_ARM_REG_SP))
|
||||
state['instructions'] += 1
|
||||
uc.hook_add(UC_HOOK_CODE, instruction)
|
||||
meter = ArmInstructionMeter(uc)
|
||||
document = json.loads((ROOT / 'tools/bible/reader-web.json').read_text(encoding='utf-8'))
|
||||
counts = (51, 25, 36, 54, 47, 71, 53, 59, 41, 42, 57,
|
||||
50, 38, 31, 27, 33, 26, 40, 42, 31, 25)
|
||||
@@ -59,6 +57,7 @@ def check(path, every_page=False):
|
||||
reverse=True)[:3])
|
||||
maximum, calls, total_instructions, checked_bytes = 0, 0, 0, 0
|
||||
max_ref = None
|
||||
maximum_instructions, instruction_ref, instruction_page = 0, None, None
|
||||
guard = b'\x91\xa2\xb3\xc4'
|
||||
for index, ref in enumerate(references):
|
||||
raw = document['verses'][ref]
|
||||
@@ -72,7 +71,7 @@ def check(path, every_page=False):
|
||||
for register, value in zip((arm.UC_ARM_REG_R0, arm.UC_ARM_REG_R1, arm.UC_ARM_REG_R2, arm.UC_ARM_REG_R3), (selection, page, reply, capacity)):
|
||||
uc.reg_write(register, value)
|
||||
uc.mem_write(reply-4, guard + b'?'*capacity + guard)
|
||||
state['minimum_sp'], state['instructions'] = top, 0
|
||||
meter.reset(top)
|
||||
uc.emu_start(elf.address(name) | 1, stop, timeout=10000000, count=10000000)
|
||||
if uc.reg_read(arm.UC_ARM_REG_PC) != stop or uc.reg_read(arm.UC_ARM_REG_SP) != top:
|
||||
raise ValueError('Function return/stack restoration failed for ' + ref)
|
||||
@@ -92,6 +91,7 @@ def check(path, every_page=False):
|
||||
raise ValueError('Canonical linked text mismatch for ' + ref + ' page ' + str(page))
|
||||
reconstructed += fragment
|
||||
checked_bytes += len(fragment)
|
||||
state = meter.snapshot()
|
||||
used = top - state['minimum_sp']
|
||||
if used + 1536 > 8192:
|
||||
raise ValueError('Reader frames exceed 8 KiB with caller reserve: ' + str(used))
|
||||
@@ -99,6 +99,8 @@ def check(path, every_page=False):
|
||||
maximum, max_ref = used, ref
|
||||
calls += 1
|
||||
total_instructions += state['instructions']
|
||||
if state['instructions'] > maximum_instructions:
|
||||
maximum_instructions, instruction_ref, instruction_page = state['instructions'], ref, page
|
||||
if next_command is None:
|
||||
if reconstructed != expected:
|
||||
raise ValueError('Canonical text incomplete for ' + ref)
|
||||
@@ -120,7 +122,9 @@ def check(path, every_page=False):
|
||||
'canonical_source_sha256': source_digest,
|
||||
'maximum_reply_verse_stack_bytes': maximum, 'maximum_stack_reference': max_ref,
|
||||
'caller_reserve_bytes': 1536, 'loop_stack_bytes': 8192,
|
||||
'total_arm_instructions': total_instructions,
|
||||
'total_arm_instructions': total_instructions, 'measurement_backend': meter.backend,
|
||||
'maximum_reply_arm_instructions': maximum_instructions,
|
||||
'maximum_instruction_reference': instruction_ref, 'maximum_instruction_page': instruction_page,
|
||||
'scope': 'Actual linked replyVerse instructions and callees; excludes firmware caller/ISR/task and physical high-water.'}
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ TRIM_DEFINES = {
|
||||
# Keep MeshCore's own startup logo; only omit the invisible library bitmap.
|
||||
"SSD1306_NO_SPLASH": 1,
|
||||
}
|
||||
# The reviewed main runs the Arduino loop forever. Scheduler failure and every
|
||||
# normal reset/update path reset the MCU; global process-exit cleanup cannot run.
|
||||
# Unknown/custom core revisions retain the standard ABI registration behavior.
|
||||
NRF52_MAIN_NO_EXIT_SHA256 = "25c3a05b2631a6daff58048a517b8f86c33e97aa978d8b365f03a5b4fa371231"
|
||||
|
||||
CC310_ARCHIVE_SHA256 = "97dc648d44520e47252f62c402a71f976e36d7b3aef99235bbdc4de79a928577"
|
||||
CC310_ARCHIVE = "src/cortex-m4/fpv4-sp-d16-hard/libnrf_cc310_0.9.13-no-interrupts.a"
|
||||
|
||||
@@ -149,6 +154,13 @@ def supported_cc310(framework_dir, mcu, defines):
|
||||
return archive.is_file() and hashlib.sha256(archive.read_bytes()).hexdigest() == CC310_ARCHIVE_SHA256
|
||||
|
||||
|
||||
def supported_program_lifetime(framework_dir):
|
||||
if not framework_dir:
|
||||
return False
|
||||
main = Path(framework_dir) / "cores/nRF5/main.cpp"
|
||||
return main.is_file() and hashlib.sha256(main.read_bytes()).hexdigest() == NRF52_MAIN_NO_EXIT_SHA256
|
||||
|
||||
|
||||
def companion_trim_policy(platform, mcu, src_filter, flags=(), cppdefines=()):
|
||||
"""Qualify the actual Companion sources, independent of board/alias name."""
|
||||
if platform != "nordicnrf52" or not str(mcu).lower().startswith("nrf52"):
|
||||
@@ -237,6 +249,9 @@ def install(build_env):
|
||||
print("nRF52 Companion flash trim: small crypto table, unused USB device classes/GPS cache/vendor splash; fonts and Reader retained")
|
||||
framework = platform.get_package_dir("framework-arduinoadafruitnrf52")
|
||||
values = flag_defines(flags, cppdefines)
|
||||
if "MESH_NRF52_NO_PROGRAM_EXIT" not in values and supported_program_lifetime(framework):
|
||||
build_env.AppendUnique(CPPDEFINES=[("MESH_NRF52_NO_PROGRAM_EXIT", 1)])
|
||||
print("nRF52 reviewed firmware lifetime: omit unreachable process-exit registrations")
|
||||
if infrastructure and values.get("ST7789") is not None:
|
||||
# Keep the font license with this qualified display build, including
|
||||
# cached DFU packages. The hook independently checks the effective
|
||||
|
||||
Reference in new issue
Block a user