应用笔记:在裸机系统中使用编译器栈保护机制
1. 概述
在嵌入式裸机系统中,函数栈溢出(Stack Overflow) 是一种常见但隐蔽的异常。
现代编译器提供 栈保护机制(Stack Protector),在函数栈中插入特殊检测值(Canary)或边界检查逻辑,以在函数返回前检测栈破坏。
本文介绍在多种编译器下启用与验证该机制,重点展示 Cortex-M0 裸机环境下的使用。
2. 栈保护原理
编译器在函数入口插入 Canary 值,返回时检查是否被修改:
高地址
| 局部变量 (buffer[16]) |
| Canary 值             |
| 返回地址 (LR)         |
低地址- 若 Canary 被修改 → 调用 __stack_chk_fail()
 - 若未修改 → 函数正常返回
 
3. 各编译器实现方式
| 编译器 | 启用参数 | 检测方式 | 异常回调 | 特点 | 
|---|---|---|---|---|
| GCC / ARMCLANG | -fstack-protector-strong / -fstack-protector-all | Canary 检查 | __stack_chk_fail() | 通用方案,支持裸机与 RTOS | 
| GHS | --stack_check | SP 边界检测 | _StackOverflowed() | 精确检测,无需 Canary | 
| IAR | --stack_guard | SP 边界+Canary | __stack_chk_fail() | 自动分析栈深度 | 
| Keil ARMCC5 | --check_stack | 栈边界检测 | _sys_exit() | 传统机制,依赖启动符号 | 
| Keil ARMCLANG | -fstack-protector-strong | Canary 检查 | __stack_chk_fail() | 与 GCC 兼容 | 
| TASKING | --stack-check | SP 范围检查 | _stack_overflow_handler() | 车规平台常用 | 
3.1 GCC
-fstack-protector-strong -fstack-usage -Wstack-protector -fstack-protector-all这些选项都是 GCC/Clang 编译器 中用于 栈保护(Stack Protector) 和 栈使用分析(Stack Usage Analysis) 的编译参数。
它们的目的都是为了提高程序的安全性和可分析性,防止栈溢出攻击或分析函数栈空间使用。
下面我详细解释每一个选项的作用和区别👇
🧱 fstack-protector
基础栈保护机制
在函数中如果检测到存在“可能被溢出的局部变量”(例如包含 char buf[32] 的数组),编译器会在函数栈帧中插入一个“canary(金丝雀)值”。
函数返回前会检查这个值是否被破坏,如果被修改,则调用 __stack_chk_fail() 中止程序执行。
🔹保护范围:
- 仅保护有“危险局部变量”的函数(例如局部数组或 alloca())
 - 对普通函数(如只用整型局部变量的函数)不加保护
 
🔒 fstack-protector-strong
增强版栈保护(推荐实际使用)
这是 GCC 4.9 引入的增强版本,保护范围更广。
🔹保护范围包括:
- 含有数组(如 char buf[64])的函数
 - 含有 alloca() 调用的函数
 - 含有引用参数的函数(例如 void foo(int &x))
 - 含有局部结构体(包含数组成员)的函数
 
⚙️简单来说,它比 -fstack-protector 检测更智能、更全面,但不会像 -fstack-protector-all 那样对每个函数都加保护。
✅ 推荐用于一般系统或嵌入式场景的“平衡安全策略”。
🧨 fstack-protector-all
全函数栈保护
强制对所有函数都启用栈保护,即使函数中没有局部数组或其他危险对象。
🔹优点:
- 最大化安全性
 - 可防御潜在但未显式出现的溢出风险
 
🔹缺点:
- 编译生成代码变大
 - 执行效率略有下降(每个函数都要插入 canary 检查)
 
⚙️常见于安全敏感系统(例如操作系统内核或关键任务固件),一般项目不建议默认开启。
📊 4.fstack-usage
输出每个函数的栈使用信息
在启用此选项后,编译器会为每个源文件生成一个对应的 .su 文件,其中记录了:
- 函数名
 - 每个函数的栈空间大小(单位:字节)
 - 栈是否为静态或动态分配
 
📁 例如,编译 main.c 时会生成 main.su:
app/main.c:61:6:trigger_stack_overflow    32    static
app/main.c:75:6:Test_StackProtector    16    static
app/main.c:88:5:main    16    static这个文件非常适合用于:
- 静态分析函数的栈消耗
 - 确认嵌入式系统的栈深度是否在安全范围内
 - 与 -Wstack-usage 联合使用生成警告
 
