美文网首页
C/C++内存和crash分析

C/C++内存和crash分析

作者: NullBugs | 来源:发表于2018-11-27 14:42 被阅读0次

    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)
      待续


    相关文章

      网友评论

          本文标题:C/C++内存和crash分析

          本文链接:https://www.haomeiwen.com/subject/kzygqqtx.html