3、故障注入技术:硬件故障注入与软件故障注入

故障注入,说白了就是故意让系统出问题。你可能会问:好好的系统,干嘛要折腾它?

我在项目中遇到过不少这样的场景:产品在实验室跑得好好的,一到客户现场就莫名其妙地重启。查了几天,最后发现是电源纹波在特定负载下超标了。如果早做电压扰动注入,这个问题在研发阶段就能暴露。

这一章,我们就来聊聊怎么主动给系统“下绊子”,看看它到底有多扛造。

3.1 硬件故障注入

硬件故障注入,就是直接在物理层面对电路施加干扰。我个人习惯把它分成两类:电压扰动和信号毛刺。

3.1.1 电压扰动注入

电压扰动,说白了就是让电源“喘口气”。你想想看,一个嵌入式系统如果供电不稳,会发生什么?

  • 欠压:CPU 逻辑电平混乱,程序跑飞
  • 过压:器件击穿,永久损坏
  • 纹波:时钟抖动,通信误码
  • 跌落:系统复位,数据丢失

我在做一款工业控制器时,就遇到过纹波导致 ADC 采样值跳变的问题。当时用示波器一看,电源纹波有 80mV,而 ADC 的参考电压纹波抑制比只有 60dB,算下来误差刚好超标。

我的经验:电压扰动注入不要只盯着额定值。试试在额定电压的 ±10% 边界上做长时间跑测,很多隐性 bug 会自己冒出来。

常用的注入手段有这些:

注入方式 实现方法 典型场景
直流偏移 串联可调直流源 模拟电池电压下降
交流叠加 信号发生器 + 功率放大器 模拟电源纹波
瞬态跌落 电子负载快速切换 模拟大电流冲击
脉冲干扰 静电枪 / 浪涌发生器 模拟 ESD / 雷击

3.1.2 信号毛刺注入

信号毛刺,就是给数字信号线上塞一个窄脉冲。为什么这么做?因为很多芯片的时序容限并没有数据手册写的那么宽裕。

我曾经测试一款 SPI Flash,数据手册说片选信号低电平时间最小 20ns。我实际用毛刺发生器注入 18ns 的片选脉冲,结果有 3% 的概率读出错误数据。这个 bug 在正常时序下根本复现不了。

注意:毛刺注入要控制能量。太窄的毛刺可能被管脚电容滤掉,太宽又可能损坏器件。我一般从数据手册标称值的 80% 开始试,逐步收窄。

信号毛刺的典型注入点:

  • 时钟线:注入毛刺导致时钟沿偏移,触发时序违规
  • 数据线:注入毛刺改变数据采样值,模拟串扰
  • 控制线:注入毛刺模拟错误的使能/复位信号
  • 中断线:注入毛刺模拟虚假中断

3.2 软件故障注入

硬件注入成本高、周期长。软件注入就灵活多了,你可以在代码层面模拟各种异常。

3.2.1 代码插桩

代码插桩,就是在源代码或二进制中插入故障触发点。我常用的做法是:

  1. 在关键函数入口/出口插入钩子
  2. 运行时通过配置文件控制故障类型和概率
  3. 故障触发后记录上下文(寄存器、堆栈)

举个例子,模拟内存分配失败:

// 原始代码
void* my_malloc(size_t size) {
    return malloc(size);
}

// 插桩后的代码
void* my_malloc(size_t size) {
    // 故障注入点:模拟 5% 概率的分配失败
    if (fault_inject_enabled && (rand() % 100 < 5)) {
        log_fault("malloc_fail", size);
        return NULL;
    }
    return malloc(size);
}

嗯,这里要注意:插桩代码不能影响正常逻辑的时序。我见过有人插桩后忘了关,结果生产版本里还带着故障注入代码,导致系统随机崩溃。

核心原则:插桩代码必须通过编译开关控制,默认关闭。发布前做一次全局扫描,确保所有故障注入点都被禁用。

3.2.2 API 劫持

API 劫持,就是拦截系统调用或库函数,替换成你的故障模拟版本。这种方法不需要修改源代码,特别适合测试第三方库或闭源组件。

在 Linux 上,我常用 LD_PRELOAD 来实现:

// fault_malloc.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

void* malloc(size_t size) {
    static void* (*real_malloc)(size_t) = NULL;
    if (!real_malloc) {
        real_malloc = dlsym(RTLD_NEXT, "malloc");
    }
    
    // 模拟 10% 概率返回 NULL
    if (rand() % 100 < 10) {
        fprintf(stderr, "[FAULT] malloc(%zu) returns NULL\n", size);
        return NULL;
    }
    return real_malloc(size);
}

// 编译:gcc -shared -fPIC -o libfault_malloc.so fault_malloc.c -ldl
// 运行:LD_PRELOAD=./libfault_malloc.so ./your_program

我在测试一个网络协议栈时,就用这个手法劫持了 send() 和 recv(),模拟丢包和超时。效果立竿见影,半小时就找到了三个未处理的错误码。

小技巧:API 劫持可以配合概率控制。我习惯用三段式:0%(关闭)、1%~5%(低频测试)、50%(压力测试)。低频测试用来验证错误处理路径是否被执行,高频测试用来压系统的容错极限。

3.3 故障注入知识体系

下面这张图,是我自己梳理的故障注入技术全景。你可以把它当作一个检查清单,做可靠性验证时对照着看,不容易漏项。

故障注入技术全景 故障注入 硬件故障注入 软件故障注入 电压扰动 信号毛刺 电磁干扰 代码插桩 API劫持 Fuzz测试 欠压/过压/纹波 时钟/数据/控制线 辐射/传导/耦合 函数入口/出口 系统调用/库函数 随机/变异输入 硬件注入适合验证物理层容错能力,软件注入适合验证逻辑层错误处理路径 两者结合使用,才能覆盖从芯片引脚到应用层的完整故障链路

3.4 故障注入的工程实践要点

最后,分享几个我在项目中踩过的坑:

  • 不要只做单点注入:真实故障往往是组合出现的。比如电压跌落的同时,通信线路上刚好有个毛刺。我建议设计组合注入场景。
  • 记录注入日志:每次注入的时间、类型、参数、系统响应都要记下来。否则复现问题的时候,你根本不知道当时做了什么。
  • 区分故障和缺陷:故障注入是为了验证系统的容错能力,不是用来找代码 bug 的。如果注入后系统崩溃,先确认是容错逻辑没写好,还是注入本身破坏了保护机制。
安全提醒:硬件故障注入,尤其是过压和浪涌测试,一定要做好防护。我见过有人用浪涌发生器时没接地,结果把自己电了一下。安全第一,别为了测试把自己搭进去。