Linux下内存检测工具:asan

1.GCC 版本 6.3

2.安装asan

  yum install devtoolset-6-libasan-devel

3.注意

  asan只是开发中使用工具,因此只能在debug模式下有效 不能用于release版本

介绍

首先,先介绍一下 Sanitizer 项目,该项目是谷歌出品的一个开源项目,该项目包含了 ASANLSANMSANTSAN等内存、线程错误的检测工具,这里简单介绍一下这几个工具的作用:

  • ASAN: 内存错误检测工具,在编译命令中添加-fsanitize=address启用

  • LSAN: 内存泄漏检测工具,已经集成到 ASAN 中,可以通过设置环境变量ASAN_OPTIONS=detect_leaks=0来关闭ASAN上的LSAN,也可以使用-fsanitize=leak编译选项代替-fsanitize=address来关闭ASAN的内存错误检测,只开启内存泄漏检查。

  • MSAN: 对程序中未初始化内存读取的检测工具,可以在编译命令中添加-fsanitize=memory -fPIE -pie启用,还可以添加-fsanitize-memory-track-origins选项来追溯到创建内存的位置

  • TSAN: 对线程间数据竞争的检测工具,在编译命令中添加-fsanitize=thread启用 其中ASAN就是我们今天要介绍的重头戏。

ASAN,全称 AddressSanitizer,可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。

根据谷歌的工程师介绍 ASAN 已经在 chromium 项目上检测出了300多个潜在的未知bug,而且在使用 ASAN 作为内存错误检测工具对程序性能损耗也是及其可观的。

根据检测结果显示可能导致性能降低2倍左右,比Valgrind(官方给的数据大概是降低10-50倍)快了一个数量级。

而且相比于Valgrind只能检查到堆内存的越界访问和悬空指针的访问,ASAN 不仅可以检测到堆内存的越界和悬空指针的访问,还能检测到栈和全局对象的越界访问。

这也是 ASAN 在众多内存检测工具的比较上出类拔萃的重要原因,基本上现在 C/C++ 项目都会使用ASAN来保证产品质量,尤其是大项目中更为需要。

 

1、编译选项
1.1 Gcc编译选项

# -fsanitize=address:开启内存越界检测

# -fsanitize-recover=address:一般后台程序为保证稳定性,不能遇到错误就简单退出,而是继续运行,采用该选项支持内存出错之后程序继续运行,需要叠加设置ASAN_OPTIONS=halt_on_error=0才会生效;若未设置此选项,则内存出错即报错退出

ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address

 

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS } -fsanitize=address -fsanitize-recover=address")

1.2 Ld链接选项

ASAN_LDFLAGS += -fsanitize=address -g1

如果使用gcc链接,此处可忽略。

 

2、ASAN运行选项
2.1 ASAN_OPTIONS设置

ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。

# halt_on_error=0:检测内存错误后继续运行

# detect_leaks=1:使能内存泄露检测

# malloc_context_size=15:内存错误发生时,显示的调用栈层数为15

# log_path=/home/xos/asan.log:内存检查问题日志存放文件路径

# suppressions=$SUPP_FILE:屏蔽打印某些内存错误

export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=/home/xos/asan.log:suppressions=$SUPP_FILE

除了上述常用选项,以下还有一些选项可根据实际需要添加:

# detect_stack_use_after_return=1:检查访问指向已被释放的栈空间

# handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号

# quarantine_size=4194304:内存cache可缓存free内存大小4M

ASAN_OPTIONS=${ASAN_OPTIONS}:verbosity=0:handle_segv=1:allow_user_segv_handler=1:detect_stack_use_after_return=1:fast_unwind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1:quarantine_size=4194304

2.2 LSAN_OPTIONS设置
LSAN_OPTIONS是LeakSanitizier运行选项的环境变量,而LeakSanitizier是ASAN的内存泄漏检测模块,常用运行选项有:

# exitcode=0:设置内存泄露退出码为0,默认情况内存泄露退出码0x16

# use_unaligned=4:4字节对齐

export LSAN_OPTIONS=exitcode=0:use_unaligned=4

 

 

 

ASAN 的基本原理

ASAN的内存检测方法与ValgrindAddrCheck工具很像,都是使用shadow内存来记录应用程序的每个字节是否可以被安全的访问,在访问内存时都对其映射的shadow内存进行检查。

但是,ASAN使用一个更具效率的shadow内存映射机制和更加紧凑的内存编码来实现,并且除了堆内存外还能检测栈和全局对象中的错误访问,且比AddrCheck快一个数量级。

