摘要:ASAN全稱:Address Sanitizer,google發明的一種記憶體地址錯誤檢查器。目前已經被整合到各大編譯器中。
本文分享自華為雲社群《記憶體定位利器-ASAN使用小結》,作者:雲端儲存開發者支援團隊。
ASAN全稱:Address Sanitizer,google發明的一種記憶體地址錯誤檢查器。目前已經被整合到各大編譯器中。
在c/c++開發過程中,經常出現記憶體異常使用的問題,比如踩記憶體,被踩的記憶體如果未被使用對外無影響。而一旦使用了被踩的記憶體,可能會出現程序core,死迴圈,進入異常分支等等各種千奇百怪的問題。這個時候要去定位這段記憶體為什麼被踩,相當困難,因為已經錯過了案發現場。如果不幸,遇到了這種問題,常用手段是:
1)分析被踩記憶體的特徵值,比如是否是一個magic值,然後從程式碼庫中找特徵值,分析程式碼,縮小排查方向。
2)找到必現條件,通過gdb的watch功能,watch被踩的記憶體地址,一旦被踩,gdb將會打出踩記憶體的堆疊。
根據作者的經驗,出現踩記憶體的問題需要消耗大量的人力定位。少則一人周,多種數人月。而這類問題,往往是由於某個低階編碼錯誤引起的。
所以,我們迫切的希望,能在踩記憶體的第一現場就把凶手抓住,而不是在破壞已經表現出來的時候再去分析定位。而asan就能達到這個目的,它會接管記憶體的申請和釋放,每次的記憶體的讀寫都會檢查,因此可以做到快速的定位踩記憶體的問題。在asan之前也有其他的記憶體分析工具,但是asan是這些工具中比較優秀的,並不會損失大量的效能和記憶體(官方資料,效能下降兩倍,而valgrind下降20倍:https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools)。
1、Heap OOB(HeapOutOfBounds 堆記憶體越界)
int main(int argc, char **argv) { int *array = new int[100]; array[0] = 0; int res = array[argc + 100]; // BOOM delete [] array; return res; }
2、Stack OOB(StackOutOfBounds 棧越界)
int main(int argc, char **argv) { int stack_array[100]; stack_array[1] = 0; return stack_array[argc + 100]; // BOOM }
3、Global OOB(GlobalOutOfBounds 全域性變數越界)
int global_array[100] = {-1}; int main(int argc, char **argv) { return global_array[argc + 100]; // BOOM }
4、UAF(UseAfterFree 記憶體釋放後使用)
int main(int argc, char **argv) { int *array = new int[100]; delete [] array; return array[argc]; // BOOM }
5、UAR(UseAfterReturn 棧記憶體回收後使用,該功能還存在少量bug,預設未開啟,開啟ASAN_OPTIONS=detect_stack_use_after_return=1)
int *ptr; __attribute__((noinline)) void FunctionThatEscapesLocalObject() { int local[100]; ptr = &local[0]; } int main(int argc, char **argv) { FunctionThatEscapesLocalObject(); return ptr[argc]; }
6、UMR(uninitialized memory reads讀取未初始化記憶體)
7、Leaks(記憶體洩露)
現在大部分編譯器已經整合了支援asan的能力,編譯的時候加上編譯選項即可。
常見的編譯選項:
本文使用的是華為 EulerOS v2r9 版本。
下面開始我們的asan之旅
1、寫個bug,寫一個釋放後的記憶體還在使用的例子。
#include <stdlib.h> int main() { int *p = malloc(sizeof(int)*10); free(p); *p = 3;//該程式正常情況下並不會導致程序core,因為free後的記憶體被glibc的記憶體分配器快取著 return 0; }
2、加上編譯選項編譯:gcc -fsanitize=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/ 其中-L指定的是libasan.so存放的位置。
3、指定asan的so的目錄,export LD_LIBRARY_PATH=/root/buildbox/gcc-10.2.0/lib64/,執行./a.out執行程式,將可以看到asan報錯。指出了記憶體異常使用的位置和原因。
4、在工程中,我們更希望程式遇到錯誤能不中斷,而繼續執行下去,我們可以使用 -fsanitize-recover=address 方法。這次我們更改下程式碼,多引入幾個錯誤。
#include <stdlib.h> int main() { int *p = malloc(sizeof(int)*10); free(p); *p = 3; //錯誤1.釋放後繼續使用 p = malloc(sizeof(int)*10); p[11] = 3;//錯誤2,越界寫 return 0; }
5、編譯:gcc -fsanitize=address -fsanitize-recover=address -g ./test.c -lasan -L /root/buildbox/gcc-10.2.0/lib64/
6、設定環境變數:export ASAN_OPTIONS=halt_on_error=0:log_path=/var/log/err.log,執行程式./a.out
7、檢視紀錄檔路徑:在/var/log目錄下,形成一個err.log.212的檔案,212是執行./a.out的程序號。檔案記錄了詳細的錯誤資訊。
ASAN要記錄每一塊記憶體的可用性。把使用者程式所在的記憶體區域叫做主記憶體, 而記錄主記憶體可用性的記憶體區域,則叫做影子記憶體 (Shadow memory)。
所有主記憶體的分配都按照 8 位元組的方式對齊。然後按照 1:8 的壓縮比例對主記憶體的可用性進行記錄,然後存入影子記憶體中。影子記憶體無法被使用者直接讀寫, 需要編譯器生成相關的程式碼來存取。
每一次記憶體的分配和釋放, 都會寫入影子記憶體。每次讀/寫記憶體區域前, 都會讀取一下影子記憶體, 獲得這塊記憶體存取合法性 (是否被分配, 是否已被釋放)。
對影子記憶體的寫入只在分配記憶體的時候發生, 所以只要分配記憶體是多執行緒安全的, ASan 就是多執行緒安全的, 這在大部分情況下也確實成立。
計算影子記憶體的地址需要快速,他們採用了: 主記憶體地址除以 8,再加上一個偏移量的做法. 因為堆疊分別在虛擬記憶體地址空間的兩端,這樣影子記憶體就會落在中間。而如果使用者以外存取了影子記憶體,那麼影子記憶體的"影子記憶體"就會落到一個非法的範圍 (Shadow Gap) 內,就可以知道存取出了些問題。