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
) 是一种基于编译器的快速检测工具,用于检测原生代码中的内存错误。它与 Valgrind
(Memcheck
工具)相差不大,但 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到系统中,主要有两部分:
- /system/bin/valgrind
- /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
更多内存调试工具可以参考这里