要解決的問題
由於 C/C++ 這類編程語言與硬件(主要是內存)非常貼近,使用 C/C++ 編程,經常遇到的的一個問題就是內存錯誤,其中可能包括:
- 內存泄漏:忘記 free 之前在堆中申請的內存,並丟失了所申請內存的指針;
- 內存訪問越界:包括對全局內存、棧內存、堆內存訪問的越界;
- 釋放后使用:訪問已經被 free 的內存;
- 返回后使用:訪問已經返回的函數棧中的內存;
- ……
這些錯誤,有的會在程序運行的過程中報錯並使程序終止,這還算好的;有的則根本不會報錯,暗中影響程序的正確性。
Address Sanitizer 應運而生
因此,Google 開發了一款專門用於檢測內存訪問錯誤的工具:AddressSanitizer(簡稱 ASan),它可以自動檢測程序運行時(runtime)發生的許多內存訪問錯誤。
你不需要專門安裝它,因為它被集成到了 LLVM 3.1 版本及以后、GCC 4.8 版本及以后了,所以你可以直接讓 Clang 或 GCC 打開 AddressSanitizer。比如,寫一個最簡單的內存訪問越界的程序:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v(4, 123); // 長度為 4 的 vector
cout << v[6] << "\n"; // 訪問越界
return 0;
}
編譯的時候打開-fsanitize=address
開關:
clang++ -fsanitize=address -o main main.cpp
編譯完成以后,運行程序,就會出現這樣的情況:
第一行紅色報錯:heap-buffer-overflow
,說明是堆上的內存訪問越界了。確實,因為vector
是被分配在堆上的。
再看第二行藍色,說是線程T0
在READ
一塊兒長度為 4 (一個int
的大小)的內存的時候發生的錯誤,緊跟着下面幾行就是錯誤處的調用棧了:
#0 0x10237ae4c in main main.cpp:8
#1 0x10252d0f0 in start+0x204 (dyld:arm64e+0x50f0)
看到棧頂的元素#0
就是main.cpp:8
,說明是 main.cpp 源代碼的第 8 行出現了錯誤,就是cout << v[6] << "\n";
這一句話。這時就已經定位到發生錯誤的地方了。
如果不使用 AddressSanitizer,這個程序很有可能“正常運行”,即輸出v[6]
為0
,讓人以為這個程序沒問題。但實際上程序是錯誤的。這種錯誤單靠 Debugger 是找不出來的,或者說很難找出來。
LeetCode 對 AddressSanitizer 的應用
LeetCode 在編譯代碼的時候也會自動開啟 AddressSanitizer 來檢測內存訪問越界和釋放后使用錯誤:
因此,你很可能在 LeetCode 上運行的時候會報出這樣看不懂的 runtime error:
這里的報錯信息較為簡陋,因此你可以在自己本機編譯調試的時候加上-fsanitize=address
選項來打開 AddressSanitizer,在本機運行,就可以看到更為詳細的報錯。注意,最好再加上-g
選項,這樣程序中就會帶有調試符號和源碼行號,報錯信息中就會顯示到底是程序中的哪一個函數的哪一行發生了內存訪問錯誤。
結語
本文僅僅介紹了 Address Sanitizer 最基本的概念和使用方法。如果你對它的詳細用法和底層原理很感興趣,最好的閱讀材料就是官方 Wiki:https://github.com/google/sanitizers/wiki。還有其他的常用的內存檢測工具,如 Valgrind Memcheck。