應用 AddressSanitizer 發現程序內存錯誤

應用 AddressSanitizer 發現程序內存錯誤

做爲 C/ C++ 工程師,在開發過程當中會遇到各種問題,最多見即是內存使用問題,好比,越界泄漏。過去經常使用的工具是 Valgrind,但使用 Valgrind 最大問題是它會極大地下降程序運行的速度,初步估計會下降 10 倍運行速度。而 Google 開發的 AddressSanitizer 這個工具很好地解決了 Valgrind 帶來性能損失問題,它很是快,只拖慢程序 2 倍速度。html

AddressSanitizer 概述

AddressSanitizer 是一個基於編譯器的測試工具,可在運行時檢測 C/C++ 代碼中的多種內存錯誤。嚴格上來講,AddressSanitizer 是一個編譯器插件,它分爲兩個模塊,一個是編譯器的 instrumentation 模塊,一個是用來替換 malloc/free 的動態庫。ios

Instrumentation 主要是針對在 llvm 編譯器級別對訪問內存的操做(store,load,alloc等),將它們進行處理。動態庫主要提供一些運行時的複雜的功能(好比 poison/unpoison shadow memory)以及將 malloc/free 等系統調用函數 hook 住。git

AddressSanitizer 基本使用

根據 AddressSanitizer Wiki 能夠檢測下面這些內存錯誤github

  • Use after free:訪問堆上已經被釋放的內存
  • Heap buffer overflow:堆上緩衝區訪問溢出
  • Stack buffer overflow:棧上緩衝區訪問溢出
  • Global buffer overflow:全局緩衝區訪問溢出
  • Use after return:訪問棧上已被釋放的內存
  • Use after scope:棧對象使用超過定義範圍
  • Initialization order bugs:初始化命令錯誤
  • Memory leaks:內存泄漏

這裏我只簡單地介紹下基本的使用,詳細的使用文檔能夠看官方的編譯器使用文檔,好比 Clang 的文檔:https://clang.llvm.org/docs/AddressSanitizer.html算法

Use after free 實踐例子

下面這段代碼是一個很簡單的 Use after free 的例子:shell

//use_after_free.cpp
#include <iostream>
int main(int argc, char **argv) {
  int *array = new int[100];
  delete [] array;
  std::cout << array[0] << std::endl;
  return 1;
}

編譯代碼,而且運行,這裏能夠看到只須要在編譯的時候帶上 -fsanitize=address  選項就能夠了。數據庫

clang++  -O -g -fsanitize=address ./use_after_free.cpp
./a.out

最終咱們會看到以下的輸出:bash

==10960==ERROR: AddressSanitizer: heap-use-after-free on address 0x614000000040 at pc 0x00010d471df0 bp 0x7ffee278e6b0 sp 0x7ffee278e6a8
READ of size 4 at 0x614000000040 thread T0
    #0 0x10d471def in main use_after_free.cpp:6
    #1 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc)

0x614000000040 is located 0 bytes inside of 400-byte region [0x614000000040,0x6140000001d0)
freed by thread T0 here:
    #0 0x10d4ccced in wrap__ZdaPv (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x51ced)
    #1 0x10d471ca1 in main use_after_free.cpp:5
    #2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc)

previously allocated by thread T0 here:
    #0 0x10d4cc8dd in wrap__Znam (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x518dd)
    #1 0x10d471c96 in main use_after_free.cpp:4
    #2 0x7fff732c17fc in start (libdyld.dylib:x86_64+0x1a7fc)

SUMMARY: AddressSanitizer: heap-use-after-free use_after_free.cpp:6 in main

能夠看到一目瞭然,很是清楚的告訴了咱們在哪一行內存被釋放,而又在哪一行內存再次被使用。app

還有一個是內存泄漏,好比下面的代碼,顯然 p 所指的內存沒有被釋放。分佈式

void *p;

int main() {
        p = malloc(7);
        p = 0; // The memory is leaked here.
        return 0;
}

編譯而後運行

clang -fsanitize=address -g  ./leak.c
./a.out

能夠看到以下的結果

=================================================================
==17756==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 7 byte(s) in 1 object(s) allocated from:
    #0 0x4ffc80 in malloc (/home/simon.liu/workspace/a.out+0x4ffc80)
    #1 0x534ab8 in main /home/simon.liu/workspace/./leak.c:4:8
    #2 0x7f127c42af42 in __libc_start_main (/usr/lib64/libc.so.6+0x23f42)

SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

不過這裏要注意內存泄漏的檢測只會在程序最後退出以前進行檢測,也就是說若是你在運行時若是不斷地分配內存,而後在退出的時候對內存進行釋放,AddressSanitizer 將不會檢測到內存泄漏,這種時候可能你就須要另外的工具了 JeMalloc / TCMalloc。

