用栈水位线与结构体哨兵检测内存损坏

问题

偶发复位、看门狗、跑飞、字段值莫名变化,调试器接上现场已经丢。背后的根因通常是:

在主机和现代芯片上,ASan、MPU、编译器栈保护、KASLR 都能定位这类问题。换到下列环境就用不上:

下文给两种低成本检测方法,运行时开销可控,编译器无要求。共同思路:往内存里写一个业务代码不会自然写入的标记字节,事后回来看它有没有被改过。下文统一用 0xA50x5A,挑值理由见文末。

一、栈水位线

栈水位线两种实现对比

要回答的问题是:线上跑这么久,栈历史最深用到过哪。

反例:周期采样 SP

void timer_tick_isr(void) {
    uintptr_t sp_now = read_sp();
    if (sp_now < sp_min) sp_min = sp_now;  // 栈向低地址增长
}

采样式测量。两次 tick 之间的瞬时栈峰值采不到。线上偶发复位排查无效。

正解:上电填 0xA5,按需反扫

启动早期把栈区全部填为 0xA5。栈字节被压过又弹出后,值不再是 0xA5。任意时刻扫描栈区,第一个非 0xA5 字节的位置就是历史最大栈深度。

// 以栈向低地址增长为例(Cortex-M、x86 等)
extern uint8_t __stack_base;   // 栈区低地址端
extern uint8_t __stack_top;    // 栈区高地址端,SP 初值
#define STACK_MAGIC 0xA5

void stack_paint(void) {
    for (uint8_t *p = &__stack_base; p < &__stack_top; ++p)
        *p = STACK_MAGIC;
}

size_t stack_used_bytes(void) {
    uint8_t *p = &__stack_base;
    while (p < &__stack_top && *p == STACK_MAGIC) ++p;
    return (size_t)(&__stack_top - p);
}

stack_paint 必须在任何业务函数压栈之前调用,常见放法是 main 入口第一行,或链接脚本指定的启动钩子。栈向高地址增长(8051 等)只需把 stack_used_bytes 的扫描方向反过来。

局限

二、结构体 / 缓冲区哨兵

结构体哨兵布局

要回答的问题是:这块缓冲区 / 这个结构体有没有被别人踩过。

做法是给关键对象前后各加一段哨兵字节,定期检查:

typedef struct {
    uint8_t      guard_head[4];   // A5 A5 A5 A5
    sensor_pkt_t obj;
    uint8_t      guard_tail[4];   // 5A 5A 5A 5A
} safe_sensor_pkt_t;

void guard_init(safe_sensor_pkt_t *p) {
    for (int i = 0; i < 4; ++i) {
        p->guard_head[i] = 0xA5;
        p->guard_tail[i] = 0x5A;
    }
}

bool guard_check(const safe_sensor_pkt_t *p) {
    for (int i = 0; i < 4; ++i) {
        if (p->guard_head[i] != 0xA5) return false;
        if (p->guard_tail[i] != 0x5A) return false;
    }
    return true;
}

head 用 0xA5、tail 用 0x5A,破坏发生时立即区分前向越界与后向溢出。

检查时机

检测覆盖

三、组合套餐

层级 手段 检查时机 RAM 成本
上电 paint,按需反扫 故障时 / 低频任务 占用栈区,无额外 RAM
关键对象 head / tail 哨兵 协议入口、低频任务 每对象 8 B
Flash 配置 CRC + 魔术头 上电、写入后 数字节
复位原因 保留 1 字节 + WDT 上电早期读取 1 B

复位原因码做法:保留 1 字节非初始化 RAM,检测到哨兵破坏时写入原因码并触发软件复位,上电早期通过串口或 LED 输出。代价 1 字节 RAM,能把"看门狗复位"区分成"哨兵 A 被踩"、"栈撞顶"等可定位信号。

四、注意

附:魔法数字选值

魔法数字选值依据

挑值的三个硬性条件:

满足条件的常用值:

二进制 备注
0xA5 1010 0101 高低交错
0x5A 0101 1010 0xA5 互补
0xAA 1010 1010 严格交替

0xA50x5A 互为镜像,用作 head / tail 哨兵能区分越界方向。0x000xFF 不可用:前者与未初始化 RAM 难以区分,后者与擦除态 Flash 和上电默认值难以区分。0x550xA5 等价,可作备用。

总结

本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。