今天在调试一个内存问题的时候,偶尔看到一个内存调试方式——Sanitizer,
并且很惊讶它自 gcc4.8 版本就一直就放在 gcc 工具里,只是一直我蹲在知识囚笼里,
Sanitizer 是由 Google 发起的开源工具集(见 https://github.com/google/sanitizers/wiki/),用于检测内存泄露等问题。
它包括了AddressSanitizer、MemorySanitizer、ThreadSanitizer、LeakSanitizer 等多种工具,
这些工具最初是LLVM项目的一部分,后来也被GNU的GCC编译器支持,
从 gcc4.8版本开始,就已经支持 AddressSanitizer 和 ThreadSanitizer,而 gcc4.9 版本开始支持 LeakSanitizer。
直接写了一个缺陷代码
void test() {
int a[6];
int a2 = a[6];
}
void main() {
test();
}
使用 gcc 编译,需要带上 -fsanitize=address -g 编译
gcc overflow.c -fsanitize=address -g -o a.out
运行结果的“夯”爆感彻底震惊了我(注意:夯有双引号,后面会说明原因)
=================================================================
==8295==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffdeeef3e68 at pc 0x55a772016291 bp 0x7ffdeeef3e10 sp 0x7ffdeeef3e00
READ of size 4 at 0x7ffdeeef3e68 thread T0
#0 0x55a772016290 in test /home/frank/DataDisk/workspace/doraemon/overflow.c:5
#1 0x55a772016311 in main /home/frank/DataDisk/workspace/doraemon/overflow.c:10
#2 0x7fb772cf8d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x7fb772cf8e3f in __libc_start_main_impl ../csu/libc-start.c:392
#4 0x55a772016104 in _start (/home/frank/DataDisk/workspace/doraemon/a.out+0x1104)
Address 0x7ffdeeef3e68 is located in stack of thread T0 at offset 56 in frame
#0 0x55a7720161d8 in test /home/frank/DataDisk/workspace/doraemon/overflow.c:3
This frame has 1 object(s):
[32, 56) 'a' (line 4) <== Memory access at offset 56 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/frank/DataDisk/workspace/doraemon/overflow.c:5 in test
Shadow bytes around the buggy address:
0x10003ddd6770: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd6780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd6790: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd67a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd67b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10003ddd67c0: 00 00 00 00 00 00 f1 f1 f1 f1 00 00 00[f3]f3 f3
0x10003ddd67d0: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd67e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd67f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd6800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10003ddd6810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==8295==ABORTING
之后,我又写了若干的测试用例,测试了一下堆溢出、线程栈溢出等让我头痛不少个夜晚的问题,
命中准确率都非常的高!
这一刻我感觉听到了 BGM,
带着这个强得可怕的经验,我马上把这套方案尝试移到嵌入式 Linux 设备上尝试运行,
但是似乎我震惊的太早了……
我尝试了 ANYKA,INGENIC,XMSILICON 身边这几个平台,
gcc 工具链版本均大于 4.9,但是编译时均报错
[1]$ /opt/arm-xmv5-linux/bin/arm-xmv5-linux-gcc overflow.c -fsanitize=address -g -o a.out
/opt/arm-xmv5-linux/bin/../lib/gcc/arm-xmv5-linux-uclibcgnueabi/6.4.0/../../../../arm-xmv5-linux-uclibcgnueabi/bin/ld: cannot find libasan_preinit.o: No such file or directory
/opt/arm-xmv5-linux/bin/../lib/gcc/arm-xmv5-linux-uclibcgnueabi/6.4.0/../../../../arm-xmv5-linux-uclibcgnueabi/bin/ld: cannot find -lasan
collect2: error: ld returned 1 exit status
把这些异常信息交给 AI,结果有点失望……
首先,目前为了节省空间,嵌入式 Linux 上使用的均是 uClibc,它的 ABI 不完整;
其次,ASan 对 ARM+uClibc 的支持极差;
最后,工具链编译时,已经把 asan 移除了(使用了–disable-libsanitizer,大概他可能知道体验不好)。
但是这次发现并非完全没有收获。
1. 我通过 AI 去分析这次问题,它虽然给我泼了一盆冷水,但是也给出了一个建议,可以通过打开以下编译选项检查栈数组长度
-Wall -Wextra -Wshadow -Warray-bounds
2. 还有一个运行时 -fstack-protector-strong 去做一些栈溢出的分析;
3. 嵌入式 Linux Gcc 一个最大的优势就是可移植,我可以在某些可移植的怀疑部分,通过移植代码,在 PC 上使用完整的 gcc 通过以上方法验证;
另外,在最近一次与友商平台对接时,遇到 __stack_chk_fail() 接口找不到的问题,
这次也从 AI 老师中学习到通过手动平替 -fstack-protector-strong 的方法
-fstack-protector-strong可用- 必须手写
__stack_chk_fail(),不要指望 uClibc 默认实现。
/*
* stack_protector.c
* 用于 uClibc / 嵌入式 Linux 的栈保护兜底实现
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/* GCC 要求的全局符号 */
void __stack_chk_fail(void) __attribute__((noreturn));
void __stack_chk_fail_local(void); /* 某些旧 GCC 会引用 */
unsigned long __stack_chk_guard = (unsigned long)0xdeadbeef;
void __stack_chk_fail(void)
{
/*
* 嵌入式环境:打印 + 立即终止
* 不用 printf 也可以,这里用 write 更安全
*/
const char msg[] =
"[StackProtector] Stack buffer overflow detected!\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
/*
* 触发崩溃,方便 coredump / 调试器定位
* abort() 在 uClibc 中可能太重
*/
*(volatile int *)0 = 0;
while (1)
;
}
/*
* GCC 6.x 有时会生成这个符号
*/
void __stack_chk_fail_local(void)
{
__stack_chk_fail();
}
#include <string.h>
void foo(void) {
char buf[8];
memset(buf, 'A', 32); // 明显栈溢出
}
int main(void) {
foo();
return 0;
}
运行的结果
[StackProtector] Stack buffer overflow detected!
Segmentation fault
关于 -fstack-protector 几种选项差异
| 选项 | 特点 |
| -fstack-protector | 只对“有数组”的函数生效 |
| -fstack-protector-strong | 对“有数组 / 取址 / 指针操作”生效 👉 嵌入式 Linux 的最佳平衡点 |
| -fstack-protector-all | 所有函数都插桩 |
Anyway,GCC 翻译过来就是“该吃吃”吧~
