Android内存问题分析方法

memory corruption 相关的 bug 通常很难解,因为内存被破坏和进程 crash 往往不是同时表现出来。而且,内存被破坏后,引发进程 crash 的时间点可能是随机的,这就导致这类问题很难定位。

常见问题

释放了不是通过malloc/new返回的内存指针,或者申请(new)一块内存,使用中改变了内存的大小,释放(delete/delete [])的时候也会有问题。

char* str = new char[5];
strcpy(str, "hello"); // 注意结尾的'\0'会一并复制过去
delete[] str;

strcpy类似的还有memcpy,比如下面这段代码:

char* data = (char *)malloc(sizeof(length) * sizeof(char));
memset(data, 0, length * sizeof(char));

执行到这里的时候可能并不会出错,但是下次申请内存的时候,如果恰好分配了这里的内存块,就会因为内存已被写而报错。

内存破坏的问题主要有以下几类:

  • 内存越界访问
  • 访问未初始化的内存
  • 访问被释放的内存
  • 释放错误的内存
  • 内存重复释放(double free)

调试技术

AddressSanitizer(ASan)

AddressSanitizer (ASan) 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。它与 ValgrindMemcheck 工具)相差不大,但 ASan 具备以下独有特性:

  • 会检测堆栈和全局对象是否有溢出
  • 不检测未初始化的读取和内存泄露
  • 速度更快(Valgrind:20-100x ASan:2-3x)
  • 内存占用空间较少

开启Clang编译

对于之前使用GCC编译的模块,加入以下编译规则即可

LOCAL_CLANG:=true

使用ASan编译需要调试的模块

加入以下编译规则:

LOCAL_SANITIZE:=address

由于不是通过ASan编译的可执行文件无法使用通过ASan编译的共享库,在使用ASan调试编译共享库时,需要考虑不要影响其他程序的的运行。

添加一下规则可以编译ASan(位于system/lib/asan/)和常规(位于system/lib/)两个版本的共享库。

LOCAL_MODULE_RELATIVE_PATH := asan

了解更多

UndefinedBehaviorSanitizer(UBSan)

执行编译时插桩,以检查各种类型的未定义行为。
启用UBSan的方法和ASan类似,在Android.mk中加入对应编译规则即可

LOCAL_SANITIZE := alignment bounds null unreachable integer
LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer

了解更多

Valgrind

Valgrind会显著降低被调试进程的运行速度,需要根据实际情况决定是否使用。

编译

在Android源码目录external/valgrind下,有valgrind源码,mm编译即可。
将编译结果push到系统中,主要有两部分:

  1. /system/bin/valgrind
  2. /system/lib/valgrind/*

执行valgrind ls -l可查看是否成功

也可以下载最新源码交叉编译

下载源码

按照README.android进行编译即可.注意如果是最新的ndk,可以先制作独立的编译工具链,并使用clang编译.
如果编译过程中出现符号重定义的错误,删掉源码中针对Android的重定义即可.

运行

system_server

$ adb shell setprop wrap.system_server "logwrapper valgrind"
$ adb shell stop && adb shell start

app

$ adb shell setprop wrap.app_name "logwrapper valgrind"
$ adb shell am start -a android.intent.action.MAIN -n app_name/.MainActivity

注意property的名称不要超过31个字符。

mediaserver

如果需要在开机启动时就开始追踪 mediaserver 进程,则需要修改init.rc启动参数, 然后编译boot.img,再烧录进系统即可。

调试符号

$ adb shell mkdir /data/local/symbols
$ adb push $OUT/symbols /data/local/symbols

Libc malloc debug

集成在 Android libc 中的调试方式,只需要设置相应的系统属性即可。

$ adb shell setprop libc.debug.malloc 10
$ adb shell stop
$ adb shell start
  • 0 默认级别
  • 1 在malloc记录调用栈,分析检测内存泄露
  • 5 在申请后会填充固定的内容,分析检测越界访问
  • 10 在所申请内存的头部和尾部插入特殊字节内容,当内存块被free时进行检查。这也限制了它的检测范围,对于内存块在运行时越界被破坏这种情况就不会被察觉到。

默认是对所有进程进行检测, Android 7.0 之后可以指定进程:

$ adb shell setprop libc.debug.malloc.options backtrace
$ adb shell setprop libc.debug.malloc 1
$ adb shell setprop libc.debug.malloc.program /system/bin/cameraserver

关于libc.debug.malloc.options更多选项可以参考源码目录中的bionic/libc/malloc_debug/README.md

crash dump

debuggerd在Android发生崩溃时,会生成一个tombstone文件,保存在/data/tombstones/目录。
tombstone文件包含所有线程(不仅仅是崩溃线程)的回溯、浮点寄存器、原始堆栈转储,以及寄存器中地址周围的内存转储。还包括完整的内存映射。

可以使用development/scripts/stack分析tombstone文件以获取更多符号相关信息。

system/core/debuggerd/中包含crasher/crasher64,可以根据命令行参数产生各种崩溃,用于分析进程调用等信息。

编译选项

-fstack-protector-fstack-protector-all

gcc编译选项,启用堆栈保护,可以用于分析检测栈溢出相关错误。

gdb

coredump

通过coredump文件,可以查看分析crash时进程的调用栈、参数等信息。系统在crash时是否生成coredump文件主要依赖两个属性:coredump file size以及coredump file path。可以通过ulimit -c查看coredump file size的设置,通过cat /proc/sys/kernel/core_pattern查看coredump file path的设置。

在Android上需要修改init.rc,在setrlimit 13 40 40后面添加:

setrlimit 4 -1 -1
mkdir /data/coredump/ 0777 system system
write /proc/sys/kernel/core_pattern /data/core/core.%E.%e.%p
write /proc/sys/fs/suid_dumpable 1

设置需要转储的内存块,默认是0x23,即00100011,第0、1、5位。0x27表示转储所有可以转储的数据。

$ adb shell echo 0x27 > /proc/XXXPIDXXX/coredump_filter
  • (bit 0) 匿名私有内存段
  • (bit 1) 匿名共享内存段
  • (bit 2) file-backed 私有内存段
  • (bit 3) file-bakced 共享内存段
  • (bit 4) ELF 文件映射,只有在bit 2 复位的时候才起作用
  • (bit 5) 大页面私有内存
  • (bit 6) 大页面共享内存

attach

如果希望进程崩溃时能暂停住,以便通过gdb附加上去查看调用栈等信息,可以设置下面的系统属性:

$ adb shell setprop debug.db.uid 999999                 # <= M
$ adb shell setprop debug.debuggerd.wait_for_gdb true   # > M

更多内存调试工具可以参考这里

你可能感兴趣的:(Android内存问题分析方法)