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阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容