記憶體問題難定位,那是因為你沒用ASAN

2022-08-05 12:00:17
摘要:ASAN全稱:Address Sanitizer,google發明的一種記憶體地址錯誤檢查器。目前已經被整合到各大編譯器中。

本文分享自華為雲社群《記憶體定位利器-ASAN使用小結》,作者:雲端儲存開發者支援團隊。

1.什麼是ASAN

ASAN全稱:Address Sanitizer,google發明的一種記憶體地址錯誤檢查器。目前已經被整合到各大編譯器中。

2.為什麼我們需要ASAN

在c/c++開發過程中,經常出現記憶體異常使用的問題,比如踩記憶體,被踩的記憶體如果未被使用對外無影響。而一旦使用了被踩的記憶體,可能會出現程序core,死迴圈,進入異常分支等等各種千奇百怪的問題。這個時候要去定位這段記憶體為什麼被踩,相當困難,因為已經錯過了案發現場。如果不幸,遇到了這種問題,常用手段是:

1)分析被踩記憶體的特徵值,比如是否是一個magic值,然後從程式碼庫中找特徵值,分析程式碼,縮小排查方向。

2)找到必現條件,通過gdb的watch功能,watch被踩的記憶體地址,一旦被踩,gdb將會打出踩記憶體的堆疊。

根據作者的經驗,出現踩記憶體的問題需要消耗大量的人力定位。少則一人周,多種數人月。而這類問題,往往是由於某個低階編碼錯誤引起的。

所以,我們迫切的希望,能在踩記憶體的第一現場就把凶手抓住,而不是在破壞已經表現出來的時候再去分析定位。而asan就能達到這個目的,它會接管記憶體的申請和釋放,每次的記憶體的讀寫都會檢查,因此可以做到快速的定位踩記憶體的問題。在asan之前也有其他的記憶體分析工具,但是asan是這些工具中比較優秀的,並不會損失大量的效能和記憶體(官方資料,效能下降兩倍,而valgrind下降20倍:https://github.com/google/sanitizers/wiki/AddressSanitizerComparisonOfMemoryTools)。

3.ASAN可以定位哪些記憶體使用問題

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(記憶體洩露)

4.怎麼使用ASAN工具

現在大部分編譯器已經整合了支援asan的能力,編譯的時候加上編譯選項即可。

常見的編譯選項:

  • -fsanitize=address 開起asan能力,gcc 4.8版本開啟支援。
  • -fsanitize-recover=address :asan檢查到錯誤後,不core繼續執行,需要配合環境變數ASAN_OPTIONS=halt_on_error=0:report_path=xxx使用。gcc 6版本開始支援。

本文使用的是華為 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的程序號。檔案記錄了詳細的錯誤資訊。

5. ASAN的原理是什麼

ASAN要記錄每一塊記憶體的可用性。把使用者程式所在的記憶體區域叫做主記憶體, 而記錄主記憶體可用性的記憶體區域,則叫做影子記憶體 (Shadow memory)。

所有主記憶體的分配都按照 8 位元組的方式對齊。然後按照 1:8 的壓縮比例對主記憶體的可用性進行記錄,然後存入影子記憶體中。影子記憶體無法被使用者直接讀寫, 需要編譯器生成相關的程式碼來存取。

每一次記憶體的分配和釋放, 都會寫入影子記憶體。每次讀/寫記憶體區域前, 都會讀取一下影子記憶體, 獲得這塊記憶體存取合法性 (是否被分配, 是否已被釋放)。

對影子記憶體的寫入只在分配記憶體的時候發生, 所以只要分配記憶體是多執行緒安全的, ASan 就是多執行緒安全的, 這在大部分情況下也確實成立。

計算影子記憶體的地址需要快速,他們採用了: 主記憶體地址除以 8,再加上一個偏移量的做法. 因為堆疊分別在虛擬記憶體地址空間的兩端,這樣影子記憶體就會落在中間。而如果使用者以外存取了影子記憶體,那麼影子記憶體的"影子記憶體"就會落到一個非法的範圍 (Shadow Gap) 內,就可以知道存取出了些問題。

 

點選關注,第一時間瞭解華為雲新鮮技術~