一个用于 Cortex-M 设备脱离调试器运行时检测内存踩踏和非法访问的独立 C99 模块。它利用处理器 DWT 数据观察点和 DebugMonitor,在目标地址被读写时捕获 PC、LR、xPSR 等现场,再由主循环或 RTOS 诊断任务完成日志、符号化和处置。
内存踩踏经常表现为“某个变量偶尔被改坏”,真正的写入位置未知,而且问题可能只在 设备运动、无线连接、传感器工作、休眠唤醒或长时间运行等真实环境中出现。 Ozone、GDB 等常规调试方式通常需要设备持续连接 SWD/JTAG 调试器或飞线,这会限制 设备移动,也可能改变系统时序、功耗和运行环境,难以复现只在真实使用状态发生的 问题。
本模块把 DWT 观察点配置和事件采集直接放进固件。设备可以断开 halting debugger, 在正常形态下独立运行;命中事件由静态队列保存,并可交给 FreeRTOS worker task 异步记录。因此它特别适合:
- 在可疑变量、对象字段或内存地址已知时,定位未知代码从哪里执行了越界或非法写入;
- 分析非法读取、非预期状态修改、释放后仍访问以及并发时序导致的数据破坏;
- 在运动、无线、传感器和低功耗切换等真实设备场景下持续检测;
- 对低概率、长时间后才出现的问题进行多设备批量压测,而不需要为每台设备持续连接调试器;
- 在线上复现固件或诊断版本中保留轻量现场,事后结合对应 ELF/MAP 定位函数和源码。
典型使用方式是:先根据损坏结果确定需要保护的 1/2/4 字节地址,再设置 WRITE
watchpoint;未知代码写入该地址时无需调用本模块,也不需要插入 __DSB()。模块会在
Runtime 自动捕获异常返回现场。
它与 Ozone/GDB 是互补关系,而不是替代关系:本模块负责脱机、长时间和批量运行时
取证,捕获后仍建议使用对应 ELF、addr2line、反汇编以及常规调试器完成源码分析。
DWT 是访问后的审计机制,不能阻止踩踏;记录的 stacked_pc 也不保证恰好等于
STR/LDR 指令地址。
核心实现不依赖 RTOS、不使用动态内存,并且不在 DebugMonitor 异常中执行 I/O; FreeRTOS 仅作为可选下半段适配层。
当前发布版本:0.3.0。STM32F407 + FreeRTOS V11.3.0 真机闭环已经验证通过。 快速移植请阅读 适配指南,原始板端日志见 真机验证记录。
- DWTv1:Cortex-M3/M4/M7(Armv7-M/Armv7E-M)。
- DWTv2:Cortex-M33/M35P/M52/M55/M85(Armv8-M Mainline/Armv8.1-M)。
- 单地址 1/2/4 字节读、写、读写观察点。
- 固定资源池、代际 handle、启停/删除、DebugMonitor 事件队列。
- 空闲、指定掩码和独占三种 DWT 比较器认领策略。
- 保存并恢复模块接管的比较器、DEMCR 位和 DebugMonitor 优先级。
- DWTv2 按 FUNCTION.ID 探测数据地址能力,DWTv1 使用安全回读探测。
- 裸机优先;模块本身不包含 FreeRTOS/CMSIS-RTOS 依赖。
- 可选非法读取判定层:按异常 PC 和授权代码范围分类,不在异常中执行策略。
- 系统无关事件就绪回调,以及可选 FreeRTOS task-notification 下半段适配层。
Cortex-M0/M0+/M23 没有本模块所需的完整 DebugMonitor 路径,当前明确不支持。 地址范围、数据值匹配、SMP、多核和 TrustZone 跨安全域也不在第一版范围内。
发布包不内置 CMSIS-Core、芯片 Device Header 或 FreeRTOS Kernel。当前版本使用官方 CMSIS 6.3.0 和 FreeRTOS Kernel V11.3.0 完成验证,集成时也可以使用目标 SDK 自带的 兼容版本。
实际 MCU 工程应包含厂商 device header(推荐),因为它会间接包含正确的
core_cm*.h。编译 src/cwp.c 时定义:
target_compile_definitions(your_target PRIVATE
CWP_CMSIS_HEADER="stm32f4xx.h"
CWP_PROVIDE_DEBUGMON_HANDLER=1)
target_include_directories(your_target PRIVATE
path/to/cmsis-runtime-watchpoint/include)
target_sources(your_target PRIVATE
path/to/cmsis-runtime-watchpoint/src/cwp.c
path/to/cmsis-runtime-watchpoint/src/cwp_debugmon_gcc.c)若用本目录的 CMake,也可以直接 add_subdirectory(...) 后链接源码型目标:
target_link_libraries(your_target PRIVATE cwp::core)CWP_CMSIS_HEADER 必须定义在最终 your_target 上;模块不能在不知道具体 MCU
device header 的情况下预编译成通用二进制库。
如果启动文件已经定义 DebugMon_Handler,不要设置
CWP_PROVIDE_DEBUGMON_HANDLER;在已有处理器中按示例汇编取得 MSP/PSP 和
EXC_RETURN,再调用 cwp_debugmon_dispatch()。提供的 handler 适用于 Arm 目标上的
GCC/Clang。
static volatile uint32_t state;
cwp_watchpoint_config_t cfg = {
.address = (uintptr_t)&state,
.size = sizeof(state),
.access = CWP_ACCESS_WRITE,
.user_data = 42
};
cwp_handle_t handle;
cwp_init(3);
cwp_create(&cfg, &handle);
for (;;) {
cwp_event_t event;
while (cwp_poll_event(&event)) {
/* event.pc 是触发访问后的返回现场,用 ELF/map 文件解析。 */
}
}cwp_init() 是兼容便捷入口,默认只认领初始化时空闲且支持数据地址匹配的比较器,
不会清空已经启用的外部比较器。需要固定资源边界时使用扩展入口:
cwp_init_config_t init = {
.struct_size = sizeof(cwp_init_config_t),
.api_version = CWP_INIT_CONFIG_VERSION,
.debug_monitor_priority = 3,
.claim_policy = CWP_CLAIM_MASK,
.comparator_mask = (1UL << 1) | (1UL << 2)
};
cwp_status_t status = cwp_init_ex(&init);认领策略:
CWP_CLAIM_FREE_ONLY:认领所有空闲且有能力的比较器,默认且最安全。CWP_CLAIM_MASK:只检查和认领掩码内的比较器;任一繁忙或不支持则整体失败。CWP_CLAIM_EXCLUSIVE:接管全部有能力的比较器,退出时恢复初始化前的配置。
cwp_get_capabilities() 可查询已实现、支持数据地址、初始繁忙及本模块已认领的位图。
若调试器在初始化后抢占了一个尚未配置的已认领比较器,创建操作会返回
CWP_ERROR_RESOURCE_BUSY,不会覆盖它。
DebugMonitor 优先级必须结合芯片的 __NVIC_PRIO_BITS 和应用中断优先级规划。
若芯片实现了 DWT Lock Access Register,需在编译时令
CWP_PLATFORM_PREPARE() 完成厂商规定的解锁。调试器也可能占用 DWT 比较器;产品
运行时使用前应规定“调试器观察点”和本模块的所有权边界。处理器处于 halting
debug(C_DEBUGEN=1)时,匹配通常交给调试器而不是 DebugMonitor,因此不要把
“连接调试器”和“脱机运行”的行为视为完全等价。
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
CMSIS_CORE_INCLUDE=/path/to/CMSIS/Core/Include ./tests/cross_compile.sh测试不会声称模拟 Cortex-M 指令流水线;它验证 DWTv1/v2 寄存器编码、资源管理、
异常栈解析、事件队列与溢出行为。交叉编译脚本还会用 CMSIS 6.3.0 的真实
core_cm4.h/core_cm33.h 验证 DWTv1、DWTv2 和 GCC DebugMon handler。最终仍需
在目标芯片验证 DebugMonitor 是否实现、
比较器数量、MATCHED 清除语义,以及调试器连接时的行为。
DWT 只能报告“受监控地址发生了读取”,不知道业务上的合法与非法。M2 的
cwp_policy 在主循环取出 cwp_event_t 后,用异常 PC 是否落在授权代码区进行分类:
READ watchpoint -> DebugMonitor -> event.pc
|
authorized section range?
yes / no
allowed / unauthorized
不要用“函数入口地址 + 猜测长度”计算函数结束地址,也不要用下一个函数的入口作为 结束地址。编译优化、LTO、链接排序、尾调用和对齐填充都会使这种方法不可靠。
推荐把授权读取函数放入专用 section:
#include "cwp_policy.h"
static uint32_t read_secret(const volatile uint32_t *address)
CWP_AUTHORIZED_CODE;
static uint32_t read_secret(const volatile uint32_t *address)
{
uint32_t value = *address;
__DSB(); /* 让异步数据 watchpoint 在返回授权区前被接收。 */
return value;
}链接脚本在普通 .text 之前增加:
.cwp_authorized :
{
. = ALIGN(4);
__cwp_authorized_start__ = .;
KEEP(*(.cwp_authorized))
KEEP(*(.cwp_authorized.*))
. = ALIGN(4);
__cwp_authorized_end__ = .;
} > FLASH应用使用链接器自动生成的半开区间 [begin, end):
extern const uint8_t __cwp_authorized_start__[];
extern const uint8_t __cwp_authorized_end__[];
static const cwp_code_range_t allowed[] = {{
(uintptr_t)__cwp_authorized_start__,
(uintptr_t)__cwp_authorized_end__
}};
cwp_read_policy_t policy;
cwp_read_policy_init(&policy, allowed, 1);
while (cwp_poll_event(&event)) {
cwp_policy_result_t result;
cwp_read_policy_classify(&policy, &event, &result);
if (result.verdict == CWP_POLICY_READ_UNAUTHORIZED) {
/* 延迟日志、统计、复位或上报。 */
}
}用于非法读取判定的观察点必须配置 CWP_ACCESS_READ。DWTv1 的 READ_WRITE 匹配只
表明发生了其中一种访问,事件无法反推出实际是哪一种,策略层会将其标记为
CWP_POLICY_NOT_READ,避免误报。
这是一种审计和告警机制:读取指令已经执行。需要在读取发生前阻止访问时,应由 MPU 或 TrustZone 提供权限隔离,DWT 用于补充诊断。
核心层通过 cwp_set_event_ready_callback() 提供系统无关唤醒点。回调在
DebugMonitor 异常上下文、事件写入静态队列之后执行,只能进行有界且异常安全的
通知操作。未配置回调时,原来的轮询方式保持不变。
FreeRTOS 适配层位于 ports/freertos,不进入核心源文件,也不会自动创建任务:
static cwp_freertos_notifier_t notifier;
static void cwp_worker(void *argument)
{
cwp_freertos_notifier_bind_current(¬ifier);
cwp_init(8); /* 必须满足 FreeRTOS FromISR 优先级约束。 */
for (;;) {
cwp_freertos_wait(portMAX_DELAY);
while (cwp_poll_event(&event)) {
/* 当前 task/PSP:分类、日志和处置。 */
}
}
}适配层默认占用 task notification slot 0。支持 notification array 的工程可以定义:
#define CWP_FREERTOS_NOTIFICATION_INDEX 2U并保证它小于 configTASK_NOTIFICATION_ARRAY_ENTRIES。notifier 对象必须比绑定关系存活
更久;删除 worker task 前先调用 cwp_freertos_notifier_unbind()。
DebugMonitor 回调调用 FreeRTOS FromISR API 时,其数值优先级必须不小于
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。例如最大系统调用优先级为 5 时,可用
5~15,示例选择 8。内核临界区可能暂时延迟该异常,这是允许调用 RTOS API所需的
代价。
M3 还提供 cwp_access_policy_t,分别配置 READ 和 WRITE 授权范围。调用者必须分别
创建 CWP_ACCESS_READ 和 CWP_ACCESS_WRITE 观察点;READ_WRITE 在 DWTv1 上不能说明
实际触发类型,因此返回 CWP_ACCESS_POLICY_NOT_APPLICABLE。
cwp_event_t.pc 为硬件异常栈中的异常返回 PC,并带有
CWP_EVENT_FLAG_PC_IS_EXCEPTION_RETURN。它用于判断最终落在哪个授权代码区,但硬件
不保证它就是 LDR/STR 指令地址;数据 watchpoint 可能在后续 DSB 等同步点被接收。
因此日志应写作 stacked_pc,不要宣称为精确 access_instruction_pc。授权函数应在
访问后、离开授权 section 前执行 DSB,确保范围判定稳定。
STM32F407 + FreeRTOS V11.3.0 实板验证结果:授权 READ、非法 READ、授权 WRITE、
非法 WRITE 各产生一次事件;FreeRTOS worker 通过 task notification 被唤醒 4 次,
分类全部正确且 dropped=0。可独立构建的验证工程位于
examples/stm32f407_freertos,原始日志已随发布包保存。
- API 只在主循环/线程上下文创建和销毁观察点;DebugMonitor 只生产事件。
- 队列满时保留已有事件并增加
cwp_dropped_event_count(),不会阻塞异常处理。 - 浮点扩展异常栈依据 EXC_RETURN 自动跳过 18 个字;如平台启用了非标准上下文 保存,应由平台 handler 传入正确的硬件栈帧。
cwp_deinit()只撤销由本模块首次设置的 TRCENA/MON_EN;初始化前已经开启的位 保持不变,并恢复原 DebugMonitor 优先级。- DWTv1 与 DWTv2 的访问编码不同,后端会把统一 API 映射到各自架构编码;调用者
不应把
cwp_access_t的数值直接写入 DWT FUNCTION。
代码采用 Apache-2.0;CMSIS_6 由 Arm 以 Apache-2.0 发布,版权归各自作者。 第三方依赖不会打入发布包,版本与许可证边界见 THIRD_PARTY_NOTICES.md。版本变更见 CHANGELOG.md。
后续生产化增强、优先级与验收标准见 OPTIMIZATION_ROADMAP.md。