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

人生有三寶:吃飯、玩樂、睡飽飽
程式有三種錯誤:語法錯誤 (syntax error)、執行錯誤 (runtime error)、語意錯誤 (semantic error)
今天要來解決的就是執行錯誤中的常見的記憶體相關的錯誤
前言
常常在寫程式的時候,發生 out of bound 都不知道,只能等到看到 Segmentation Fault 才知道錯誤。
而其中就算知道錯誤了,也找不出來錯誤在哪裡,一般都是在編譯的時候用 -g
,然後用 gdb 重新執行一遍,等到出錯後看錯誤訊息來找出錯誤的地方,但我有遇過一次是 gdb 報錯的地方在 malloc 的地方,檢查了很多遍也找不出來錯誤的地方 QAQ,之後是靠著 AddressSanitizer 才找出正確錯誤的地方。
AddressSanitizer
今天介紹的 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
其中 Windows 的 MSVC 在安裝時的時候要記得勾選,可以去檢查看看是否有安裝到。

使用方式
你只需要在編譯的時候加上 -fsanitize=address
,接著在執行的時候,遇到錯誤就會顯示詳細的錯誤資訊,可以參考以下範例。
- 確保程式的執行效率不要下降太多,建議使用
-O1
或者更高的最佳化。 - 使用
-g
來顯示詳細的錯誤行數。
-fsanitize=address -O1
如果是要檢查 heap leak 或是 heap overflow,則使用下方的指令
-fsanitize=leak -O1
全部指令就會是這樣:
g++ -fsanitize=address -O1 memory_leak.cpp -o exec.memory_leak
如果是編譯和連結是分開的話,連結的時候加上 -lasan:
g++ -fsanitize=address -O1 -c memory_leak.cpp
g++ memory_leak.o -o exec.memory_leak -lasan
Case 1: Out Of Bound
直接將 array a 存取第 6 個元素。
void case1(){
/* out of bound */
int a[5];
for(int i = 0; i <= 5; i++){
a[i] = 0;
}
return ;
}
Output:
=================================================================
==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
Case 2: Memory Leak
malloc array a 之後,不進行釋放,馬上 return。
void case2(){
/* memory leak */
int* a = new int[5];
return ;
}
Output:
剛好可以看到,通常一個 int 為 4 bytes,開 5 個 int 的 array,一共是 20 bytes,這邊 AddressSanitizer 有抓到 20 bytes 的 memory leak。
=================================================================
==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).
Case 3: Use After Free
將 array a malloc 之後,釋放並存取。
void case3(){
/* use after free */
int* a = new int[5];
delete [] a;
a[0] = 0;
return ;
}
Output:
=================================================================
==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
完整範例
#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
}
以下使用此指令進行編譯
g++ main.cpp -o exec.main -O2 -fsanitize=address -g
效能問題
根據 AddressSanitizerPerformanceNumbers 上面所測試的結果,所造成的效能下降最多可以達到 3.79 倍的時間,因此在找出 bug 之後,最好不要使用,讓程式達到最快的效率。
Reference
如果你覺得這篇文章有用 可以考慮贊助飲料給大貓咪