目录
[一、嵌入式 C 高频内存风险点](#一、嵌入式 C 高频内存风险点)
[1. 局部数组过大造成栈溢出【最常见】](#1. 局部数组过大造成栈溢出【最常见】)
[2. 数组越界访问](#2. 数组越界访问)
[3. 野指针与空指针解引用](#3. 野指针与空指针解引用)
[4. 未初始化局部变量](#4. 未初始化局部变量)
[5. 中断与主循环共享数据缺少保护](#5. 中断与主循环共享数据缺少保护)
[二、安全编码实操规范 + 正反示例](#二、安全编码实操规范 + 正反示例)
[1. 函数传参带上缓冲区长度](#1. 函数传参带上缓冲区长度)
[2. 禁止无边界字符串函数](#2. 禁止无边界字符串函数)
[3. 慎用 malloc 堆内存](#3. 慎用 malloc 堆内存)
[4.typedef 封装结构体,结构体初始化清零](#4.typedef 封装结构体,结构体初始化清零)
[四、Keil MDK 重要编译选项配置(提升代码安全性)](#四、Keil MDK 重要编译选项配置(提升代码安全性))
前言
结构做好之后,就要面对 C 语言本身带来的各类隐性风险:数组越界、野指针、栈溢出、内存踩踏、未初始化变量,这类 Bug 不会必现,属于偶现玄学故障,复现难度极高,量产设备死机大部分根源都来源于此。
本篇聚焦嵌入式 C 安全编码,附带可直接参考的代码示例,讲解内存风险点、编码约束、栈 / 堆防护、编译器选项配置,从编码层面提前规避隐性漏洞。
一、嵌入式 C 高频内存风险点
1. 局部数组过大造成栈溢出【最常见】
反面错误代码
void func(void)
{
//局部变量存放在栈,栈空间通常只有1K‑4K
uint8_t buf[1024];
//业务处理
}
问题:函数内定义大数组,栈空间直接被打穿,破坏返回地址,程序随机 HardFault 死机。
正确写法
void func(void)
{
//static变量存放全局内存段,不占用栈
static uint8_t buf[1024];
}
原则:大于 128 字节的缓冲区不要定义为局部自动变量,使用 static 或者全局。
2. 数组越界访问
uint8_t arr[8];
//错误,下标范围0~7,arr[8]属于越界,篡改周边内存
for(int i = 0; i <= 8; i++)
{
arr[i] = 0;
}
越界不会立刻报错,悄悄改写其他变量,出现随机数据错乱。
编码规范:循环边界严格控制,所有数组访问增加长度判断。
3. 野指针与空指针解引用
uint8_t *p = NULL;
*p = 0x01; //直接访问0地址,进入硬件异常
void test(uint8_t *buf)
{
//没有判空,如果传入NULL直接崩溃
buf[0] = 1;
}
安全写法,入口处增加指针有效性校验
void test(uint8_t *buf, uint16_t len)
{
if(buf == NULL || len == 0)
{
return;
}
buf[0] = 1;
}
4. 未初始化局部变量
局部栈变量不会自动清零,里面是随机垃圾值。
void demo(void)
{
uint16_t val;
if(val > 100) //val随机,条件行为不可预测
{
}
}
规范:局部变量手动初始化。
5. 中断与主循环共享数据缺少保护
uint32_t g_cnt = 0;
void SysTick_Handler(void)
{
g_cnt++;
}
void main_task(void)
{
uint32_t temp = g_cnt;
//32位以下单片机,读取4字节变量会被中断打断,读取一半数据
}
共享多字节变量读写需要关闭中断做临界区保护
uint32_t get_g_cnt(void)
{
uint32_t temp;
__disable_irq();
temp = g_cnt;
__enable_irq();
return temp;
}
二、安全编码实操规范 + 正反示例
1. 函数传参带上缓冲区长度
❌不安全
void parse_msg(uint8_t *buf)
{
//不知道buf实际大小,极易越界
}
✅安全
void parse_msg(uint8_t *buf, uint16_t buf_len)
{
if(buf == NULL) return;
}
2. 禁止无边界字符串函数
strcpy、sprintf没有长度检测,极易缓冲区溢出。
替换方案:
strncpy、snprintf
char out_buf[64];
//不安全
//sprintf(out_buf,"data:%d",val);
//安全
snprintf(out_buf, sizeof(out_buf), "data:%d", val);
3. 慎用 malloc 堆内存
单片机堆资源很小,频繁 malloc/free 产生内存碎片,长时间运行内存耗尽。
//不推荐频繁动态申请
uint8_t *p = malloc(128);
free(p);
量产建议:优先静态数组 static,尽量减少堆的使用。如果必须使用堆,做好返回值判空。
4.typedef 封装结构体,结构体初始化清零
typedef struct
{
uint16_t id;
uint16_t param;
}dev_cfg_t;
dev_cfg_t cfg = {0}; //全部成员初始化为0
三、栈空间分析与防护手段
-
根据芯片手册配置栈大小,启动文件
startup_stm32f10x.sStack_Size EQU 0x400 ;1024字节栈
Heap_Size EQU 0x200 ;512字节堆
2.RTOS 任务栈:每个任务栈预留冗余,不能刚好够用。 3. 栈水印检测,FreeRTOS 可以开启栈水位统计,查看栈最大使用深度。
//查询任务剩余栈空间
uxTaskGetStackHighWaterMark(NULL);
四、Keil MDK 重要编译选项配置(提升代码安全性)
1.‑O1 / ‑O2 优化等级
- O0 调试方便,但代码体积大,产品不要使用 O0 发布;
- 量产一般 O1,追求速度选 O2; 禁止开发调试直接用 O2,优化会删掉调试变量,故障难以定位。
-
开启警告提示,把警告当作错误处理 Keil 设置勾选 Treat Warnings as Errors,很多内存隐患编译器警告即可提前发现。
-
硬件溢出检测,部分内核可以开启栈溢出检测。
-
生成 map 文件,分析内存占用 map 文件查看:ROM、RAM 全局变量占用大小,定位大缓冲区,防止 RAM 耗尽。
五、常用防御性编程技巧
-
所有外部输入:通信数据、参数、ADC 采样值做范围限制,超过阈值直接截断。
val = val > 4000 ? 4000 : val;
2.switch 语句必须写 default 分支,拦截未定义 case。
switch(mode)
{
case 0: break;
case 1: break;
default:
mode = 0; //异常兜底
break;
}
-
重要状态变量增加魔术字校验,存储参数增加校验标志,判断参数是否有效。 4. 断言 Assert,调试阶段捕获非法入参,发布版本关闭断言。
#ifdef DEBUG
#define ASSERT(x) if(!(x)){while(1);}
#else
#define ASSERT(x)
#endifvoid set_val(uint16_t v)
{
ASSERT(v < 1000);
}
六、容易忽略的隐式风险
-
有符号数与无符号数混合比较,出现逻辑异常。
int a = -1;
uint16_t b = 10;
if(a < b) //‑1会被转成很大无符号数,判断逻辑出错
{}
规范:变量尽量保持符号类型一致。
-
位操作运算符优先级,& | 低于 ==,条件表达式务必加括号。
//错误
if(flag & 0x01 == 0)//正确
if((flag & 0x01) == 0)
小结
硬件异常 HardFault 很多时候不是芯片问题,而是 C 语言内存不规范埋下的隐患。 本篇编码规则配合上一篇分层模块化架构,可以大幅降低随机死机、偶现 Bug。
- 大缓冲区优先 static,拒绝局部大数组
- 指针判空、数组边界校验
- 淘汰 strcpy/sprintf,使用带长度限制接口
- 临界区保护共享多字节变量
- 善用 map 文件、编译器警告提前发现内存问题