Introduction
memory corruption是最难搞的问题之一,这是因为:
1. 破坏内存的地方和内存破坏的结果常常是分离的,所以很难定位。
2. 有些破坏在特定case下才会出现,不好复现。
Memory corruption errors大概可以分成以下几类:
1. 使用未初始化的内存
2. 使用非法内存(不是自己申请的内存,使用空指针,野指针)
3. buffer overflow,内存越界读写
4. 错误的heap管理问题:申请的内存没有free,free后又使用,两次free
5. Kernel driver 冲应用内存,特别是cma这种方式
fatal signal
memory corruption 会引起进程crash,触发的signal有以下几种:
SIGSEGV (segment fault)
程序读写它没有分配的内存,或者写只读内存,会抛出 SIGSEGV
非法内存访问(段错误), 常见的case:- 空指针解引用
- 引用非法地址
- 防问无权限的地址
- 写只读的内存(比如code段)
SIGBUS (bus error)
通常非法指针解引用,和SIGSEGV的区别在于:
SIGSEGV indicates an invalid access to valid memory,
SIGBUS indicates an access to an invalid address.
SIGBUS通常是解引用未对齐的pointer,比如解引用一个four-word integer但是地址不能被4整除。也就是未对齐的数据访问导致SIGILL(illegal instruction)
非法程序映像,例如非法指令。SIGILL通常表明,可执行文件被破坏了,或者程序在执行data。后一种情况,比较常见的是对函数指针非法赋值;或者是stack被破坏;或者是stack overflow。
debugger
https://en.wikipedia.org/wiki/Memory_debugger
上面的wiki里,列举了各种debug方法。一种是在编译时检查,另一种是运行时检测。
对于android memory corruption的debugger方法大概有:
- Asan: address sanitizer
- UbSan: undefined Behavior Sanitizer
- Valgrind
- Libc malloc debug (setprop libc.debug.malloc 10)
这些方法只对native 层有用,可以检测null pointer, buffer overflow, free后又使用等。但是对于java heap 则无能为力。java heap出问题的概率非常小,android 官网上推荐可以gc verify 和 jni check。
https://source.android.com/devices/tech/dalvik/gc-debug
对于比较复杂的case,可以打开coredump分析。
asan
http://clang.llvm.org/docs/AddressSanitizer.html
https://source.android.com/devices/tech/debug/asan.html
introduction
LLVM是用于构建Android的编译器,其中包含多个组件可以进行静态和动态分析。Sanitizers就是其中之一。sanitizers通常指addressSanitizer和UndefinedBehaviorSanitizer。
Ascan是用来检测C/C++程序memory问题的编译工具,是在运行时检测的。由一个编译器 (external/clang)和一个运行时 library (external/compile-rt/lib/asan)组成
可检测的memory问题包括:
- Out-of-bounds memory access
- Double free
- use after free
相比于valgrind,它具有以下特点:
- 检测stack和global objects的overflow问题
- 内存泄漏无能为力
- 比valgrind快(慢两三倍,valgrind要慢20~100倍)
- 内存消耗更少
usage
1 native process build with Ascan
将下面这两行加到Android.mk文件里
LOCAL_CLANG:=true
LOCAL_SANITIZE:=address
2 Share library build with Ascan
将下面这三行加到Android.mk文件里
LOCAL_CLANG:=true
LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan
这会将共享库编到 /system/lib/asan而不是 /system/lib里
3 Native process used asan so lib
可执行程序要使用asan库,指定下面这个环境变量:
LD_LIBRARY_PATH=/system/lib/asan
例如,对于rc文件定义启动的native程序,可以在rc文件里加上。
如何确认进程使用ascan的lib呢?可以查看/proc/$PID/maps里面,是否使用的是/system/lib/asan/**的库。另外,需要关闭selinux
4 asan with app
address Sanitizer java 代码并不能使用,但是可以用来检测JNI 代码。需要编译的是 /system/bin/app_process(32|64)
1) app_process mk里打开asan
frameworks/base/cmds/app_process/Android.mk
LOCAL_CLANG:=true
LOCAL_SANITIZE:=address
2) 设置环境变量
init.zygote(32|64).rc
setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib
setenv ASAN_OPTIONS
allow_user_segv_handler=true
3) Using the wrap property
上面的方法,所有的java程序都会使用Asan. 这样的话,比较费内存。
可以指定一个或几个apk使用Asan,下面的例子,gmail under asan
$ adb root
$ adb shell setenforce 0 # disable SELinux
$ adb shell setprop wrap.com.google.android.gm "asanwrapper"
如果asan发现问题的话,信息会打印到logcat中。
Symbolization
有两种方法,可以将asan的输出定位到源文件和代码行:
- 将llvm-symbolizer 编译到平台里/system/bin/Llvm-symbolizer,这个bin文件源码在:third_party/llvm/tools/llvm-symbolizer
- 用这脚本分析 external/compiler-rt/lib/asan/scripts/symbolize.py
第二种方法能产生更多的信息,因为可以访问主机上的 symbolized libraries.
external/compiler-rt/lib/asan/scripts/symbolize.py < log | c++filt
makefile里加上这两行,可以得到更好的backtrace
LOCAL_CFLAGS:=-fno-omit-frame-pointer
LOCAL_ARM_MODE:=arm
另外,kernel也支持asan,但是kernel要是4.9以后的。
http://stackoverflow.com/questions/24566416/how-do-i-get-line-numbers-in-the-debug-output-with-clangs-fsanitize-address
http://clang.llvm.org/docs/AddressSanitizer.html#symbolizing-the-reports
UBSan
introduction
UBSan在编译时,检测变量的未定义操作。在android上,支持的检测有以下几种:
alignment, bool, bounds, enum, float-cast-overflow, float-divide-by-zero, integer-divide-by-zero, nonnull-attribute, null, return, returns-nonnull-attribute, shift-base, shift-exponent, signed-integer-overflow, unreachable, unsigned-integer-overflow, and vla-bound.
Usage
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_CFLAGS := -std=c11 -Wall -Werror -O0
LOCAL_SRC_FILES:= sanitizer-status.c
LOCAL_MODULE:= sanitizer-status
LOCAL_MODULE_TAGS := debug
LOCAL_SANITIZE := alignment bounds null unreachable integer LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer
include $(BUILD_EXECUTABLE)
UBSan shortcuts
有两个shortcuts: integer 和 default-ub,可以同时enable多个sanitizers。
integer enables integer-divide-by-zero, signed-integer-overflow and unsigned-integer-overflow.
default-ub enables the checks that have minimal compiler performance issues: bool, integer-divide-by-zero, return, returns-nonnull-attribute, shift-exponent, unreachable and vla-bound.
integer sanitizer 可以用在 SANITIZE_TARGET 和 LOCAL_SANITIZE中;而 default-ub只能用在 SANITIZE_TARGET中。
Better error reporting
Android.mk文件里:
LOCAL_SANITIZE:=integer
LOCAL_SANITIZE_DIAG:=integer
其中LOCAL_SANITIZE 在build时,enable UBSan。LOCAL_SANITIZE_DIAG 打开特定sanitizer的diagnostic mode 。这两个可以设成不同的值,但是此时只有 LOCAL_SANITIZE 是 enabled。如果某种sanitizer没有在LOCAL_SANITIZE中指定,但是在 LOCAL_SANITIZE_DIAG中指定,那么检查不会enabled。
Valgrind
introduction
http://valgrind.org/docs/manual/mc-manual.html
valgrind是用来检查内存问题的工具,memcheck是默认打开的,可以检测 C/C++程序以下几种内存问题:
- Accessing memory you shouldn’t, e.g. overrunning and underrunning heap blocks, overrunning the top of the stack, and accessing memory after it has been freed.
- Using undefined values, i.e. values that have not been initialised, or that have been derived from other undefined values.
- Incorrect freeing of heap memory, such as double-freeing heap blocks, or mismatched use of malloc/new/new[] versus free/delete/delete[]
- Overlapping src and dst pointers in memcpy and related functions.
- Passing a fishy (presumably negative) value to the size parameter of a memory allocation function.
- Memory leaks.
当然使用valgrind会带来以下问题:
- 程序的内存使用量大大增加
- 程序的运行速度大大减慢(通常会慢20~100倍)
use valgrind in android
https://source.android.com/devices/tech/debug/valgrind
有两点需要注意,一个建议使用eng版本,另一个是valgrind下运程程序会超级慢。这实际上也就限制了它的使用场景,只有比较单纯的环境下才能用。
1 Build valgrind
$ mmm -j6 external/valgrind
2 Push Valgrind to the device,connect adb
$ adb root
$ adb remount
$ export ANDROID_PRODUCT_OUT=$ANDROID_PRODUCT_OUT
$ adb sync
3 Set up the temporary directory:
$ adb shell mkdir /data/local/tmp
$ adb shell chmod 777 /data/local/tmp
4 disable selinux
$ adb shell setenforce 0
5 push symbols
$ adb shell mkdir /data/local/symbols
$ adb push $OUT/symbols /data/local/symbols
$ adb push $ANDROID_PRODUCT_OUT/symbols /data/local/symbols
6 使用valgrind 运行需要debug的程序
对于native 程序,开机启动的services,需要修改init.*.rc文件。例如下面的
service example /system/bin/foo --arg1 --arg2
change to:
service example /system/bin/logwrapper /system/bin/valgrind /system/bin/foo --arg1 --arg2
对与java程序,使用wrap property,例如calculator:
$ adb shell setprop wrap.com.android.calculator2 "TMPDIR=/data/data/com.android.calculator2 logwrapper valgrind"
kill calculator and restart it.
下面的例子是system server使用valgrind:
adb root
adb shell setenforce 0
adb shell chmod 777 /data/local/tmp
adb shell setprop wrap.system_server "logwrapper valgrind"
adb shell stop && adb shell start
valgrind other option
memory check选项,valgrind是默认打开。valgrind还有其它的一些选项,可以在选择打开,方法是在valgrind命令后面,增加这些选项和参数。
valgrind --error-limit=no --trace-children=yes --sigill-diagnostics=no --extra-debuginfo-path=/data/local/symbols/
https://source.android.com/devices/tech/dalvik/gc-debug.html#valgrind
Problems
在有些android版本上, system_server在valgrind下跑不起来,有两个问题。
- 有些指令识别不了
- 找不到system server这个class, path不对
其它apk在valgrind下跑,会出现anr,watchdog kill system server等问题。总之valgrind并不太好用。
Libc malloc debugger
Libc malloc debugger是android libc里自带的,也是最简单易用的corruption debugger了。使用方法,编个userdebug或eng版本,同时设上这两上property:
setprop libc.debug.malloc 10
setprop libc.debug.malloc.program nativeprocess
虽然简单易用,但是这种方法也有自身的限制。检测方法是在申请buffer的头部和尾部都写入了特殊字符,但是只有当buffer被free,或者是realloc时,才会检查是否发生了变化。很多情况下,当某块buffer被破坏时,但是crash发生是检查前,就无能为力了。
Coredump
introduction
Coredump 是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态等其他信息写出的一个磁盘文件。这种信息往往用于调试。
1 查看系统是否打开了coredump
ulimit -c
如果返回0,则表明功能关闭,不会生成core文件
2 打开coredump的方法\
- 命令行方式 ulimit -c 1024 , 限制core文件大小1024K。ulimit -c unlimited 不加限制
- linux下,修改配置文件, /etc/profile 加上 ulimit -S -c unlimited > /dev/null 2>&1
- 修改init.rc文件
3 core 文件的保存路径和文件名格式
core文件路径和格式在:/proc/sys/kernel/core_pattern
core 文件格式如下:
%p – insert pid into filename
%u – insert current uid into filename
%g – insert current gid into filename
%s – insert signal that caused the coredump into the filename
%t – insert UNIX time that the coredump occurred into filename
%h – insert hostname where the coredump happened into filename
%e – insert coredumping executable name into filename
4 要生成core文件,要向进程发送SIGSEGV 信号
Open coredump in android
Coredump文件都比较大,android系统上默认是关掉的。
1 native 程序 init.rc 修改如下
diff --git a/rootdir/init.rc b/rootdir/init.rc
index 179316c..d32cb85 100755
--- a/rootdir/init.rc
+++ b/rootdir/init.rc
@@ -267,6 +267,9 @@ on boot
# set RLIMIT_NICE to allow priorities from 19 to -20
setrlimit 13 40 40
+ setrlimit 4 -1 -1
+ write /proc/sys/kernel/core_pattern "/data/core-%e.%p.%u"
+ write /proc/sys/fs/suid_dumpable 1
2 java程序
Android zygote和system server里关掉了core dump。
TODO find the open way.
coredump debug
得到coredump文件后,可以使用gdb 进行debug,查看当时的内存信息。
arm-eabi-gdb --core=corefile (arm-eabi-gdb 在ndk里有)
方法如下:
1 将core文件和symbols放到同一目录下,symbols包括可执行程序和so库文件。在out/target/product/*/symbols/目录下
2 运行gdb,要用arm-eabi-gdb
arm-eabi-gdb
3 设置搜索路径(在gdb命令下)
set solib-search-path system/lib
4 file 命令载入主执行文件
file system/bin/execfile
5 core-file 载入Coredump文件
core-file coredumpfile
- 查看so库的加载路径是否正确可使用
info sharedlibrary
7 使用bt 命令,查看crash时的backtrace. backtrace中,每个函数都有一个frame number。可以使用frame [num] 查看特定的stack frame. 使用list查看源码,info locals 查看局部变量。
在设置了搜索路路径后,最好先用file命令载入主执行文件,再用core命令载入Coredump文件,这样才能保证正确载入库的符号表。
problem
Coredump不是特别有用,由于编译优化,局部变量都看不到了。只能看一些全局变量。而且java层,coredump默认是关闭的。对于,特别复杂的case,可以使用coredump。
如何关掉android 编译优化呢?
可以将下面这一行,加到make file里,可以关闭掉编译优化。
LOCAL_CFLAGS += -O0
Summary
另外,倒版本是终极大招。
但是如果是driver冲了上层内存,就比较麻烦。如果被踩踏的位置比较固定,位置固定是指物理地址固定或者在特定范围;可以将分配这块物理地址的callstack dump出来,找到怀疑点。也可以将这个固定的物理地址,设成只读的,如果有人写它的话,就可以抓到。
但是如果破坏位置是随机的,这种情况是最难定位。一种可行的,比较暴力的debug方法是随机地申请内存,将这申请到的内存设成只读的。那么如果踩踏到这些只读内存时,就可以抓到破坏者。当申请的只读内存比较大的时候,抓到的概率也是比较高的。
如何是硬件DMA冲掉应用内存造成crash的问题,没有好的手段可以debug,很多时候问题的解决得靠猜测以及运气,并且需要反反复复各种测试。