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

目录

前言

[一、嵌入式 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 文件、编译器警告提前发现内存问题
相关推荐
xiangw@GZ16 小时前
Type‑C USB DRD 电路
单片机·嵌入式硬件
常州晟凯电子科技16 小时前
sigmaStar SSC305DE/SSC308开发笔记之开发环境搭建和SDK编译
人工智能·笔记·嵌入式硬件·物联网·性能优化·视频编解码
沐欣工作室_lvyiyi16 小时前
基于单片机的跑步机控制系统设计(论文+源码)
c语言·单片机·嵌入式硬件·课程设计·keil·跑步机设计
黑妹天下第一乖17 小时前
第 11 讲:阿加犀 AidLux 多组件综合实战——端侧“智能摄像头解说“全链路
人工智能·嵌入式硬件·语言模型·自然语言处理·iot
py有趣17 小时前
硬件SPI和软件模拟SPI在配置方面的一点区别
c语言·stm32·单片机
AUTO_1507567596517 小时前
【KT70022】昆腾微 单片音频编解码器
单片机·音频
LCG元18 小时前
STM32F103 GPIO 翻转方式耗时实测:CRL/CRH 逐位拆解、BSRR 原子写与 ODR 读改写竞态陷阱
stm32·单片机·嵌入式硬件
搁浅小泽18 小时前
压敏电阻(MOV)选型与设计规范
单片机·嵌入式硬件
一条破秋裤18 小时前
08_GPIO点灯_从J41插针查到复用与寄存器
单片机·嵌入式硬件
SuperByteMaster1 天前
allegro 布线规则管理器
嵌入式硬件