⚠️ Wstack-protector
对栈保护行为发出警告
当编译器检测到某些函数未被栈保护覆盖时,会给出警告。
常用于调试阶段,以确认编译器实际为哪些函数插入了保护。
📘例如:
warning: stack protector not protecting function: no local buffers🧩 综合使用建议
| 场景 | 推荐选项 | 说明 | 
|---|---|---|
| 普通嵌入式项目 | -fstack-protector-strong | 安全与性能平衡良好 | 
| 高安全需求(如 Bootloader、加密模块) | -fstack-protector-all | 强制所有函数保护 | 
| 分析栈使用 | -fstack-usage | 生成 .su 文件统计 | 
| 检查保护范围 | -Wstack-protector | 编译时发出提示 | 
🧠 延伸说明:工作机制简图
函数栈帧布局:
+------------------+
| 返回地址         |
| 局部变量         |
| Canary 值 (随机) |
| 调用者保存寄存器 |
+------------------+
执行流程:
1️⃣ 函数入口:写入 canary
2️⃣ 函数运行期间:若溢出破坏 canary
3️⃣ 函数返回前:检查 canary
4️⃣ 若不匹配 → 调用 __stack_chk_fail()使用示例:
/* Note: __stack_chk_fail defined libc.a, if not use default library
 * define __stack_chk_fail function. 
 */
