目錄

廣告 AD

AddressSanitizer: C++ 檢查記憶體工具,解決 Segmentation Fault 錯誤

人生有三寶:吃飯、玩樂、睡飽飽

程式有三種錯誤:語法錯誤 (syntax error)、執行錯誤 (runtime error)、語意錯誤 (semantic error)

今天要來解決的就是執行錯誤中的常見的記憶體相關的錯誤

廣告 AD

前言

常常在寫程式的時候,發生 out of bound 都不知道,只能等到看到 Segmentation Fault 才知道錯誤。

而其中就算知道錯誤了,也找不出來錯誤在哪裡,一般都是在編譯的時候用 -g,然後用 gdb 重新執行一遍,等到出錯後看錯誤訊息來找出錯誤的地方,但我有遇過一次是 gdb 報錯的地方在 malloc 的地方,檢查了很多遍也找不出來錯誤的地方 QAQ,之後是靠著 AddressSanitizer 才找出正確錯誤的地方。


AddressSanitizer

google/sanitizers

今天介紹的 AddressSanitizer 可以在 GCC 中使用 1,根據 github GCC 只要版本 >= 4.8 之後就內建 AddressSanitizer 2,如果今天你是用 Visual Studio 開發的話可以參考 Windows Visual Studio 使用說明,以下我們用 GCC 當作範例。


一共可以檢查到下列這些種類的錯誤:

  • Out of bound: 出界錯誤,包含 heap, stack 和 global。
  • Use-after-free: 釋放後使用錯誤。
  • Use-after-return: 回傳後使用錯誤。
  • Use-after-scope: 超出範圍後使用錯誤。
  • Double-free, invalid free: 重複釋放錯誤,非法釋放錯誤。
  • Memory leaks: 記憶體洩漏。

根據查到的資料顯示下列平台上的編譯器都支援 AddressSanitizer,需要特別注意的是 Windows 的 MinGW 不支援,可以使用 MSVC 取代。

  • Windows - MSVC
  • Windows - Clang
  • Windows - LLVM MinGW (基於 LLVM/Clang/LLD 的 mingw-w64) Github
  • Linux - GCC
  • Linux - Clang

其中 Windows 的 MSVC 在安裝時的時候要記得勾選,可以去檢查看看是否有安裝到。


你只需要在編譯的時候加上 -fsanitize=address,接著在執行的時候,遇到錯誤就會顯示詳細的錯誤資訊,可以參考以下範例。

  • 確保程式的執行效率不要下降太多,建議使用 -O1 或者更高的最佳化。
  • 使用 -g 來顯示詳細的錯誤行數。

bash

-fsanitize=address -O1

如果是要檢查 heap leak 或是 heap overflow,則使用下方的指令

bash

-fsanitize=leak -O1

全部指令就會是這樣:

bash

g++ -fsanitize=address -O1 memory_leak.cpp -o exec.memory_leak

如果是編譯和連結是分開的話,連結的時候加上 -lasan:

bash

g++ -fsanitize=address -O1 -c memory_leak.cpp
g++ memory_leak.o -o exec.memory_leak -lasan

直接將 array a 存取第 6 個元素。

cpp

void case1(){
  /* out of bound */
  int a[5];

  for(int i = 0; i <= 5; i++){
    a[i] = 0;
  }

  return ;
}

Output:

text

=================================================================
==566==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffe6534a24 at pc 0x7f4c18400a7f bp 0x7fffe65349e0 sp 0x7fffe65349d0
WRITE of size 4 at 0x7fffe6534a24 thread T0
    #0 0x7f4c18400a7e in case1() /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:8
    #1 0x7f4c18400a7e in main /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:32
    #2 0x7f4c16c61b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #3 0x7f4c18400ae9 in _start (/mnt/d/Blog/source/_posts/AddressSanitizer/exec.case1+0xae9)

Address 0x7fffe6534a24 is located in stack of thread T0 at offset 52 in frame
    #0 0x7f4c184008bf in main /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:30

  This frame has 1 object(s):
    [32, 52) 'a' <== Memory access at offset 52 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:8 in case1()
Shadow bytes around the buggy address:
  0x10007cc9e8f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e910: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e920: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e930: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1
=>0x10007cc9e940: f1 f1 00 00[04]f2 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e950: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e960: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e970: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10007cc9e990: 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
==566==ABORTING

malloc array a 之後,不進行釋放,馬上 return。

cpp

void case2(){
  /* memory leak */
  int* a = new int[5];

  return ;
}

Output:

剛好可以看到,通常一個 int 為 4 bytes,開 5 個 int 的 array,一共是 20 bytes,這邊 AddressSanitizer 有抓到 20 bytes 的 memory leak。

text

=================================================================
==568==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 20 byte(s) in 1 object(s) allocated from:
    #0 0x7fd7f9f20608 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0608)
    #1 0x7fd7fb2008bd in case2() /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:16
    #2 0x7fd7fb2008bd in main /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:34

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

將 array a malloc 之後,釋放並存取。

cpp

void case3(){
  /* use after free */
  int* a = new int[5];
  delete [] a;
  a[0] = 0;

  return ;
}

Output:

text

=================================================================
==570==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000010 at pc 0x7f209ee00c35 bp 0x7ffffc73eb30 sp 0x7ffffc73eb20
WRITE of size 4 at 0x603000000010 thread T0
    #0 0x7f209ee00c34 in case3() /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:25
    #1 0x7f209ee008b8 in main /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:36
    #2 0x7f209d661b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #3 0x7f209ee00909 in _start (/mnt/d/Blog/source/_posts/AddressSanitizer/exec.case3+0x909)

0x603000000010 is located 0 bytes inside of 20-byte region [0x603000000010,0x603000000024)
freed by thread T0 here:
    #0 0x7f209db21480 in operator delete[](void*) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe1480)
    #1 0x7f209ee00c05 in case3() /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:24

previously allocated by thread T0 here:
    #0 0x7f209db20608 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0608)
    #1 0x7f209ee00bfa in case3() /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:23

SUMMARY: AddressSanitizer: heap-use-after-free /mnt/d/Blog/source/_posts/AddressSanitizer/main.cpp:25 in case3()
Shadow bytes around the buggy address:
  0x0c067fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c067fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c067fff8000: fa fa[fd]fd fd fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
==570==ABORTING

cpp

#include <cstdlib>

void case1(){
  /* out of bound */
  int a[5];

  for(int i = 0; i <= 5; i++){
    a[i] = 0;
  }

  return ;
}

void case2(){
  /* memory leak */
  int* a = new int[5];

  return ;
}

void case3(){
  /* use after free */
  int* a = new int[5];
  delete [] a;
  a[0] = 0;

  return ;
}

int main(){
#ifdef CASE1
  case1();
#elif defined CASE2
  case2();
#elif defined CASE3
  case3();
#endif
}

以下使用此指令進行編譯

bash

g++ main.cpp -o exec.main -O2 -fsanitize=address -g

根據 AddressSanitizerPerformanceNumbers 上面所測試的結果,所造成的效能下降最多可以達到 3.79 倍的時間,因此在找出 bug 之後,最好不要使用,讓程式達到最快的效率。


Reference

廣告 AD