应用笔记:在裸机系统中使用编译器栈保护机制

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-allCanary 检查__stack_chk_fail()通用方案,支持裸机与 RTOS
GHS--stack_checkSP 边界检测_StackOverflowed()精确检测,无需 Canary
IAR--stack_guardSP 边界+Canary__stack_chk_fail()自动分析栈深度
Keil ARMCC5--check_stack栈边界检测_sys_exit()传统机制,依赖启动符号
Keil ARMCLANG-fstack-protector-strongCanary 检查__stack_chk_fail()与 GCC 兼容
TASKING--stack-checkSP 范围检查_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.c
void _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_Limit

ARMCLANG:

armclang -fstack-protector-strong main.c
void __stack_chk_fail(void)
{
    while (1);
}

3.5 TASKING

ctc --stack-check main.c
void _stack_overflow_handler(void)
{
    while (1);
}

4. 栈保护验证步骤

  1. 开启编译选项
  2. 使用 objdump 检查 __stack_chk_fail 是否插入
  3. 构造溢出函数
  4. 在调试器中观察异常触发
  5. 在异常函数中执行复位或报警动作

增加对应的Link段

在 ARM 平台上,当你使用 C++ 异常(try/catch/throw) 或者启用了某些编译选项(例如 -funwind-tables-fexceptions)时,编译器会自动生成两个关键的表:

  • .ARM.exidxException Index Table(异常索引表)

    
    用于快速查找对应函数的异常信息(每个函数一条记录)。
    
  • .ARM.extabException 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 = .;
    } > TEXT

5. 适用场景与建议

场景启用建议
Bootloader / Flash 驱动✅ 推荐
RTOS 内核任务✅ 推荐
通信协议栈 / 安全功能✅ 推荐
普通业务逻辑⚠️ 视资源而定
资源极限系统(32KB Flash)❌ 可仅测试时启用

6. 优势与局限性

优势

  • 可检测栈溢出;
  • 无需外部硬件;
  • 提升系统稳定性。

局限

  • 仅检测栈区,并且只是对连续越栈的检测比较有效,如果直接跳到栈内进行修改,则该方法无法正常检测。
  • 增加少量开销,会增加部分函数压栈的操作,如果增加-fstack-protector-all参数则所有函数都会额外压栈,并在出栈阶段进行检查。
  • 优化可能影响检测,对于高等级优化,因为涉及到展开以及各种高级特性的函数调用优化,所以这种方式检测可能并不准确。

7. 结论

Stack Protector 是一种轻量、安全的防护机制,可在裸机或 RTOS 环境中检测栈破坏。

通过合理启用可显著提高固件安全性与健壮性,特别适合车规和工业控制类应用。


附录:常见问题 FAQ

问题原因解决方案
重定义 __stack_chk_guardC库已定义删除自定义
无触发效果溢出方向错误改为低地址覆盖
优化级别高失效被内联优化使用 -fno-inline
程序直接复位库中 abort() 默认行为自定义异常函数
执行开销大全局启用仅关键模块使用


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

发表新评论