void __stack_chk_fail(void)
{
    PRINTF("!!! Stack corruption detected !!!\n");
    while (1) {
        __asm("BKPT #0");
    }
}
void trigger_stack_overflow(void)
{
    volatile char buffer[16];
    PRINTF("Writing beyond buffer...\n");
    // 故意越界写,破坏栈上的 canary
    for (int i = 0; i < 64; i++) {
        buffer[i] = buffer[i] + (char)i;
    }
    PRINTF("Buffer overflow finished.\n");
}
void Test_StackProtector(void)
{
    PRINTF("Start stack protector test...\n");
    trigger_stack_overflow();
    PRINTF("End of test (should never reach here)\n");
}生成的汇编代码:
Test_StackProtector
$Thumb
{
 00000E20   B500        PUSH           {LR}
 00000E22   B083        SUB            SP, SP, #12
 00000E24   4B0B        LDR            R3, =__stack_chk_guard        ; [PC, #44] [0x00000E54] =0x20000770 
 00000E26   681B        LDR            R3, [R3]
 00000E28   9301        STR            R3, [SP, #4]
 00000E2A   2300        MOVS           R3, #0
PRINTF("Start stack protector test...\n");
 00000E2C   480A        LDR            R0, =0x000000F8               ; [PC, #40] [0x00000E58] 
 00000E2E   F001 F961   BL             PRINTF                        ; 0x000020F4
trigger_stack_overflow();
 00000E32   F7FF FFCF   BL             trigger_stack_overflow        ; 0x00000DD4
PRINTF("End of test (should never reach here)\n");
 00000E36   4809        LDR            R0, =0x00000118               ; [PC, #36] [0x00000E5C] 
 00000E38   F001 F95C   BL             PRINTF                        ; 0x000020F4
 00000E3C   4B05        LDR            R3, =__stack_chk_guard        ; [PC, #20] [0x00000E54] =0x20000770 
 00000E3E   681A        LDR            R2, [R3]
 00000E40   9B01        LDR            R3, [SP, #4]
 00000E42   405A        EORS           R2, R3
 00000E44   2300        MOVS           R3, #0
 00000E46   2A00        CMP            R2, #0
 00000E48   D101        BNE            0x00000E4E                    ; <Test_StackProtector>+0x2E
 00000E4A   B003        ADD            SP, SP, #12
 00000E4C   BD00        POP            {PC}
 00000E4E   F005 FC87   BL             __stack_chk_fail              ; 0x00006760添加例外
在 -fstack-protector-all(或全局开启栈保护)的编译下,有些低级初始化函数(比如 RAM init、early boot、在修改 SP 的函数)必须手工管理栈帧,这类函数被插入 canary 检查后会产生不正确的行为。解决办法就是对这些特定函数单独关闭栈保护:
void RamInit1() __attribute__((optimize("no-stack-protector")));
/**
 * @brief RamInit1 for copying initialized data and zeroing uninitialized data
 */
void RamInit1(){
    ....
}3.2 GHS
ccarm --stack_check main.cvoid _StackOverflowed(void)
{
    while (1); // 用户自定义异常处理
}需在链接脚本定义 _stack_base 与 _stack_end。
3.3 IAR
在 IDE 中启用:
Project → Options → C/C++ Compiler → Code → Stack Protection
或命令行方式:
iccarm --stack_guard main.c异常处理:
void __stack_chk_fail(void)
{
    while (1);
}3.4 Keil
ARMCC5:
armcc --check_stack main.c需定义:
Stack_Mem SPACE Stack_Size
Stack_LimitARMCLANG:
armclang -fstack-protector-strong main.cvoid __stack_chk_fail(void)
{
    while (1);
}3.5 TASKING
ctc --stack-check main.cvoid _stack_overflow_handler(void)
{
    while (1);
}4. 栈保护验证步骤
- 开启编译选项
 - 使用 objdump 检查 __stack_chk_fail 是否插入
 - 构造溢出函数
 - 在调试器中观察异常触发
 - 在异常函数中执行复位或报警动作
 
增加对应的Link段
在 ARM 平台上,当你使用 C++ 异常(try/catch/throw) 或者启用了某些编译选项(例如 -funwind-tables 或 -fexceptions)时,编译器会自动生成两个关键的表:
.ARM.exidx:Exception Index Table(异常索引表)用于快速查找对应函数的异常信息(每个函数一条记录)。.ARM.extab:Exception Table(异常表)保存异常处理所需的详细信息(比如栈展开指令、异常范围等)。
这些表由编译器生成,但需要在 链接阶段 正确放入镜像中、并在运行时让异常处理库(如 libgcc)能够访问它们。
正因为我们要用到栈的检查功能,所以上面两个段的定义是必须的,在GCC中可以通过如下的形式进行定义:
    .ARM  : {
        
        ARM_start = .;
        ARM.exidx_region_start = .;
        __exidx_start = .; 
        *(.ARM.exidx)
        *(.ARM.exidx*)
        
        /* Add ARM exception handling tables */
        *(.ARM.extab)
        *(.ARM.extab*)
        
        ARM.exidx_region_end = .;
         __exidx_end = .; 
        ARM_end = .;
    } > TEXT5. 适用场景与建议
| 场景 | 启用建议 | 
|---|---|
| Bootloader / Flash 驱动 | ✅ 推荐 | 
| RTOS 内核任务 | ✅ 推荐 | 
| 通信协议栈 / 安全功能 | ✅ 推荐 | 
| 普通业务逻辑 | ⚠️ 视资源而定 | 
| 资源极限系统(32KB Flash) | ❌ 可仅测试时启用 | 
6. 优势与局限性
优势
- 可检测栈溢出;
 - 无需外部硬件;
 - 提升系统稳定性。
 
局限
- 仅检测栈区,并且只是对连续越栈的检测比较有效,如果直接跳到栈内进行修改,则该方法无法正常检测。
 - 增加少量开销,会增加部分函数压栈的操作,如果增加
-fstack-protector-all参数则所有函数都会额外压栈,并在出栈阶段进行检查。 - 优化可能影响检测,对于高等级优化,因为涉及到展开以及各种高级特性的函数调用优化,所以这种方式检测可能并不准确。
 
7. 结论
Stack Protector 是一种轻量、安全的防护机制,可在裸机或 RTOS 环境中检测栈破坏。
通过合理启用可显著提高固件安全性与健壮性,特别适合车规和工业控制类应用。
附录:常见问题 FAQ
| 问题 | 原因 | 解决方案 | 
|---|---|---|
| 重定义 __stack_chk_guard | C库已定义 | 删除自定义 | 
| 无触发效果 | 溢出方向错误 | 改为低地址覆盖 | 
| 优化级别高失效 | 被内联优化 | 使用 -fno-inline | 
| 程序直接复位 | 库中 abort() 默认行为 | 自定义异常函数 | 
| 执行开销大 | 全局启用 | 仅关键模块使用 | 
最后更新于 2025-10-30 07:15:00 并被添加「」标签,已有 102 位童鞋阅读过。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。