Linux 下的 AddressSanitizer
AddressSanitizer 是一個性能非常好的 C/C++ 內(nèi)存錯誤探測工具。它由編譯器的插樁模塊(目前,LLVM 通過)和替換了 malloc 函數(shù)的運行時庫組成。這個工具可以探測如下這些類型的錯誤:
- 對堆,棧和全局內(nèi)存的訪問越界(堆緩沖區(qū)溢出,棧緩沖區(qū)溢出,和全局緩沖區(qū)溢出)
- UAP(Use-after-free,懸掛指針的解引用,或者說野指針)
- Use-after-return(無效的棧上內(nèi)存,運行時標記 ASAN_OPTIONS=detect_stack_use_after_return=1)
- Use-After-Scope(作用域外訪問,clang 標記 -fsanitize-address-use-after-scope )
- 內(nèi)存的重復(fù)釋放
- 初始化順序的 bug
- 內(nèi)存泄漏
這個工具非常快。通常情況下,內(nèi)存問題探測這類調(diào)試工具的引入,會導(dǎo)致原有應(yīng)用程序運行性能的大幅下降,比如大名鼎鼎的 valgrind 據(jù)說會導(dǎo)致應(yīng)用程序性能下降到正常情況的十幾分之一,但引入 AddressSanitizer 只會減慢運行速度的一半。
AddressSanitizer 的使用
自 LLVM 的版本 3.1 和 GCC 的版本 4.8 開始,AddressSanitizer 就是它們的一部分。如果需要的話,也可以從源碼編譯 AddressSanitizerHowToBuild。
查看自己的 LLVM 版本和 GCC 版本來確認是否內(nèi)置了對 AddressSanitizer 的支持:
$ clang --version clang version 3.9.1-4ubuntu3~16.04.2 (tags/RELEASE_391/rc2) Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin$ gcc --version gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609 Copyright (C) 2015 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.我本地的工具雖然版本比較老,但對 AddressSanitizer 還是支持的。
看一下前面提到的 AddressSanitizer 的運行時庫:
$ locate asan /usr/lib/gcc/x86_64-linux-gnu/5/libasan.a /usr/lib/gcc/x86_64-linux-gnu/5/libasan.so /usr/lib/gcc/x86_64-linux-gnu/5/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/5/32/libasan.a /usr/lib/gcc/x86_64-linux-gnu/5/32/libasan.so /usr/lib/gcc/x86_64-linux-gnu/5/32/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/5/include/sanitizer/asan_interface.h /usr/lib/gcc/x86_64-linux-gnu/5/x32/libasan.a /usr/lib/gcc/x86_64-linux-gnu/5/x32/libasan.so /usr/lib/gcc/x86_64-linux-gnu/5/x32/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/7/libasan.a /usr/lib/gcc/x86_64-linux-gnu/7/libasan.so /usr/lib/gcc/x86_64-linux-gnu/7/libasan_preinit.o /usr/lib/gcc/x86_64-linux-gnu/7/include/sanitizer/asan_interface.h /usr/lib/gcc-cross/arm-linux-gnueabihf/5/libasan.a /usr/lib/gcc-cross/arm-linux-gnueabihf/5/libasan.so /usr/lib/gcc-cross/arm-linux-gnueabihf/5/libasan_preinit.o /usr/lib/gcc-cross/arm-linux-gnueabihf/5/include/sanitizer/asan_interface.h /usr/lib/gcc-cross/arm-linux-gnueabihf/5/sf/libasan.a /usr/lib/gcc-cross/arm-linux-gnueabihf/5/sf/libasan.so /usr/lib/gcc-cross/arm-linux-gnueabihf/5/sf/libasan_preinit.o /usr/lib/llvm-3.9/lib/clang/3.9.1/asan_blacklist.txt /usr/lib/llvm-3.9/lib/clang/3.9.1/include/sanitizer/asan_interface.h /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-i386.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-i386.so /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-i686.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-i686.so /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-preinit-i386.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-preinit-i686.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-preinit-x86_64.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-x86_64.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-x86_64.a.syms /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan-x86_64.so /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan_cxx-i386.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan_cxx-i686.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan_cxx-x86_64.a /usr/lib/llvm-3.9/lib/clang/3.9.1/lib/linux/libclang_rt.asan_cxx-x86_64.a.syms /usr/lib/llvm-5.0/lib/clang/5.0.2/asan_blacklist.txt /usr/lib/llvm-5.0/lib/clang/5.0.2/include/sanitizer/asan_interface.h /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-i386.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-i386.so /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-i686.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-i686.so /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-preinit-i386.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-preinit-i686.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-preinit-x86_64.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-x86_64.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-x86_64.a.syms /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan-x86_64.so /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan_cxx-i386.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan_cxx-i686.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan_cxx-x86_64.a /usr/lib/llvm-5.0/lib/clang/5.0.2/lib/linux/libclang_rt.asan_cxx-x86_64.a.syms /usr/lib/x86_64-linux-gnu/libasan.so.2 /usr/lib/x86_64-linux-gnu/libasan.so.2.0.0 /usr/lib/x86_64-linux-gnu/libasan.so.4 /usr/lib/x86_64-linux-gnu/libasan.so.4.0.0 /usr/lib32/libasan.so.2 /usr/lib32/libasan.so.2.0.0 /usr/libx32/libasan.so.2 /usr/libx32/libasan.so.2.0.0為了使用 AddressSanitizer,需要在使用 GCC 或 Clang 編譯鏈接程序時加上 -fsanitize=address 開關(guān)。為了獲得合理的性能,可以加上 -O1 或更高。為了在錯誤信息中獲得更友好的棧追蹤信息可以加上 -fno-omit-frame-pointer。為了獲得完美的棧追蹤信息,還可以禁用內(nèi)聯(lián)(使用 -O1)和尾調(diào)用消除(-fno-optimize-sibling-calls)
下面是一段存在內(nèi)存訪問錯誤的代碼:
// main.cpp int main(int argc, char **argv) {int *array = new int[100];delete [] array;return array[argc]; // BOOM }使用 GCC 編譯并運行:
$ gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g -o main main.cpp $ ./main ================================================================= ==5385==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44 at pc 0x0000004007d4 bp 0x7ffddf0bafb0 sp 0x7ffddf0bafa0 READ of size 4 at 0x61400000fe44 thread T0#0 0x4007d3 in main addresssanitizer_demo/main.cpp:4#1 0x7f297fc2282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)#2 0x4006b8 in _start (addresssanitizer_demo/main+0x4006b8)0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0) freed by thread T0 here:#0 0x7f2980065caa in operator delete[](void*) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99caa)#1 0x4007a8 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:3#2 0x7f297fc2282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)previously allocated by thread T0 here:#0 0x7f29800656b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)#1 0x400798 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:2#2 0x7f297fc2282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)SUMMARY: AddressSanitizer: heap-use-after-free addresssanitizer_demo/main.cpp:4 main Shadow bytes around the buggy address:0x0c287fff9f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c287fff9fc0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd0x0c287fff9fd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9fe0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9ff0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa0x0c287fffa000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fffa010: 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: faHeap right redzone: fbFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack partial redzone: f4Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: fe ==5385==ABORTINGAddressSanitizer 在探測到內(nèi)存錯誤之后,向 stderr 打印了錯誤信息并以非 0 值返回碼退出。AddressSanitizer 在發(fā)現(xiàn)第一個錯誤時退出程序。這主要是基于如下的設(shè)計:
- 這種方法允許 AddressSanitizer 產(chǎn)生更快和更小的生成碼(總共 ~5%)。
- 解決 bug 變得無法避免。AddressSanitizer 不產(chǎn)生誤報。一旦內(nèi)存崩潰發(fā)生,則程序進入不一致的狀態(tài),這可能導(dǎo)致令人費解的結(jié)果和潛在的誤導(dǎo)性的后續(xù)報告。
如果進程運行在沙盒中且運行在 OS X 10.10 或更早的版本上,則需要設(shè)置DYLD_INSERT_LIBRARIES 環(huán)境變量并把它指向由編譯器打包用于構(gòu)建可執(zhí)行文件的 ASan 庫。(可以搜索名字中包含 asan 的動態(tài)鏈接庫來找到這個庫。)如果沒有設(shè)置環(huán)境變量,則進程將試圖重新執(zhí)行。同時記住,當(dāng)把可執(zhí)行文件移動到另一臺機器時,ASan 庫也需要復(fù)制過去。
編譯時如果遺漏了 -g 參數(shù),導(dǎo)致可執(zhí)行文件中缺乏調(diào)試信息,則探測到內(nèi)存錯誤時,AddressSanitizer 吐出來的錯誤信息中,無法顯示具體的出錯的代碼行,就像下面這樣:
$ gcc -fsanitize=address -fno-omit-frame-pointer -O1 -o main main.cpp $ ./main ================================================================= ==5403==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44 at pc 0x0000004007d4 bp 0x7ffd981fce20 sp 0x7ffd981fce10 READ of size 4 at 0x61400000fe44 thread T0#0 0x4007d3 in main (addresssanitizer_demo/main+0x4007d3)#1 0x7f9de8af482f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)#2 0x4006b8 in _start (addresssanitizer_demo/main+0x4006b8)0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0) freed by thread T0 here:#0 0x7f9de8f37caa in operator delete[](void*) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99caa)#1 0x4007a8 in main (addresssanitizer_demo/main+0x4007a8)#2 0x7f9de8af482f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)previously allocated by thread T0 here:#0 0x7f9de8f376b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)#1 0x400798 in main (addresssanitizer_demo/main+0x400798)#2 0x7f9de8af482f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)SUMMARY: AddressSanitizer: heap-use-after-free ??:0 main Shadow bytes around the buggy address:0x0c287fff9f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c287fff9fc0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd0x0c287fff9fd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9fe0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9ff0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa0x0c287fffa000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fffa010: 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: faHeap right redzone: fbFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack partial redzone: f4Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: fe ==5403==ABORTING上面的 -g 選項也可以用 -ggdb 選項(盡可能的生成 gdb 的可以使用的調(diào)試信息)替換。
$ gcc -fsanitize=address -fno-omit-frame-pointer -O1 -ggdb -o main main.cpp $ ./main ================================================================= ==5495==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44 at pc 0x0000004007d4 bp 0x7fff7014ca10 sp 0x7fff7014ca00 READ of size 4 at 0x61400000fe44 thread T0#0 0x4007d3 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:4#1 0x7fdddb19982f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)#2 0x4006b8 in _start (addresssanitizer_demo/main+0x4006b8)0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0) freed by thread T0 here:#0 0x7fdddb5dccaa in operator delete[](void*) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x99caa)#1 0x4007a8 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:3#2 0x7fdddb19982f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)previously allocated by thread T0 here:#0 0x7fdddb5dc6b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)#1 0x400798 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:2#2 0x7fdddb19982f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)SUMMARY: AddressSanitizer: heap-use-after-free /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:4 main Shadow bytes around the buggy address:0x0c287fff9f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c287fff9fc0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd0x0c287fff9fd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9fe0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9ff0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa0x0c287fffa000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fffa010: 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: faHeap right redzone: fbFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack partial redzone: f4Stack after return: f5Stack use after scope: f8Global redzone: f9Global init order: f6Poisoned by user: f7Container overflow: fcArray cookie: acIntra object redzone: bbASan internal: fe ==5495==ABORTING使用 LLVM/Clang 編譯與上面使用 GCC 編譯基本相同:
$ clang++ -fsanitize=address -fno-omit-frame-pointer -O1 -g -o test main.cpp $ ./test ================================================================= ==5508==ERROR: AddressSanitizer: heap-use-after-free on address 0x61400000fe44 at pc 0x00000050770f bp 0x7ffea20137b0 sp 0x7ffea20137a8 READ of size 4 at 0x61400000fe44 thread T0#0 0x50770e in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:4:10#1 0x7fdeb802382f in __libc_start_main /build/glibc-Cl5G7W/glibc-2.23/csu/../csu/libc-start.c:291#2 0x4192b8 in _start (addresssanitizer_demo/test+0x4192b8)0x61400000fe44 is located 4 bytes inside of 400-byte region [0x61400000fe40,0x61400000ffd0) freed by thread T0 here:#0 0x504c20 in operator delete[](void*) (addresssanitizer_demo/test+0x504c20)#1 0x5076de in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:3:3#2 0x7fdeb802382f in __libc_start_main /build/glibc-Cl5G7W/glibc-2.23/csu/../csu/libc-start.c:291previously allocated by thread T0 here:#0 0x5045a0 in operator new[](unsigned long) (addresssanitizer_demo/test+0x5045a0)#1 0x5076d3 in main /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:2:16#2 0x7fdeb802382f in __libc_start_main /build/glibc-Cl5G7W/glibc-2.23/csu/../csu/libc-start.c:291SUMMARY: AddressSanitizer: heap-use-after-free /home/hanpfei0306/data/MyProjects/addresssanitizer_demo/main.cpp:4:10 in main Shadow bytes around the buggy address:0x0c287fff9f70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =>0x0c287fff9fc0: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd0x0c287fff9fd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9fe0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd0x0c287fff9ff0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa0x0c287fffa000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa0x0c287fffa010: 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: faHeap right redzone: fbFreed heap region: fdStack left redzone: f1Stack mid redzone: f2Stack right redzone: f3Stack partial redzone: f4Stack 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: cb ==5508==ABORTING可以通過 readelf 看一下 GCC 和 LLVM 生成的可執(zhí)行文件有什么差別:
# GCC 生成的可執(zhí)行文件 $ readelf -s -W main | grep asan Symbol table '.dynsym' contains 15 entries:Num: Value Size Type Bind Vis Ndx Name1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __asan_report_load410: 0000000000400650 0 FUNC GLOBAL DEFAULT UND __asan_init_v444: 0000000000000000 0 FILE LOCAL DEFAULT ABS asan_preinit.cc57: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __asan_report_load462: 0000000000400650 0 FUNC GLOBAL DEFAULT UND __asan_init_v469: 0000000000600dd0 8 OBJECT GLOBAL HIDDEN 19 __local_asan_preinit# LLVM 生成的可執(zhí)行文件 $ readelf -s -W test | grep asan3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __asan_default_options16: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __asan_on_error49: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __asan_default_suppressions66: 0000000000426740 466 FUNC GLOBAL DEFAULT 13 __asan_stack_malloc_070: 00000000004269b0 504 FUNC GLOBAL DEFAULT 13 __asan_stack_malloc_1 . . . . . .271: 0000000000428000 134 FUNC GLOBAL DEFAULT 13 __asan_stack_free_8272: 000000000042b1e0 44 FUNC GLOBAL DEFAULT 13 __asan_register_image_globals273: 00000000004d7550 44 FUNC GLOBAL DEFAULT 13 __asan_report_load4_noabort275: 00000000004282a0 134 FUNC GLOBAL DEFAULT 13 __asan_stack_free_9 . . . . . .635: 00000000004d7460 44 FUNC GLOBAL DEFAULT 13 __asan_report_load2636: 00000000004d74f0 44 FUNC GLOBAL DEFAULT 13 __asan_report_load4644: 00000000004d7580 44 FUNC GLOBAL DEFAULT 13 __asan_report_load8 . . . . . .37: 0000000000000000 0 FILE LOCAL DEFAULT ABS asan_allocator.cc.o38: 0000000000419370 254 FUNC LOCAL DEFAULT 13 _ZN6__asanL10RZSize2LogEj39: 0000000000421c90 214 FUNC LOCAL DEFAULT 13 _ZN11__sanitizer20SizeClassAllocator64ILm105553116266496ELm4398046511104ELm0ENS_12SizeClassMapILm17ELm128ELm16EEEN6__asan20AsanMapUnmapCallbackEE15DeallocateBatchEPNS_14AllocatorStatsEmPNS2_13TransferBatchE.isra.3240: 0000000000419470 1241 FUNC LOCAL DEFAULT 13 _ZN6__asan9AsanChunk8UsedSizeEb.part.191162: 0000000000422670 1101 FUNC WEAK HIDDEN 13 _ZN11__sanitizer28SizeClassAllocatorLocalCacheINS_20SizeClassAllocator64ILm105553116266496ELm4398046511104ELm0ENS_12SizeClassMapILm17ELm128ELm16EEEN6__asan20AsanMapUnmapCallbackEEEE6RefillEPS6_m1178: 00000000004d74f0 44 FUNC GLOBAL DEFAULT 13 __asan_report_load41194: 00000000004cff10 57 FUNC GLOBAL HIDDEN 13 _ZN6__asan10AsanTSDGetEv1196: 00000000004d6ea0 8 FUNC GLOBAL DEFAULT 13 __asan_get_report_sp1202: 00000000004a75e0 63 FUNC GLOBAL HIDDEN 13 _ZN6__asan13SetThreadNameEPKc1212: 00000000004d7b50 96 FUNC GLOBAL DEFAULT 13 __asan_load1_noabort . . . . . .2202: 00000000004d9780 96 FUNC GLOBAL DEFAULT 13 __asan_init2208: 000000000041a720 2468 FUNC GLOBAL HIDDEN 13 _ZN6__asan22FindHeapChunkByAddressEm2219: 00000000004d9800 7 FUNC GLOBAL HIDDEN 13 _ZN6__asan20GetMallocContextSizeEv . . . . . .3790: 0000000000419ac0 19 FUNC GLOBAL HIDDEN 13 _ZN6__asan13AsanChunkView11IsAllocatedEv3796: 00000000004d08e0 97 FUNC GLOBAL HIDDEN 13 _ZN6__asan25ThreadNameWithParenthesisEPNS_17AsanThreadContextEPcm主要看兩個可執(zhí)行文件中都有的符號 __asan_report_load4 和 __asan_init,可以看到 GCC 生成的可執(zhí)行文件動態(tài)鏈接 ASan 庫,LLVM 靜態(tài)鏈接。
符號化輸出
AddressSanitizer 收集如下事件的調(diào)用棧:
- malloc 和 malloc
- 線程創(chuàng)建
- 失敗
malloc 和 malloc 發(fā)生的相對頻繁,且它對于快速解開調(diào)用棧非常重要。AddressSanitizer 使用一個依賴幀指針的簡單的 unwinder。
如果不關(guān)心 malloc/free 調(diào)用棧,簡單地完全禁用(使用 malloc_context_size=0 運行時標記)unwinder。
每個棧幀需要被符號化(當(dāng)然,如果二進制文件編譯時帶有調(diào)試信息)。給定一臺 PC,我們需要輸出:
#0xabcdf function_name file_name.cc:1234AddressSanitizer 使用 Clang 包中的 llvm-symbolizer 符號化棧追蹤信息(注意理想的 llvm-symbolizer 版本必須與 ASan 運行時庫匹配)。為了使 AddressSanitizer 符號化它的輸出,需要設(shè)置 ASAN_SYMBOLIZER_PATH 環(huán)境變量指向 llvm-symbolizer 二進制文件,或確保 llvm-symbolizer 在 $PATH 中:
$ ASAN_SYMBOLIZER_PATH=/usr/local/bin/llvm-symbolizer ./a.out ==9442== ERROR: AddressSanitizer heap-use-after-free on address 0x7f7ddab8c084 at pc 0x403c8c bp 0x7fff87fb82d0 sp 0x7fff87fb82c8 READ of size 4 at 0x7f7ddab8c084 thread T0#0 0x403c8c in main example_UseAfterFree.cc:4#1 0x7f7ddabcac4d in __libc_start_main ??:0 0x7f7ddab8c084 is located 4 bytes inside of 400-byte region [0x7f7ddab8c080,0x7f7ddab8c210) freed by thread T0 here:#0 0x404704 in operator delete[](void*) ??:0#1 0x403c53 in main example_UseAfterFree.cc:4#2 0x7f7ddabcac4d in __libc_start_main ??:0 previously allocated by thread T0 here:#0 0x404544 in operator new[](unsigned long) ??:0#1 0x403c43 in main example_UseAfterFree.cc:2#2 0x7f7ddabcac4d in __libc_start_main ??:0 ==9442== ABORTINGllvm-symbolizer 符號化工具屬于 llvm 包,Ubuntu 下具體的安裝方法可以參考 LLVM Debian/Ubuntu nightly packages。
如果上面的方法不起作用,可以使用一個單獨的腳本來離線地符號化結(jié)果(在線的符號化可以通過設(shè)置 ASAN_OPTIONS=symbolize=0,或者設(shè)置一個空的 ASAN_SYMBOLIZER_PATH 環(huán)境變量($ export ASAN_SYMBOLIZER_PATH=)來強制禁用):
$ ASAN_OPTIONS=symbolize=0 ./a.out 2> log $ projects/compiler-rt/lib/asan/scripts/asan_symbolize.py / < log | c++filt ==9442== ERROR: AddressSanitizer heap-use-after-free on address 0x7f7ddab8c084 at pc 0x403c8c bp 0x7fff87fb82d0 sp 0x7fff87fb82c8 READ of size 4 at 0x7f7ddab8c084 thread T0#0 0x403c8c in main example_UseAfterFree.cc:4#1 0x7f7ddabcac4d in __libc_start_main ??:0 ...這個腳本接收一個可選的參數(shù) -- a file prefix。子串 .*prefix 將被從文件名中移除。
上面的 c++filt 用于解函數(shù)名的符號重組,這也可以通過給 asan_symbolize.py 腳本添加 -d 參數(shù)來完成。
在 OS X 上可能需要對二進制文件運行 dsymutil 以在 AddressSanitizer 報告中獲得 file:line 信息。
還可以引入自己的棧追蹤格式,使用 stack_trace_format 運行時標記完成。例如:
% ./a.out...#0 0x4b615d in main /home/you/use-after-free.cc:12:3... % ASAN_OPTIONS='stack_trace_format="[frame=%n, function=%f, location=%S]"' ./a.out...[frame=0, function=main, location=/home/you/use-after-free.cc:12:3]AddressSanitizer 算法
簡單的版本
運行時庫替換 malloc 和 free 函數(shù)。把 malloc 分配的內(nèi)存區(qū)域(紅色區(qū)域)附近放入一些特定的字節(jié)(使中毒)。把 free 的內(nèi)存放入隔離區(qū),并且也放入一些特定的字節(jié)(使中毒)。程序中的每次內(nèi)存訪問由編譯器以下面的方式做一個轉(zhuǎn)換。
之前:
之后:
if (IsPoisoned(address)) {ReportError(address, kAccessSize, kIsWrite); } *address = ...; // or: ... = *address;棘手的部分是如何把 IsPoisoned 實現(xiàn)的高效,且 ReportError 緊湊。同時,對某些訪問的插樁可能被證明是冗余的。
內(nèi)存映射
虛擬地址空間被分割為 2 個互斥的類別:
- 主應(yīng)用內(nèi)存(Mem):這種內(nèi)存由常規(guī)的應(yīng)用代碼使用。
- 陰影內(nèi)存(Shadow):這種內(nèi)存包含陰影值(或元數(shù)據(jù))。陰影和主應(yīng)用程序內(nèi)存之間存在對應(yīng)關(guān)系。在主內(nèi)存中 使中毒 一個字節(jié)意味著在對應(yīng)的陰影區(qū)寫入一些特殊的值。
這兩種類別的內(nèi)存應(yīng)該以陰影內(nèi)存(MemToShadow)可以被快速計算出來的方式進行組織。
編譯器執(zhí)行的插樁如下:
shadow_address = MemToShadow(address); if (ShadowIsPoisoned(shadow_address)) {ReportError(address, kAccessSize, kIsWrite); }映射
AddressSanitizer 把 8 個字節(jié)的應(yīng)用內(nèi)存映射為 1 個字節(jié)的陰影內(nèi)存。
對于任何 8 字節(jié)對齊的應(yīng)用內(nèi)存只有 9 個不同的值:
- qword 中的所有 8 字節(jié)是未中毒的(比如,可尋址)。陰影值為 0。
- qword 中的所有 8 字節(jié)是中毒的(比如,不可尋址)。陰影值為負數(shù)。
- 起始的 k 字節(jié)是未中毒的,其余的 8-k 字節(jié)是中毒的。陰影值為 k。這主要由 malloc 的行為保證,即 malloc 總是返回 8 字節(jié)對齊的內(nèi)存塊。一個 qword 對齊的不同字節(jié)具有不同狀態(tài)的僅有的情況是 malloc 的區(qū)域的尾部。比如,如果我們調(diào)用 malloc(13),我們將擁有一個完整的未中毒的 qword 和一個開頭 5 字節(jié)未中毒的 qword。
插樁看起來像下面這樣:
byte *shadow_address = MemToShadow(address); byte shadow_value = *shadow_address; if (shadow_value) {if (SlowPathCheck(shadow_value, address, kAccessSize)) {ReportError(address, kAccessSize, kIsWrite);} } // Check the cases where we access first k bytes of the qword // and these k bytes are unpoisoned. bool SlowPathCheck(shadow_value, address, kAccessSize) {last_accessed_byte = (address & 7) + kAccessSize - 1;return (last_accessed_byte >= shadow_value); }MemToShadow(ShadowAddr) 落入不可尋址的 ShadowGap 區(qū)域。因此,如果程序試圖直接訪問陰影區(qū)域中的內(nèi)存位置,它將崩潰。
64-bit
Shadow = (Mem >> 3) + 0x7fff8000;| [0x02008fff7000, 0x10007fff7fff] | HighShadow |
| [0x00008fff7000, 0x02008fff6fff] | ShadowGap |
| [0x00007fff8000, 0x00008fff6fff] | LowShadow |
| [0x000000000000, 0x00007fff7fff] | LowMem |
32 bit
Shadow = (Mem >> 3) + 0x20000000;| [0x28000000, 0x3fffffff] | HighShadow |
| [0x24000000, 0x27ffffff] | ShadowGap |
| [0x20000000, 0x23ffffff] | LowShadow |
| [0x00000000, 0x1fffffff] | LowMem |
超緊湊的陰影區(qū)
使用更緊湊的陰影區(qū)內(nèi)存也是可能的,比如:
Shadow = (Mem >> 7) | kOffset;還在實驗中。
報告錯誤
ReportError 可以被實現(xiàn)為一個調(diào)用(當(dāng)前默認就是這樣),但也有一些其它的,稍微更加高效和/或更加緊湊的方案。此刻默認的行為是:
- 把失敗地址拷貝到 %rax (%eax)
- 執(zhí)行 ud2 (產(chǎn)生 SIGILL)
- 在 ud2 之后的一個字節(jié)指令中編碼訪問類型和大小。整體上這 3 個指令需要 5-6 個字節(jié)的機器碼。
僅使用單個指令(比如 ud2)也是可能的,但這需要在運行時庫中有一個完整的反匯編器(或一些其它的 hacks)。
棧
為了捕獲棧溢出,AddressSanitizer 插樁的代碼像這樣:
原始的代碼:
void foo() {char a[8];...return; }插樁后的代碼:
void foo() {char redzone1[32]; // 32-byte alignedchar a[8]; // 32-byte alignedchar redzone2[24];char redzone3[32]; // 32-byte alignedint *shadow_base = MemToShadow(redzone1);shadow_base[0] = 0xffffffff; // poison redzone1shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'shadow_base[2] = 0xffffffff; // poison redzone3...shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison allreturn; }插樁的代碼示例(x86_64)
# long load8(long *a) { return *a; } 0000000000000030 <load8>:30: 48 89 f8 mov %rdi,%rax33: 48 c1 e8 03 shr $0x3,%rax37: 80 b8 00 80 ff 7f 00 cmpb $0x0,0x7fff8000(%rax)3e: 75 04 jne 44 <load8+0x14>40: 48 8b 07 mov (%rdi),%rax <<<<<< original load43: c3 retq 44: 52 push %rdx45: e8 00 00 00 00 callq __asan_report_load8 # int load4(int *a) { return *a; } 0000000000000000 <load4>:0: 48 89 f8 mov %rdi,%rax3: 48 89 fa mov %rdi,%rdx6: 48 c1 e8 03 shr $0x3,%raxa: 83 e2 07 and $0x7,%edxd: 0f b6 80 00 80 ff 7f movzbl 0x7fff8000(%rax),%eax14: 83 c2 03 add $0x3,%edx17: 38 c2 cmp %al,%dl19: 7d 03 jge 1e <load4+0x1e>1b: 8b 07 mov (%rdi),%eax <<<<<< original load1d: c3 retq 1e: 84 c0 test %al,%al20: 74 f9 je 1b <load4+0x1b>22: 50 push %rax23: e8 00 00 00 00 callq __asan_report_load4未對齊的訪問
當(dāng)前緊湊的映射將不捕獲未對齊的部分越界訪問:
int *x = new int[2]; // 8 bytes: [0,7]. int *u = (int*)((char*)x + 6); *u = 1; // Access to range [6-9]https://github.com/google/sanitizers/issues/100 中描述了一個可行的方案,但它付出了性能的代價。
運行時庫
Malloc
運行時庫替換 malloc/free,并提供錯誤報告函數(shù),如 __asan_report_load8。
malloc 分配由紅區(qū)圍繞的請求數(shù)量的內(nèi)存。陰影值對應(yīng)的紅區(qū)被下毒,主內(nèi)存區(qū)域的陰影值被清除。
free 用陰影值對整個區(qū)域下毒,并把內(nèi)存塊放入一個隔離區(qū)(這樣在一定時間內(nèi)這個內(nèi)存塊將不會再次被 malloc 返回)。
參考文檔
- Clang AddressSanitizer
- AddressSanitizerAlgorithm
- llvm-symbolizer
- Address Sanitizer 用法
- sanitizers
- AddressSanitizerExampleHeapOutOfBounds
- AddressSanitizerCallStack
總結(jié)
以上是生活随笔為你收集整理的Linux 下的 AddressSanitizer的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QUIC 之类的可靠传输协议
- 下一篇: WebRTC Audio 接收和发送的关