C/C++內存和crash分析
標簽(空格分隔): C/C++ native內存 段錯誤 native內存泄露 C++Crash C內存泄露
-
問題解決思路
- 根因分析-簡單問題
追根究底(Why):APP crash --> 內存不足 --> 內存泄露 --> 代碼問題 --> 設計不合理 --> 水平太菜(開個玩笑) - 麥肯錫的七步程式-復雜問題
如何提升我們APP的performance:
- 根因分析-簡單問題
-
問題解決技巧
- log定位;
- 堆棧/性能等現場分析;
- 工具
- 其他
- 猜測-最高境界
為什么"猜測是最高境界"
- 代碼的理解和熟悉程度極高(眼中無碼,心中有碼);
- 思路清晰,胸有成竹(眼中無路,心中有路);
- 極強的理解能力,能夠從現象到本質的快速還原;
- 綜合能力極強(手上無劍,心中有劍);
-
分析工具-addressSanitize
- github地址:https://github.com/google/sanitizers
AddressSanitizer (aka ASan) is a memory error detector for C/C++. It finds: Use after free (dangling pointer dereference) Heap buffer overflow Stack buffer overflow Global buffer overflow Use after return Use after scope Initialization order bugs Memory leaks
??This tool is very fast. The average slowdown of the instrumented program is ~2x (see AddressSanitizerPerformanceNumbers).
??The tool consists of a compiler instrumentation module (currently, an LLVM pass) and a run-time library which replaces the malloc function.-
支持平臺
支持android,IOS,linux等大多數平臺
asan_support.png
-
實現原理
ASAN原理時在mem前后插裝,插樁主要是針對在llvm編譯器級別對訪問內存的操作(store,load,alloca等),將它們進行處理。動態運行庫主要提供一些運行時的復雜的功能(比如poison/unpoison shadow memory)以及將malloc,free等系統調用函數hook住。其實該算法的思路很簡單,如果想防住Buffer Overflow漏洞,只需要在每塊內存區域右端(或兩端,能防overflow和underflow)加一塊區域(RedZone),使RedZone的區域的影子內存(Shadow Memory)設置為不可寫即可(ASAN采用的是內存映射的方式,速度較快)。具體如下:
asan_redzone.png
-
使用方式
addresssanitize不需要修改代碼,只需要修改CMAKE,但是執行memory-leaks檢測的時候必須去掉 -O1的的優化,否則不會提示錯誤;
addresssanitize可能不會提示錯誤,此時可以修改~/.bashrc,增加響應的option:
export ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1:handle_segv=1 :fast_unfind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1
cmake增加address flag:
CMAKE_MINIMUM_REQUIRED(VERSION 3.2)
PROJECT(SanitizerTest)
#this is for memory leaks flags, if -O1, memory check is no effective
set(CMAKE_CXX_FLAGS "-g -fsanitize=address -fno-omit-frame-pointer")
ADD_EXECUTABLE(main src/sanitize_test.cpp src/main.cpp)
示例代碼:
#include "sanitize_test.h"
#include <string>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
int use_aexport ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1:handle_segv=1:fast_unfind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1fter_free(){
int* array = new int[100];
delete[] array;
return array[1]; //boom
}
int heap_buffer_overflow(){
int* array = new int[100];
array[0] = 0;
int res = array[1 + 100];
delete[] array;ad
return res;
}
int stack_buffer_overflow(){
int stack_array[100];
stack_array[1] = 0;
return stack_array[1001];//boom
}
int global_array[100] = {0};
int global_buffer_overflow(){
return global_array[10001];
}
int *ptr;
__attribute__((noinline))
int use_after_return_impl(){
int local[100];
ptr = &local[0];
return 0;
}
int use_after_return(){
use_after_return_impl();
return ptr[0];
}
volatile int *p = 0;
int use_after_scope(){
{
int x = 0;
p = &x;
}
*p = 5;
return 0;
}
int init_order_bugs(){
}
void *ptr_leak;
int memory_leaks(){
ptr_leak = malloc(10);
ptr_leak = 0;//memory is leaked here
return 0;
}
編譯執行:
jazywang$ cmake..
jazywang$ make
jazywang$ ./main
執行結果:
use after free
memory-leaks:
-
ThreadCheck
addressSanitize可以定位thread之間的讀寫同步和沖突,常見的例子如下:
#include <pthread.h>
#include <stdio.h>
int Global;
void *Thread1(void *x){
Global++;
return NULL;
}
void *Thread2(void *x){
Global--;
return NULL;
}
int main(){
pthread_t t[2];
pthread_create(&t[0], NULL, Thread1, NULL);
pthread_create(&t[1], NULL, Thread2, NULL);
pthread_join(t[0], NULL);
pthread_join(t[1], NULL);
}
執行編譯腳本:
clang src/thread_sanitize.cpp -fsanitize=thread -fPIE -pie -g
./a.out
threadsanitize會分析多線程之間可能的同步問題,并提示多線程可能造成的問題:
-
Android
ASAN在Android使用有兩種方式,第一種是編譯出可執行文件,然后push到手機當做一個可執行程序;
此方式和linux的方式基本相同,請參考linux上的方式執行NDK下的asan_device_setup,風險:最差的情況可能導致unbootable;
此方式重點執行asan_device_setup腳本,然后編譯,具體請參考,需要root手機,小心把手機起不來了(嘎嘎)
GitHub:https://github.com/google/sanitizers/wiki/AddressSanitizerOnAndroid-
直接嵌入到APP中(通過Android studio),在gradle腳本中通過wrap.sh的方式完成或者在
終端- AddressSanitize要求NDK17以上,android版本7.0以上
- Code實現
- 在工程下build.gradle聲明變量
project.ext { useASAN = true ndkDir = properties.getProperty('ndk.dir') }
- 在CMake中增加ASAN支持
if(USEASAN) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address") set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} -fsanitize=address") endif(USEASAN)
- 在app的build.gradle下增加wrap.sh以及copy ndk下的libasan.xxx.xx.so
packagingOptions { doNotStrip "**.so" if (rootProject.ext.useASAN && abiFiltersForWrapScript) { def exclude_abis = ["armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"] .findAll { !(it in abiFiltersForWrapScript) } .collect { "**/" + it + "/wrap.sh" } excludes += exclude_abis } } if (rootProject.ext.useASAN) { sourceSets { main { jniLibs { srcDir { "wrap_add_dir/libs" } } resources { srcDir { "wrap_add_dir/res" } } } } }
tasks.whenTaskAdded { task -> if (task.name.startsWith('generate')) { if(rootProject.ext.useASAN) task.dependsOn createWrapScriptAddDir } } task deleteASAN(type: Delete) { delete 'wrap_add_dir' } clean.dependsOn(deleteASAN) static def writeWrapScriptToFullyCompileJavaApp(wrapFile, abi) { if(abi == "armeabi" || abi == "armeabi-v7a") abi = "arm" if(abi == "arm64-v8a") abi = "aarch64" wrapFile.withWriter { writer -> writer.write('#!/system/bin/sh\n') writer.write('HERE="$(cd "$(dirname "$0")" && pwd)"\n') writer.write('export ASAN_OPTIONS=log_to_syslog=false,allow_user_segv_handler=1\n') // writer.write('export ASAN_OPTIONS=log_to_syslog=true,allow_user_segv_handler=0,fast_unwind_on_malloc=0\n') writer.write('export ASAN_ACTIVATION_OPTIONS=include_if_exists=/data/local/tmp/asan.options.b\n') writer.write("export LD_PRELOAD=\$HERE/libclang_rt.asan-${abi}-android.so\n") writer.write('\$@\n') } } task copyASANLibs(type:Copy) { def libDir = file("$rootProject.ext.ndkDir").absolutePath + "/toolchains/llvm/prebuilt/" for (String abi : SupportedABIs) { def dir = new File("app/wrap_add_dir/libs/" + abi) dir.mkdirs() if(abi == 'armeabi-v7a' || abi == 'armeabi') abi = "arm" if(abi == "arm64-v8a") abi = "aarch64" FileTree tree = fileTree(dir: libDir).include("**/*asan*${abi}*.so") tree.each { File file -> from file into dir.absolutePath } } } task createWrapScriptAddDir(dependsOn: copyASANLibs) { for (String abi : SupportedABIs) { def dir = new File("app/wrap_add_dir/res/lib/" + abi) dir.mkdirs() def wrapFile = new File(dir, "wrap.sh") writeWrapScriptToFullyCompileJavaApp(wrapFile, abi) println "write file " + wrapFile.path } }
- 執行結果
extern "C" JNIEXPORT jstring JNICALL Java_com_yinlib_sanitize_test_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; int a[2] = {1, 0}; int b=a[2]; // out of bound return env->NewStringUTF(hello.c_str()); }
-
使用addr2line對應debug的庫
addr2line.png
-
Android studio 32位庫無法打印出堆棧問題
在android studio上編譯,使用32位庫時,wrap.sh只能提示對應庫,無法打印出堆棧。使用64位時,堆棧如上,此問題估計和具體的系統是64位有關,這里是個坑。
-
總結
addressSanitize的非常適合快速定位內存問題,在自己編譯代碼的過程中只需要增加CMAKE(clang 或者 gcc)的FLAG,使用比較簡單,建議在代碼中調試時增加這種debug的FLAG,及時發現問題. -
分析工具-vrigrind
官網地址 : http://valgrind.org/
Valgrind 是個開源的工具,功能很多。例如檢查內存泄漏工具---memcheck
常用命令:
[options]: 常用選項,適用于所有Valgrind工具
-tool=<name> 最常用的選項。運行 valgrind中名為toolname的工具。默認memcheck。
memcheck ------> 這是valgrind應用最廣泛的工具,一個重量級的內存檢查器,能夠發現開發中絕大多數內存錯誤使用情況,比如:使用未初始化的內存,使用已經釋放了的內存,內存訪問越界等。
callgrind ------> 它主要用來檢查程序中函數調用過程中出現的問題。
cachegrind ------> 它主要用來檢查程序中緩存使用出現的問題。
helgrind ------> 它主要用來檢查多線程程序中出現的競爭問題。
massif ------> 它主要用來檢查程序中堆棧使用中出現的問題。
extension ------> 可以利用core提供的功能,自己編寫特定的內存調試工具
常用memcheck如下:
#include <stdlib.h>
void *p;
int main(){
p = malloc(7);
p = 0;
return 0;
}
memcheck命令:
clang -g src/memory_leak.cpp
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./a.out
執行結果:
-
分析工具-addr2line
使用addr2line對應debug的庫
addr2line.png 分析工具-Objdump
待續分析工具-ndk-stack(android)
待續