AddressSanitizer 基本原理

這裏簡單介紹一下 AddressSanitizer 的實現,更詳細的算法實現能夠看《AddressSanitizer: a fast address sanity checker》:https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf

AddressSanitizer 會替換你的全部 malloc 以及 free,而後已經被分配(malloc)的內存區域的先後會被標記爲 poisoned (主要是爲了處理 overflow 這種狀況),而釋放(free)的內存會被標記爲 poisoned(主要是爲了處理 Use after free)。你的代碼中的每一次的內存存取都會被編譯器作相似下面的翻譯.

before:

*address = ...;  // or: ... = *address;

after:

shadow_address = MemToShadow(address);
if (ShadowIsPoisoned(shadow_address)) {
  ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;  // or: ... = *address;

這裏能夠看到首先會對內存地址有一個翻譯(MemToShadow)的過程,而後再來判斷當所訪問的內存區域是否爲 poisoned,若是是則直接報錯並退出。

這裏之因此會有這個翻譯是由於 AddressSanitizer 將虛擬內存分爲了兩部分:

  1. Main application memory(Mem)也就是被當前程序自身使用的內存
  2. Shadow memory 簡單來講就是保存了主存元信息的一塊內存,好比主存的那些區域被 posioned 都是在 Shadow memory 中保存的

AddressSanitizer 和其餘內存檢測工具對比

下圖是 AddressSanitizer 與其餘的一些內存檢測工具的對比:

http://www.javashuo.com/tag/addresssanitizer Valgrind/Memcheck Dr. Memory Mudflap Guard Page gperftools
technology CTI DBI DBI CTI Library Library
ARCH x86, ARM, PPC x86, ARM, PPC, MIPS, S390X, TILEGX x86 all(?) all(?) all(?)
OS Linux, OS X, Windows, FreeBSD, Android, iOS Simulator Linux, OS X, Solaris, Android Windows, Linux Linux, Mac(?) All (1) Linux, Windows
Slowdown 2x 20x 10x 2x-40x ? ?
Detects:
Heap OOB yes yes yes yes some some
Stack OOB yes no no some no no
Global OOB yes no no ? no no
UAF yes yes yes yes yes yes
UAR yes (see AddressSanitizerUseAfterReturn) no no no no no
UMR no (see MemorySanitizer) yes yes ? no no
Leaks yes (see LeakSanitizer) yes yes ? no yes

參數說明:

  • DBI: dynamic binary instrumentation(動態二進制插樁)
  • CTI: compile-time instrumentation (編譯時插樁)
  • UMR: uninitialized memory reads (讀取未初始化的內存)
  • UAF: use-after-free (aka dangling pointer) (使用釋放後的內存)
  • UAR: use-after-return (使用返回後的值)
  • OOB: out-of-bounds (溢出)
  • x86: includes 32- and 64-bit.

能夠看到相比於 Valgrind,AddressSanitizer 只會拖慢程序 2 倍運行速度。當前 AddressSanitizer 支持 GCC 以及 Clang,其中 GCC 是從 4.8 開始支持,而 Clang 的話是從 3.1 開始支持。

AddressSanitizer 的使用注意事項

  1. AddressSanitizer 在發現內存訪問違規時,應用程序並不會自動崩潰。這是因爲在使用模糊測試工具時,它們一般都是經過檢查返回碼來檢測這種錯誤。固然,咱們也能夠在模糊測試進行以前經過將環境變量 ASAN_OPTIONS 修改爲以下形式來迫使軟件崩潰:
export ASAN_OPTIONS='abort_on_error=1'/
  1. AddressSanitizer 須要至關大的虛擬內存(大約 20 TB),不用擔憂,這個只是虛擬內存,你仍可使用你的應用程序。但像 american fuzzy lop 這樣的模糊測試工具就會對模糊化的軟件使用內存進行限制,不過你仍能夠經過禁用內存限制來解決該問題。惟一須要注意的就是,這會帶來一些風險:測試樣本可能會致使應用程序分配大量的內存進而致使系統不穩定或者其餘應用程序崩潰。所以在進行一些重要的模糊測試時,不要去嘗試在同一個系統上禁用內存限制。

在 Nebula Graph 中開啓 AddressSanitizer

咱們在 Nebula Graph 中也使用了 AddressSanitizer,它幫助咱們發現了很是多的問題。而在 Nebula Graph 中開啓 AddressSanitizer 很簡單,只須要在 Cmake 的時候帶上打開 ENABLE_ASAN 這個 Option 就能夠,好比:

Cmake -DENABLE_ASAN=On

這裏建議全部的開發者在開發完畢功能運行單元測試的時候都打開 AddressSanitizer 來運行單元測試,這樣能夠發現不少不容易發現的內存問題,節省不少調試的時間。

附錄

相關文章
相關標籤/搜索