寫在前面:
如果你發現你的程序棧寫漏了(發現一個值突然發生了你預想不到的變化, 比如int a = 5,然後cout的時候就成了55)。
如果你發現你的程序每天佔用內存都變大一點。
如果你發現你的程序core dump了。
如果你發現。。。
除了性能問題,都可以先用asan跑一下,說不定能發現什麼。
缺點:
會讓程序變得很慢,導致可能有些線程競爭的地方被剛好掩蓋掉了。
0. 功能:
-
Use after free (dangling pointer dereference)
註釋:版本不同,可能功能有所不同。
1. 環境
1. centos 6或7
gcc 4.8.5 : 只有Asan,即只能檢測內存越界。
gcc 4.9.2 : 有Asan和Lsan兩種,可以用asan來做越界檢測,用lsan做內存泄露檢測。(建議使用, gcc的安裝跟gcc4.8.5一樣,詳看tensorflow 配置centos6環境)
gcc 7.2 : Asan中集成了LSan。(建議使用, gcc的安裝跟gcc4.8.5一樣,詳看tensorflow 配置centos6環境),意思就是隻用asan就可以啦。
2. 編譯選項
因爲gcc 4.9.2版本最複雜,所以我們就按照4.9.2的來寫,4.8.5的不會有內存檢測,7.2的不用做lsan, 以下我們寫個編譯的例子,很簡單。
g++ main.cpp -g -llsan -fsanitize=leak -o main
1. 需要注意的是需要帶-g選項,會加入一些調試信息到符號表以供輸出使用,不帶也可以,如果不帶,可能看不到錯在哪一行;
2. 需要動態鏈接庫liblsan.so,這個動態鏈接庫會替換掉malloc等系統函數,在自己的malloc中加上統計信息,以達到檢測內存泄露的作用。需要注意的點是,我們的環境中可能會有多個gcc或者找不到dso的時候。那麼我們可以使用g++ --print-file-name=libasan.so來找到系統的動態鏈接庫(只能是系統的庫),這條命令會告訴你so在哪,但在我們的環境中我遇到了一個比較坑爹的問題。就是so是有,但是so的大小僅爲4,裏面寫着讓重新下載so。
3. -fsanitize=,這個有好幾種選項、
1. asan(內存檢測),如標題;
2. ubsan(未定義行爲檢測), 有的時候debug的程序沒問題,release的程序會奇奇怪怪的core dump掉,那麼你需要做這個檢測;
3. tsan(線程安全檢測),如標題;tsan有一個點需要注意,因爲大家代碼跑通後一般不會用tsan做檢測,再者tsan出來時間不長,一些老的庫會有非常多的線程安全問題。再加上檢測條件非常嚴格。所以,大型項目第一用tsan做檢查的時候,可能每一行都會有線程安全問題。
4. leak(泄露檢測),被1包括了。
4. 如果我們有多個libasan.so,我們需要跟-L/path/to/lib,這個大家都懂,就不再敘述了。
5. 如果提示請加載PRELOAD,那麼請export LD_PRELOAD=或者export LD_PRELOAD=/path/to/liblsan.so/libasan.so
我們今天主要講asan,如果對tsan有興趣可以看下官網,或者這篇文章,自己給自己打個廣告,哈哈。
3. 泄露檢測
我們寫如下代碼:
#include <iostream>
using namespace std;
int main()
{
int *p = new int(5);
std::cout << *p << std::endl;
return 0;
}
該代碼只有new,但沒有delete函數。當我們使用上述例子,編譯:
./g++ main.cpp -g -llsan -fsanitize=leak
然後我們可能會得到編譯錯誤的提示 lsan沒有找到。可以使用print找到,如下:
./g++ --print-file-name=liblsan.so
加上-L即可。這時我們就編好了一個帶內存泄漏檢查的可執行文件,之後我們./a.out。
可能會出現提示:請配置LD_PRELOAD環境變量(7.2),或者直接core dump(4.9), 或者提示本該屬於leak檢查的內存映射段被其他dso佔用了(4.9)。都需要配置正確的LD_PRELOAD環境變量。
我們用之前找到的liblsan,配置到LD_PRELOAD內,例如:
export LD_PRELOAD=/root/local/lib64/libasan.so
運行即可,如果還崩,那就大象zhaozheng09。
如上代碼,我們可以得到結果:
=================================================================
==1712101==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x7fd00aabac28 in operator new(unsigned long) ../../../../libsanitizer/lsan/lsan_interceptors.cc:161
#1 0x4008a7 in main /root/local/bin/main.cpp:7
#2 0x7fd009ee0504 in __libc_start_main (/lib64/libc.so.6+0x22504)
SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
第9行,是個總結,他會告訴你總共泄露了4bytes,這4bytes屬於1次開闢的。
4-7行,我們這裏只泄露了一次,如果多次的話會有多個4-7行,會告訴你那個線程開闢的空間,哪個線程釋放的空間。並且打印出詳細的開闢和釋放的函數棧,當然我們注意第五行,我們用的malloc已經被替換成lsan下的malloc了。
第2行,錯誤類型。
4.越界檢測
首先註明一點,不一定只有越界導致core dump了纔會打錯誤報告,只要訪問到了不合法的內存都會報錯,比如申請了一個包涵四個int元素的數組,而我們訪問/修改了這個數組的第五個元素。那麼就會報錯。
直接看代碼吧,如下代碼:
#include <iostream>
#include <stdint.h>
using namespace std;
int main()
{
char p[5] = "";
uint8_t tmp = 5;
p[-1] = 7;
cout << (void*)p << endl;
cout << (void*)&tmp << endl;
cout << (uint32_t)tmp << endl;
return 0;
}
編譯代碼:
g++ main.cpp -lasan -fsanitize=address -g
得到了a.out,然後我們去./a.out。如果我們裸跑,那麼我們很奇怪的發現tmp被修改成了7。其實我就想描述一下,別的線程或者一不小心的越界都會導致各種奇奇怪怪的錯誤。這個時候,我們用上述命令編譯之後。就會報告如下錯誤。
其實我覺得可能誤報,因爲這是一個在c++看來是允許的操作。但只是我們看起來用法不對而已。(過於偏激,不認同請忽略)
=================================================================
==1729613==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7ffd1db351ef at pc 0x400c24 bp 0x7ffd1db351b0 sp 0x7ffd1db351a8
WRITE of size 1 at 0x7ffd1db351ef thread T0
#0 0x400c23 in main /root/code/test/asan/main.cpp:10
#1 0x7fc9489b4504 in __libc_start_main (/lib64/libc.so.6+0x22504)
#2 0x400a98 (/root/code/test/asan/main+0x400a98)
Address 0x7ffd1db351ef is located in stack of thread T0 at offset 31 in frame
#0 0x400b75 in main /root/code/test/asan/main.cpp:7
This frame has 1 object(s):
[32, 37) 'p' <== Memory access at offset 31 underflows 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-underflow /root/code/test/asan/main.cpp:10 main
Shadow bytes around the buggy address:
0x100023b5e9e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5e9f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100023b5ea30: 00 00 00 00 00 00 00 00 00 00 f1 f1 f1[f1]05 f4
0x100023b5ea40: f4 f4 f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00
0x100023b5ea50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100023b5ea80: 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
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Contiguous container OOB:fc
ASan internal: fe
==1729613==ABORTING
我們來分析一下結果。
第二行報出了錯誤類型。stack-buffer-underflow,並報出了sp,pc,bp寄存器裏存放的值。
第三行報出操作WRITE,寫了多少字節,且操作的線程是T0
第4-6行是T0的函數棧。
第八行表示那個地址出錯,這個地址是屬於誰,誰開闢的。
剩下的行表示出錯誤細節以及出錯虛擬內存的情況,下面有有F1, F2等註解,可以詳細看說明,我們這裏表示錯在了那個位置,覆蓋了那個不該覆蓋的變量。
我們只需要看函數棧就可以。
5. ASAN_OPTIONS/UBSAN_OPTIONS/LSAN_OPTIONS/TSAN_OPTIONS
下面的不用看了。上面的一般就夠了,我也沒用過,如果寫錯了,不用大象我。
asan設置了ASAN_OPTIONS環境變量,可以帶更個性化的參數,比如可以選擇是否出現內存泄露立刻停止,或者正常跑完後停止,或者不檢測哪些文件的內存泄露,等等;但不知道我們的gcc4.8,4.9是不是支持屬性。
詳細內容請看上述官方文檔。
一般除了使用tsan之外,不需要設置OPTIONS變量。
6.加入TensorFlow
1. 儘量將gcc版本更新到最高。
2. 在bazel的編譯選項中加入-g -llsan -fsanitize=leak或-g -lasan -fsanitize=address。
3. 如果找不到leak,那麼請設置CC和CXX環境變量。編譯即可。
4. 如果讓配置LD_PRELOAD,那麼export LD_PRELOAD=即可。或者指向相應的dso。
5.注意:在使用asan和lsan的時候,我發現Python本身會報一些內存泄露或者誤報,我們都不需要管,只要在裏面沒有出現我們自己寫的代碼的內存泄露或者越界錯誤即可。
6. ubsan可以正常使用,但是tsan遇到了conflict shadow memory的問題,讓加-FPIC,加入後遇到編譯錯誤。估計是一些.cc不支持固定地址。