文章目錄
- 1 為什么需要AddressSanitizer?
- 2 如何使用AddressSanitizer
- 3 AddressSanitizer的原理
- 4 總結
1 為什么需要AddressSanitizer?
Valgrind是比較常用的內存問題定位工具,既然已經有了Valgrind,為什么還需要AddressSanitizer呢?
與Valgrind相比,AddressSanitizer存在以下優勢:
- Valgrind通過模擬CPU來檢測內存錯誤,導致會以較慢的速度運行程序;而AddressSanitizer是在編譯階段插入檢查的邏輯,執行速度比Valgrind快很多
- Valgrind是一個獨立的工具,可以使用在任何程序上;而AddressSanitizer與編譯器緊密集成,可以在構建時自動啟用
- 在錯誤信息的展示上,AddressSanitizer提供的錯誤信息比Valgrind容易理解
- AddressSanitizer作為編譯器的一部分,通過編譯選項啟用;而Valgrind作為獨立的工具,需要更多的配置和學習才能使用
- AddressSanitizer通過編譯時插樁和運行時檢查來檢測內存錯誤,誤報率較低
從使用場景來說,AddressSanitizer專注于發現內存未釋放
和訪問非法內存
的問題。
2 如何使用AddressSanitizer
從gcc 4.8開始,AddressSanitizer稱為gcc的一部分,但是,gcc 4.8的AddressSanitizer沒有符號信息,建議使用gcc 4.9及以上版本。
首先使用AddressSanitizer來檢測下常見的內存問題。
用例一:未正確釋放內存
#include <iostream>int main() {int *ptr = new(int);*ptr = 0;std::cout << *ptr << std::endl;
}
上述代碼申請了一個int的空間,但是沒有釋放,編譯該程序:g++ -fsanitize=address -fno-omit-frame-pointer -o main main.cpp
,然后直接執行:
0=================================================================
==711759==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7f37f92201c7 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99#1 0x55581dc742be in main (/root/asan/main+0x12be)#2 0x7f37f8c54fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).
結果比Valgrind更好理解:
- 出現了內存泄漏
- 導致內存泄漏的內存分配的調用棧
- 總結,在一次分配過程中泄漏了4字節
用例二:分配的內存未使用配對的函數釋放
#include <iostream>int main() {int *ptr = new(int);*ptr = 0;free(ptr);
}
=================================================================
==711960==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new vs free) on 0x602000000010#0 0x7f8ef531c517 in __interceptor_free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127#1 0x5571e62742ef in main (/root/asan/main+0x12ef)#2 0x7f8ef4d52fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58#3 0x7f8ef4d5307c in __libc_start_main_impl ../csu/libc-start.c:409#4 0x5571e62741c4 in _start (/root/asan/main+0x11c4)0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
allocated by thread T0 here:#0 0x7f8ef531e1c7 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99#1 0x5571e627429e in main (/root/asan/main+0x129e)#2 0x7f8ef4d52fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58SUMMARY: AddressSanitizer: alloc-dealloc-mismatch ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:127 in __interceptor_free
==711960==HINT: if you don't care about these errors you may set ASAN_OPTIONS=alloc_dealloc_mismatch=0
==711960==ABORTING
結果也可以直接看出來:
- alloc-dealloc-mismatch(operator new vs free):分配和銷毀不匹配
- 分配和銷毀的調用棧
用例三:使用已經被回收的內存
#include <iostream>int main() {int *ptr = new(int);*ptr = 0;delete(ptr);*ptr = 1;
}
=================================================================
==712018==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x560c4aac2331 bp 0x7ffd02a2d040 sp 0x7ffd02a2d030
WRITE of size 4 at 0x602000000010 thread T0#0 0x560c4aac2330 in main (/root/asan/main+0x1330)#1 0x7ff2fbc22fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58#2 0x7ff2fbc2307c in __libc_start_main_impl ../csu/libc-start.c:409#3 0x560c4aac21c4 in _start (/root/asan/main+0x11c4)0x602000000010 is located 0 bytes inside of 4-byte region [0x602000000010,0x602000000014)
freed by thread T0 here:#0 0x7ff2fc1ef22f in operator delete(void*, unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:172#1 0x560c4aac22f9 in main (/root/asan/main+0x12f9)#2 0x7ff2fbc22fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58previously allocated by thread T0 here:#0 0x7ff2fc1ee1c7 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99#1 0x560c4aac229e in main (/root/asan/main+0x129e)#2 0x7ff2fbc22fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58SUMMARY: AddressSanitizer: heap-use-after-free (/root/asan/main+0x1330) in main
Shadow bytes around the buggy address:0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa[fd]fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8050: 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: 00Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: faFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: feLeft alloca redzone: caRight alloca redzone: cbShadow gap: cc
==712018==ABORTING
會輸出heap-use-after-free類型的結果:
- 結果包含內存分配、釋放以及寫已經釋放的內存的調用棧
- 在SUMMARY下面出現兩塊數據,這個就涉及到AddressSanitizer的實現
3 AddressSanitizer的原理
前面說過,AddressSanitizer主要的使用場景是內存未釋放
和訪問非法內存
,為了能夠發現這兩種情況,需要為內存保存一些狀態信息。
為了能夠為內存保存狀態信息,AddressSanitizer會用自己的運行時庫替換默認的malloc/free,將需要操作內存的代碼進行替換。
例如,如果是讀操作:
... = *address;
則在訪問內存前執行檢查:
if (IsPoisoned(address)) {ReportError(address, kAccessSize, kIsWrite);
}
... = *address;
如果是寫操作:
*address = ...;
則在寫操作之前進行檢查:
if (IsPoisoned(address)) {ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;
其中的IsPoisoned()
函數就是用來檢查內存地址是否合法
。
AddressSanitizer使用影子內存
的機制進行內存地址的合法性檢查:對程序實際使用的內存使用額外的字節來存儲它的狀態信息。
使用1個字節存儲實際使用的8個字節的狀態:
- 8個字節的數據可讀寫,則1個字節的值為0
- 8個字節的數據不可讀寫,則1個字節的值為負數,不同的值表示不同類型的內存,如0xfa表示堆左邊的redzone(redzone是在正常可以使用的內存兩側的邊界),0xf1表示棧左邊的redzone
- 8個字節中前k個字節可讀寫,后8-k個字節不可讀寫,則1個字節的值為k
因此,IsPoisoned()
函數就是檢查地址對應的影子內存中的狀態是否可以訪問,如果不能訪問,則出現訪問非法內存
的問題,如果訪問的地址的影子內存是redzone,則說明出現內存訪問越界
的問題。
基于上述描述,再來看上面heap-use-after-free
的輸出的后面部分的內容:
Shadow bytes around the buggy address:0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa[fd]fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c047fff8050: 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: 00Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: faFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: feLeft alloca redzone: caRight alloca redzone: cbShadow gap: cc
==712018==ABORTING
第二部分是圖例,也就是影子內存中不同的值代表的含義,例如,fd表示已經被釋放的內存。
第一部分顯示的就是影子內存的數據,出現了fd表示已經被釋放的內存,對該內存再次訪問就會出現heap-use-after-free問題。
下面再看一個棧溢出的例子:
#include <iostream>
#include <string.h>int main() {int arr1[10];int arr2[12];memcpy(arr1, arr2, 12*sizeof(int));
}
結果:
=================================================================
==714984==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffe915be528 at pc 0x7f22667182c3 bp 0x7ffe915be4d0 sp 0x7ffe915bdc78
WRITE of size 48 at 0x7ffe915be528 thread T0#0 0x7f22667182c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827#1 0x5620a92e0349 in main (/root/asan/main+0x1349)#2 0x7f22661c8fcf in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58#3 0x7f22661c907c in __libc_start_main_impl ../csu/libc-start.c:409#4 0x5620a92e01c4 in _start (/root/asan/main+0x11c4)Address 0x7ffe915be528 is located in stack of thread T0 at offset 72 in frame#0 0x5620a92e0298 in main (/root/asan/main+0x1298)This frame has 2 object(s):[32, 72) 'arr1' (line 5)[112, 160) 'arr2' (line 6) <== Memory access at offset 72 partially underflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
Shadow bytes around the buggy address:0x1000522afc50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afc60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afc70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afc80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afc90: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
=>0x1000522afca0: 00 00 00 00 00[f2]f2 f2 f2 f2 00 00 00 00 00 000x1000522afcb0: f3 f3 f3 f3 00 00 00 00 00 00 00 00 00 00 00 000x1000522afcc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afcd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afce0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000x1000522afcf0: 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: 00Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: faFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: feLeft alloca redzone: caRight alloca redzone: cbShadow gap: cc
==714984==ABORTING
結果表明,這是一個stack-buffer-overflow的錯誤,對于棧內存來說,也會給出分配棧空間的變量的地方以及對棧內存出現非法訪問的調用棧,還分別給出了兩個變量的位置。
下面的影子內存可以看出:
- f1是棧左紅區,f2是棧中紅區,f3是棧右紅區
- 所有的棧空間都處于f1和f3之間,變量之間用f2隔開
- arr1是10個int,也就是40個字節,使用5個影子字節,即影子內存中
=>
所在行的左側的5個字節 - arr2是12個int,也就是48個字節,使用6個影子字節,即影子內存中
=>
所在行的左側的6個字節 [f2]
表明訪問棧中紅區,出現棧的訪問越界
4 總結
AddressSanitizer是進行內存異常使用分析的工具,該工具已經集成到編譯器中,因此,只能用于分析C/C++語言的內存問題分析。與Valgrind相比,運行速度更快,但是,從場景來說,AddressSanitizer主要用于檢測內存的非法使用
,當然也包括內存未正確釋放
的問題,而Valgrind則可以分析出導致內存增長的調用棧。
因此:
- 如果出現內存偏高的問題,可以使用Valgrind工具分析
- 如果出現內存導致的core問題,可以使用gdb的watchpoint或者AddressSanitizer分析