ASAN由两部分组成:代码插桩模块和运行时库。

  • 代码插桩模块会修改代码使其在访问内存时检查每块内存访问状态,称为shadow 状态,以及在内存两侧创建redzone的内存区域。

  • 运行时库则提供一组接口用来替代mallocfree以及相关的函数,使得在分配堆空间时在其周围创建redzone,并在内存出错时报告错误。

首先,我们先介绍一下什么是shadow 内存 和 redzone

  • shadow 内存

    ASANmalloc函数返回的内存地址通常至少是8个字节对齐,比如malloc(15)将分配得到2块大小为8字节的内存,在这个场景中,第二块8字节内存的前5个字节是可以访问,但剩下的3个字节是不能访问的。

    所谓的shadow 内存就是在应用程序的虚拟地址空间中预留一段地址空间,用来存储映射应用程序访问的内存块中哪些字节可以被使用的信息,这些信息就是shadow 状态。其中每1个字节的shadow 内存,映射到8个字节的应用程序内存,因此,shadow状态可能有3种:

    ASAN使用带有比例和偏移量的直接映射将应用程序地址转换为其对应的shadow内存地址:

    shadow_address = (addr >> 3) + offset

    假设max - 1是虚拟地址空间中的最大有效地址,则offset的值应选择为在启动时不被占用的从offsetoffset+Max/8的区域。

    以下是 32 位 linux 系统中的地址空间分布

    1.  
      0x1 0000 0000 ---------------
    2.  
                    |   HIGH      |
    3.  
                    |   MEMORY    |
    4.  
        0x4000 0000 ---------------
    5.  
                    | HIGH SHADOW |
    6.  
        0x2800 0000 ---------------
    7.  
                    | BAD REGION  |
    8.  
        0x2400 0000 ---------------
    9.  
                    | LOW SHADOW  |
    10.  
        0x2000 0000 ---------------
    11.  
                    | LOW MEMORY  |
    12.  
        0x0000 0000 ---------------

    虚拟地址空间被划分为高低两部分,每个部分的内存地址映射到相应的shadow 内存。注意:将shadow 内存中的地址进行映射会得到Bad 区域中的地址,Bad 区域是被页面保护标记为不可访问的地址空间。

    shadow映射方式可以推导为(addr >> scale) + offset的形式,其中scale是的取值范围是1~7,当 scale=N时,shadow 内存占用虚拟地址空间的1/2^Nred-zone的最小大小为2^N字节(保证malloc()的对齐要求)。shadow 内存中的每个字节描述了2^N个内存字节的状态并有2^N + 1个不同的值。

    • 在 32 位 linux 系统中,虚拟地址空间为:0x00000000-0xffffffffoffset = 0x20000000(2^29)

    • 在 64 位系统中,ofsset = 0x0000100000000000(2^44)

    • 在某些情况下(例如,在 Linux 上使用 -fPIE/-pie 编译器标志)可以使用零偏移来进一步简化检测。

  1. 0: 表示映射的8个字节均可以使用

  2. k(1<=k<=7): 表示表示映射的8个字节中只有前k个字节可以使用

  3. 负值: 表示映射的8个字节均不可使用,且不同的值表示所映射不同的内存类型(堆、栈、全局对象或已释放内存)

redzone

ASAN会在应用程序使用的堆、栈、全局对象的内存周围分配额外内存,这个额外的内存叫做redzoneredzone会被shadow 内存标记为不可使用状态,当应用程序访问redzone内存时说明已经溢出访问了,此时,ASAN检测redzoneshadow 状态后就会报告相应错误。readzone越大,检测内存下溢和上溢的范围越大。具体的分配策略将在下面涉及。

代码插桩

ASAN 会在应用程序访问内存的位置进行插桩,对于访问完整8字节内存的位置,插入以下代码检查内存对应的 shadow 内存,以此判断是否访问异常:

  1.  
    ShadowAddr = (Addr >> 3) + Offset;
  2.  
     
  3.  
    if (*ShadowAddr != 0)
  4.  
      ReportAndCrash(Addr);

由于应用程序访问8字节的内存,因此,其映射的shadow 内存的存储值必须是0,表示该8字节内存完全可用,否则,报错。

应用程序对 1、2、或者 4 字节内存的访问要复杂一些,如果访问的内存块对应的shadow 内存的存储值如果不是负数,且不为0,或者将要访问内存块超过了shadow 内存表示的可用范围,意味着本次将访问到不可使用的内存:

  1.  
    ShadowAddr = (Addr >> 3) + Offset;
  2.  
    k = *ShadowAddr;
  3.  
    if (k != 0 && ((Addr & 7) + AccessSize > k))
  4.  
      ReportAndCrash(Addr);

