Posted in

gcc 我一直没发现的一种很“夯”的内存调试方法

今天在调试一个内存问题的时候,偶尔看到一个内存调试方式——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 的方法

  1. -fstack-protector-strong 可用
  2. 必须手写 __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 翻译过来就是“该吃吃”吧~

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注