Fit Nordic Full KISS with lossless assets and firmware lifetime trimming

This commit is contained in:
mikecarper committed 2026-10-26 18:21:18 -07:00
1 parent 2369a3d28c
commit b4bec19e88
51 files changed
+7739 -17

No files matched your search

+27
View File
@@ -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;
}
+93
View File
@@ -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)
+13 -9
View File
@@ -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.'}
+15
View File
@@ -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