需要注意的是,ASAN对源代码的插桩时机是在LLVM对代码编译优化之后,也就意味着ASAN只能检测 LLVM 优化后幸存下来的内存访问,例如:被 LLVM 优化掉的对栈对象进行访问的代码将不会被ASAN所识别。

同时,ASAN也不会对 LLVM 生成的内存访问代码进行插桩,例如:寄存器溢出检查等等。

另外,即使错误报告代码ReportAndCrash(Addr)只会被调用一次,但由于会在代码中的许多位置进行插入,因此,错误报告代码也必须相当紧凑。

目前 ASAN 使用了一个简单的函数调用来处理错误报告,当然还有另一个选择是插入一个硬件异常。

运行时库

在应用程序启动时,将映射整个shadow 内存,因此程序的其他部分不能使用它。BAD 区域也是受保护的,应用程序也不能访问。

在 linux 操作系统中,shadow 内存区域不会被占用,因此,映射总是成功的。但在 MacOS 中可能需要禁用地址空间布局(ASLR)。

另外,根据 GOOGLE 工程师介绍,shadow 内存区域的布局也适用于 windows 操作系统。

启用 ASAN 时,源代码中的 malloc 和 free 函数将会被替换为运行时库中的 malloc 和 free 函数。

malloc 分配的内存区域被组织为为一个与对象大小相对应的空闲列表数组。当对应于所请求内存大小的空闲列表为空时,从操作系统(例如,使用mmap)分配带有redzone的内存区域。n个内存块,将分配n+1redzone

| redzone-1 | memory-1 | redzone-2 | memory-2 | redzone-3 |

free 函数会将整个内存区域置成不可使用并将其放入隔离区,这样该区域就不会马上被 malloc 分配给应用程序。

目前,隔离区是使用一个 FIFO 队列实现的,它在任何时候都拥有一定数量的内存。

默认情况下,malloc 和 free 记录当前调用堆栈,以便提供更多信息的错误报告。malloc 调用堆栈存储在左侧 redzone 中(redzone 越大,可以存储的帧数越多),而 free 调用堆栈存储在内存区域本身的开头。

到这里你应该已经明白了对于动态分配的内存,ASAN是怎么实现检测的,但你可能会产生疑惑:动态分配是通过 malloc 函数分配redzone来支持错误检测,那栈对象和全局对象这类没有malloc分类内存的对象是怎么实现的呢?其实原理也很简单:

  • 对于全局变量,redzone 在编译时创建,redzone 的地址在应用程序启动时传递给运行时库。运行时库函数会将redzone 设置为不可使用并记录地址以供进一步错误报告。

    • 对于栈对象,redzone 是在运行时创建和置为不可使用。目前,使用32字节的 redzone。例如以下代码片段:

      1.  
        void foo() {
      2.  
          char a[10];
      3.  
          <function body> 
      4.  
        }
      5.  
      ASAN 处理后的代码大致如下:
      1.  
        void foo() {
      2.  
          char rz1[32]
      3.  
          char arr[10];
      4.  
          char rz2[32-10+32];
      5.  
         
      6.  
          unsigned * shadow = (unsigned*)(((long)rz1>>8)+Offset);
      7.  
         
      8.  
          // 将 redzone 设置为不可使用
      9.  
          shadow[0] = 0xffffffff; // rz1
      10.  
          shadow[1] = 0xffff0200; // arr and rz2
      11.  
          shadow[2] = 0xffffffff; // rz2
      12.  
         
      13.  
          <function body>
      14.  
         
      15.  
          // 将所有内存设置成可以使用
      16.  
          shadow[0] = shadow[1] = shadow[2] = 0; 
      17.  
        }

        总结

        ASAN 使用shadow 内存redzone来提供准确和即时的错误检测。

        传统观点认为,shadow 内存redzone要么通过多级映射方案产生高开销,要么占用大量的程序内存。但,ASAN的使用的shadow映射机制和shadow 状态编码减少了对内存空间占用。

        最后,如果你觉得ASAN插桩代码和检测的对你某些的代码来说太慢了,那么可以使用编译器标志来禁用特定函数的,使ASAN跳过对代码中某个函数的插桩和检测, 跳过分析函数的编译器指令是:

        __attribute__((no_sanitize_address))
posted @ 2023-03-09 17:29  angryCoder996  阅读(2443)  评论(1编辑  收藏  举报