应用笔记:在裸机系统中使用编译器栈保护机制
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 并被添加「」标签,已有 100 位童鞋阅读过。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。