C/C++內存和crash分析

C/C++內存和crash分析

標簽(空格分隔): C/C++ native內存 段錯誤 native內存泄露 C++Crash C內存泄露


  • 問題解決思路

    • 根因分析-簡單問題
      追根究底(Why):APP crash --> 內存不足 --> 內存泄露 --> 代碼問題 --> 設計不合理 --> 水平太菜(開個玩笑)
    • 麥肯錫的七步程式-復雜問題
      如何提升我們APP的performance:
  • 問題解決技巧

    • log定位;
    • 堆棧/性能等現場分析;
    • 工具
    • 其他
    • 猜測-最高境界

    為什么"猜測是最高境界"

    • 代碼的理解和熟悉程度極高(眼中無碼,心中有碼);
    • 思路清晰,胸有成竹(眼中無路,心中有路);
    • 極強的理解能力,能夠從現象到本質的快速還原;
    • 綜合能力極強(手上無劍,心中有劍);
  • 分析工具-addressSanitize

    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


asan_use_after_free.png

memory-leaks:

asan_memory_leaks.jpg

  • 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會分析多線程之間可能的同步問題,并提示多線程可能造成的問題:

muti_thread.png
  • 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實現
      1. 在工程下build.gradle聲明變量
      project.ext {
            useASAN = true
            ndkDir = properties.getProperty('ndk.dir')
      }
      
      1. 在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)
      
      1. 在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());
      }
      
png.png
  • 使用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

執行結果:


valgrind_memory_check.png
  • 分析工具-addr2line
    使用addr2line對應debug的庫

    addr2line.png

  • 分析工具-Objdump
    待續

  • 分析工具-ndk-stack(android)
    待續


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容