单片机基础核心知识点汇总(二十一)

目录

前言

[一、嵌入式 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

三、栈空间分析与防护手段

  1. 根据芯片手册配置栈大小,启动文件startup_stm32f10x.s

    Stack_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,优化会删掉调试变量,故障难以定位。
  1. 开启警告提示,把警告当作错误处理 Keil 设置勾选 Treat Warnings as Errors,很多内存隐患编译器警告即可提前发现。

  2. 硬件溢出检测,部分内核可以开启栈溢出检测。

  3. 生成 map 文件,分析内存占用 map 文件查看:ROM、RAM 全局变量占用大小,定位大缓冲区,防止 RAM 耗尽。

五、常用防御性编程技巧

  1. 所有外部输入:通信数据、参数、ADC 采样值做范围限制,超过阈值直接截断。

    val = val > 4000 ? 4000 : val;

2.switch 语句必须写 default 分支,拦截未定义 case。

复制代码
switch(mode)
{
    case 0: break;
    case 1: break;
    default:
        mode = 0; //异常兜底
        break;
}
  1. 重要状态变量增加魔术字校验,存储参数增加校验标志,判断参数是否有效。 4. 断言 Assert,调试阶段捕获非法入参,发布版本关闭断言。

    #ifdef DEBUG
    #define ASSERT(x) if(!(x)){while(1);}
    #else
    #define ASSERT(x)
    #endif

    void set_val(uint16_t v)
    {
    ASSERT(v < 1000);
    }

六、容易忽略的隐式风险

  1. 有符号数与无符号数混合比较,出现逻辑异常。

    int a = -1;
    uint16_t b = 10;
    if(a < b) //‑1会被转成很大无符号数,判断逻辑出错
    {

    }

规范:变量尽量保持符号类型一致。

  1. 位操作运算符优先级,& | 低于 ==,条件表达式务必加括号。

    //错误
    if(flag & 0x01 == 0)

    //正确
    if((flag & 0x01) == 0)

小结

硬件异常 HardFault 很多时候不是芯片问题,而是 C 语言内存不规范埋下的隐患。 本篇编码规则配合上一篇分层模块化架构,可以大幅降低随机死机、偶现 Bug。

  • 大缓冲区优先 static,拒绝局部大数组
  • 指针判空、数组边界校验
  • 淘汰 strcpy/sprintf,使用带长度限制接口
  • 临界区保护共享多字节变量
  • 善用 map 文件、编译器警告提前发现内存问题
相关推荐
恒锐丰科技林技术员26 分钟前
SS6286L:大电流双向有刷电机驱动芯片,赋能电机控制多元场景
经验分享·嵌入式硬件·硬件工程
意法半导体STM3233 分钟前
【官方原创】LAT1695转换STM32CubeIDE工程到STM32CubeIDE for Visual Studio Code
vscode·stm32·单片机·嵌入式硬件
Zhang.Yi1 小时前
STM32-DMA
stm32·单片机·嵌入式硬件
Jaixln_HRF2 小时前
率能SS8837T 单通道1.8A/12V H桥电机驱动芯片,低功耗睡眠模式120nA,用于摄像机/玩具/机器人
驱动开发·嵌入式硬件·机器人·硬件工程
小鱼儿电子2 小时前
62-基于STM32的多功能水壶控制系统设计
stm32·单片机·嵌入式硬件·多功能水壶·智能水壶
vickycheung32 小时前
基于RV1126B核心板监护摄像机应用解决方案
经验分享·嵌入式硬件·arm 嵌入式开发
工业一体机老司机2 小时前
Python实现工业一体机OPC-UA通信:从SDK选型到数据采集实战
开发语言·python·单片机
深圳市宝华视联2 小时前
移动示教推车功能介绍
网络·嵌入式硬件·音视频·视频编解码·嵌入式实时数据库
泛凡(Linyongui)3 小时前
告别原始编辑器:VS Code 搭建 中微CMS IDE 编译环境完整指南
ide·单片机·编辑器