文章正文
Malloc Debug
翻译至:https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
Malloc Debug(内存调试)简介
Malloc debug 是一种native层内存问题的方法。他可以帮助我们定位内存损坏、内存泄露、释放后再使用的问题。这个文档描述如何在Android N版本以后的Android系统中使用这个功能。对于老版本的malloc debug的方法见这个链接(https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README_marshmallow_and_earlier.md)。
当malloc debug功能使能后,系统增加了一个Hook层代替正常的内存分配调用。这里主要替换了下面的方法:
1. malloc
2. free
3. calloc
4. realloc
5. posix_memalign
6. memalign
7. aligned_alloc
8. malloc_usable_size
在32位的系统,有两个被废弃的函数也被替换了
1. pvalloc
2. valloc
如果检测到任何的问题,会通过日志的方式呈现出来。
注意:在andoird P版本中,realloc有个小的改变。在以前的版本中,一块内存realloc为小一点的内存时,不会更新相关分配的回溯。从P版本开始,无论指针有没有变化,每一次realooc调用都会改变回溯。
控制malloc debug的行为
malloc debug被单独的选项控制。每个选项都可以单独的开关,也可以与其他的任意选项组合。
选项描述
front_guard[=SIZE_BYITS]
这个选项允许在分配的数据前放置一块小的buffer。This is an attempt to find memory corruption occuring to a region before the original allocation. 在第一次分配时,这个前防护已固定的数据0xaa填充。 当分配的内存被释放的时候,就会验证这一块是否被修改了。如果这个前防卫的任何数据被改变了,就会在log中报出error,并指明是哪些bytes被修改了。
如果backtrace选项使能了的话,错误的信息中就会包含分配地址的backtrace信息.
如果SIZE_BYTES被指定了的话, 这就表明这恶个前防卫块的大小。默认的大小是32 bytes,最大16384 bytes。 为了确保字节对齐,在32位系统中SIZE_BYTES 会被填充为8 bytes的倍数,在64位系统中是16 bytes的倍数 。
这个选项的报错时,会在log中除了原始分配的信息还包含特定的头信息。
例如如下的报错:
04-10 12:00:45.621 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 SIZE 100 HAS A CORRUPTED FRONT GUARD 04-10 12:00:45.622 7412 7412 E malloc_debug: allocation[-32] = 0x00 (expected 0xaa) 04-10 12:00:45.622 7412 7412 E malloc_debug: allocation[-15] = 0x02 (expected 0xaa)
rear_guard[=SIZE_BYTES]
这个于front_guard原理一样,在原始分配的后面加上了一块内存,用于检测是否在分配内存的后面发生了内存的损坏。
报错示例如下:
04-10 12:00:45.621 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 SIZE 100 HAS A CORRUPTED REAR GUARD 04-10 12:00:45.622 7412 7412 E malloc_debug: allocation[130] = 0xbf (expected 0xbb) 04-10 12:00:45.622 7412 7412 E malloc_debug: allocation[131] = 0x00 (expected 0xbb)
uard[=SIZE_BYTES]
这个选项表示同时开启前防护和后防护,相当于上面两个的组合。
backtrace[=MAX_FRAMES]
这个选项用于追踪每次分配信息。这个选项会大大减慢内存分配的速度。如果因为开启这个选项导致系统运行太慢,需要减少采集的帧数。需要注意的是在malloc backtrace库中的任何的回溯帧都不会被记录。
MAX_FRAMES参数表示回溯栈的最大层数。默认情况下是6,最大可以被设为256。在P版本以前这个选项在错误信息的log中也会有特定的头信息,但在后面就没有这个信息了。在P版本上这个选项开启后,在进程接收到SIGRTMAX-17(在大多数的Android设备上是signal 47)信号的时候会dump对应的回溯堆栈的数据信息。
dump数据的格式与运行dumpheap -n的格式一致。默认的文件路径是/data/local/tmp/backtrace_heap.PID.txt. 这对于不是从zygote进程fork出来的native进程来说是很游泳的。
backtrace_enable_on_signal[=MAX_FRAMES]
与上一个一样这个选项会捕捉每次分配的调用栈。与上面不同的是这个选项是接收到SIGRTMAX-19这个信号触发,通常在android上是signal 45。当这个选项单独使用的使用的时候,堆栈捕捉是默认不开启的,在接收到signal事件之后开始。当这个选项和backtrace选项一起设置的时候,默认是开启的,在接收到signal之后结束捕捉。
类似backtrace选项,MAX_FRAMES参数表示回溯栈的最大层数。默认情况下是6,最大可以被设为256。在P版本以前这个选项在错误信息的log中也会有特定的头信息,但在后面就没有这个信息了。
backtrace_dump_on_exit
在P版本上,在backtrace选项开启后,在程序退出的时候会dump出对应的堆栈数据。默认的dump文件路径是/data/local/tmp/backtrace_heap.PID.exit.txt.
这个文件的路径可以通过backtrace_dump_prefix选项设置。
backtrace_dump_prefix
在P版本上, 这个选项依赖于backtrace选项,用于设置dump文件的文件名前缀。默认的是/data/local/tmp/backtrace_heap。这个选项设置后, signal 产生的dump文件一般命名为backtrace_dump_prefix.PID.txt。程序退出产生的dump文件的文件名是 backtrace_dump_prefix.PID.exit.txt。
fill_on_alloc[=MAX_FILLED_BYTES]
除了calloc,其他所有的分配调用,在分配之后会使用0xeb填充。当使用realloc扩大分配内存的时候,扩大了的空间也会做填充。如果MAX_FILLED_BYTES被设置,则只填充对应大小的内存。默认情况下是填充整个分配。
fill_on_free[=MAX_FILLED_BYTES]
当一次分配的内存被释放的时候,这个选项开启后,会使用0xef填充。如果MAX_FILLED_BYTES被设置,则只填充对应大小的内存。默认情况下是填充整个分配。
fill[=MAX_FILLED_BYTES]
这个选项相当于同时开启了fill_on_alloc和the fill_on_free 选项
expand_alloc[=EXPAND_BYTES]
这个选项表示每次分配都会加上额外的分配空间。如果参数设置了,会表示分配的时候会扩展多少字节的大小,默认情况是16 bytes,最大可以设置为16384.
free_track[=ALLOCATION_COUNT]
当一个指针被释放的时候,如果开启了这个选项,内存并不会真正的释放,而只是把这个指针加入到了释放列表中。另外,在指针加入到列表后,指针所指的内存都会用0xef填充,在回溯文件中会记录释放的时间。
这个回溯的记录是和backtrace选项不想干的,如果这个选项使能后,记录会自动的进行。默认情况下,最多可以显示16层调用栈。不过这个值可以使用free_track_backtrace_num_frames 选项制定. 当然如果设置为0,这个就相当于关了这个选项。详细描述见下文。
当列表满的时候,会将最前面的分配移除掉,在移除的时候会确认这个移除的指针所指的内存没有任何的变化。在程序结束的时候,剩余在列表中的指针都会验证后释放。
ALLOCATION_COUNT参数表示列表的大小,可以释放多少了指针。默认是100,最大16384.在P版本以前的版本中,这个选项会在在输出回溯记录中加上特定的头。
例如下面的报错信息:
04-15 12:00:31.304 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 USED AFTER FREE 04-15 12:00:31.305 7412 7412 E malloc_debug: allocation[20] = 0xaf (expected 0xef) 04-15 12:00:31.305 7412 7412 E malloc_debug: allocation[99] = 0x12 (expected 0xef) 04-15 12:00:31.305 7412 7412 E malloc_debug: Backtrace at time of free: 04-15 12:00:31.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:00:31.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so
另外,还可能存在另外一种错误的信息,如果在验证之前,头信息被破坏了,会在日志文件中出现下面的错误信息:
04-15 12:00:31.604 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 HAS CORRUPTED HEADER TAG 0x1cc7dc00 AFTER FREE
free_track_backtrace_num_frames[=MAX_FRAMES]
就上上面说的,这个用于指定显示的释放时候的调用栈层数。具体的数值由参数MAX_FRAMES指定,默认是16,最大可以设置问256.
leak_track
这个选项会追踪所有的动态分配。当程序结束的时候,所有的动态分配信息会dump到文件中。如果backtrace选项使能了,这个选项会在backtrace的日志中包含内存泄露的信息。这个一般情况下不是很有用,因为大量的程序在程序结束之前都不会释放任何的分配。在P版本之前的版本中这个选项也加了特定的回溯信息。
例如在backtrace的信息中会包含下面的错误信息:
04-15 12:35:33.304 7412 7412 E malloc_debug: +++ APP leaked block of size 100 at 0x2be3b0b0 (leak 1 of 2) 04-15 12:35:33.304 7412 7412 E malloc_debug: Backtrace at time of allocation: 04-15 12:35:33.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:35:33.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:35:33.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:35:33.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so 04-15 12:35:33.305 7412 7412 E malloc_debug: +++ APP leaked block of size 24 at 0x7be32380 (leak 2 of 2) 04-15 12:35:33.305 7412 7412 E malloc_debug: Backtrace at time of allocation: 04-15 12:35:33.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:35:33.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:35:33.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:35:33.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so
record_allocs[=TOTAL_ENTRIES]
保持追踪到每个线程的每次分配上,并在接收到SIGRTMAX -18(andoird上通常是signal 46信号)dump 这些信息到对应的文件中,如果设置了TOTAL_ENTRIES, 这个表示最后会有多少哥分配释放的记录会保存下来,超过这个数值的记录都不会被记录到。 TOTAL_ENTRIES默认的是8000000,最大可以设置为50000000。在接收到dump信号后,就会将对应的记录写入到dump 文件中,并在内存中删除掉,在dump期间,任何的分配和释放都不会记录下来。
注意: 这个选项只能在O版本之后使用。分配的数据会以可直接读取的方式保存。每一行开始是gettid()获取的线程ID(THREAD_ID),表示对应的线程进行的分配或释放。 如果一个线程创建在dump文件中是没有标记的,但是一个线程的退出是有对应的标记。
线程结束的标记是下面的样子的:
THREAD_ID: thread_done 0x0
例如:
187: thread_done 0x0
下面表示出每种类型的分配释放在dump文件中如何表示出来。
pointer = malloc(size)的格式如下:
THREAD_ID: malloc pointer size
例如:
186: malloc 0xb6038060 20
free(pointer)的格式如下:
THREAD_ID: free pointer
例如:
186: free 0xb6038060
pointer = calloc(nmemb, size)的格式如下:
THREAD_ID: calloc pointer nmemb size
例如:
186: calloc 0xb609f080 32 4
new_pointer = realloc(old_pointer, size)的格式如下:
THREAD_ID: realloc new_pointer old_pointer size
例如:
186: realloc 0xb609f080 0xb603e9a0 12
pointer = memalign(alignment, size)的格式如下:
THREAD_ID: memalign pointer alignment size
pointer = aligned_alloc(alignment, size)的格式如下:
THREAD_ID: memalign pointer alignment size
posix_memalign(&pointer, alignment, size)的格式如下:
THREAD_ID: memalign pointer alignment size
例如:
186: memalign 0x85423660 16 104
pointer = valloc(size)的格式如下:
THREAD_ID: memalign pointer 4096 size
例如:
186: memalign 0x85423660 4096 112
pointer = pvalloc(size)的格式如下:
THREAD_ID: memalign pointer 4096 SIZE_ROUNDED_UP_TO_4096
例如:
186: memalign 0x85423660 4096 8192
record_allocs_file[=FILE_NAME]
这个选项在record_allocs选项开启的情况下可以设置。这个选项用于表示记录分配记录存放的温江。如果参数FILR_NAME设置了,就表示存放的文件路径。
注意:这个选项也只适用于O版本以后的版本。
verify_pointers
这个选项检查动态分配的指针是否存在。这算是一种轻量级的方式用于验证传给free/malloc_usable_size/realloc这些函数的调用是否是可用的指针
例如如下报错:
04-15 12:00:31.304 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 UNKNOWN POINTER (free) 04-15 12:00:31.305 7412 7412 E malloc_debug: Backtrace at time of failure: 04-15 12:00:31.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:00:31.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so
这里的函数是个例子,具体要看调用了什么函数。这个选项只能验证三个函数: free, malloc_usable_size, realloc.
注意: 这个选项适用于P版本及之后版本.
其他错误信息:
还有其他一些错误会出现在log中。
释放后使用
04-15 12:00:31.304 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 USED AFTER FREE (free) 04-15 12:00:31.305 7412 7412 E malloc_debug: Backtrace of original free: 04-15 12:00:31.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:00:31.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: Backtrace at time of failure: 04-15 12:00:31.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:00:31.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so
这段错误信息表示代码中试图释放一个已经释放是指针。圆括号中内容指明程序在指针上调用了free函数。
例如下面的信息:
04-15 12:00:31.304 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 USED AFTER FREE (realloc)
表示应用程序在一个已经释放的指针上调用了realloc函数。
Invalid Tag
04-15 12:00:31.304 7412 7412 E malloc_debug: +++ ALLOCATION 0x12345678 HAS INVALID TAG 1ee7d000 (malloc_usable_size) 04-15 12:00:31.305 7412 7412 E malloc_debug: Backtrace at time of failure: 04-15 12:00:31.305 7412 7412 E malloc_debug: #00 pc 00029310 /system/lib/libc.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #01 pc 00021438 /system/lib/libc.so (newlocale+160) 04-15 12:00:31.305 7412 7412 E malloc_debug: #02 pc 000a9e38 /system/lib/libc++.so 04-15 12:00:31.305 7412 7412 E malloc_debug: #03 pc 000a28a8 /system/lib/libc++.so
这段错误信息表示调用这个函数(malloc_usable_size)的指针没有被分配内存或是指针对应的内存被破坏了。和其他的错误信息一样,圆括号中的函数是指针调用的函数。
Backtrace Heap Dump Format
这一节描述堆栈dump的格式。这个数据可以通过am dumpheap -n命令,或是在P版本上通过signal或退出的时候产生。
这个数据文件有如下头部信息:
Android Native Heap Dump v1.0 Total memory: XXXX Allocation records: YYYY Backtrace size: ZZZZ
Total memory 表示当前分配的内存总大小。 Allocation records是内存分配的次数。Backtrace size 是调用栈能显示出来的最大层数。这之后是两个不同的部分,第一部分是内存的分配记录,第二部分是map数据。所有的分配记录的数据的格式如下:
z ZYGOTE_CHILD_ALLOC sz ALLOCATION_SIZE num NUM_ALLOCATIONS bt FRAMES
ZYGOTE_CHILD_ALLOC 是0或1. 0 表示这个分配由zygote本身分配或事其他不是由zygote fork出来的进程分配,1 表示这个由zygote fork出来的应用进程分配的。
ALLOCATION_SIZE 是分配的大小. NUM_ALLOCATIONS 表示由相同分配大小和相同调用栈的分配次数。is the number of allocations that have this size and have the same backtrace. FRAMES 是一个地址的列表用来表示分配调用栈的地址。
例如:
z 0 sz 400 num 1 bt 0000a230 0000b500 z 1 sz 500 num 3 bt 0000b000 0000c000
这个例子表示第一次分配由zygote创建了一个400byte大小的内存块,调用这个的调用堆栈地址是
0xa230, 0xb500。第二个记录表示是应用分配了500byte大小的内存,相同的调用栈的地方分配了3次。
dump文件的最后一个部分是进程的map data:
MAPS 7fe9181000-7fe91a2000 rw-p 00000000 00:00 0 /system/lib/libc.so . . . END
这个map数据就是简单的/proc/PID/maps的输出.这个数据可以用来解析调用栈的地址信息。
这里有个工具可以直观的展示map数据,工具地址是在源码中的development/scripts/native_heapdump_viewer.py.
例子
对于平台开发者
使能backtrace追踪所有进程的分配信息
adb shell stop adb shell setprop libc.debug.malloc.options backtrace adb shell start
使能追踪特定进程的分配信息
adb shell setprop libc.debug.malloc.options backtrace adb shell setprop libc.debug.malloc.program ls adb shell ls
使能追踪zygote以及zygote fork出来的进程
adb shell stop adb shell setprop libc.debug.malloc.program app_process adb shell setprop libc.debug.malloc.options backtrace adb shell start
使能多个选项(backtrace和guard)
adb shell stop adb shell setprop libc.debug.malloc.options "\"backtrace guard\"" adb shell start
注意:两层的双引号在adb shell命令中时必须的,外层的引号时为了host主机的shell,确保内层的引号被发送到设备中,内层的引号是为了标注‘backtrace guard’被标示为一个一个参数
使用环境变量使能malloc debug(Android O版本以前的版本):
adb shell # setprop libc.debug.malloc.env_enabled 1 # setprop libc.debug.malloc.options backtrace # export LIBC_DEBUG_MALLOC_ENABLE=1 # ls
在Android O版以及以后的版本中,使用环境变量使能malloc debug的方法如下:
adb shell # export LIBC_DEBUG_MALLOC_OPTIONS=backtrace # ls
任何从这个shell启动的进程都会使能malloc debug的backtrace选项
adb shell stop adb shell setprop libc.debug.malloc.options backtrace adb shell start adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt
上面的例子当然使用backtrace_enable_on_signal选项也是可以的,但是显然想让heap.txt中有数据必须先通过信号使能backtrace。
对于app开发者
在Android O版本及以后版本中,使能特定应用的malloc debug功能方法:
adb shell setprop wrap.<APP> '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"'
例如使能Google搜索助手的malloc debug功能:
adb shell setprop wrap.com.google.android.googlequicksearchbox '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper"' adb shell am force-stop com.google.android.googlequicksearchbox
注意: 在Android O版本以前的系统中,property属性name的长度限制在32个字符。这意味着在以前的版本中,要创建这个app的wrap属性,需要截断app的包名去适配,当然在O版本上,由于扩大了属性字符的长度限制,已经不需要截断了。
通过下面的命令就可以检测到app运行时候的内存泄漏信息了:
adb shell dumpsys meminfo --unreachable <PID_OF_APP>
如果没有使能malloc debug,这个命令只会返回是否可以检测内存泄露,而不是返回哪里发生了内存泄露。如果使能了malloc debug功能,并加了backtrace选项的话,那在运行这个dumpsys命令之前可以看到哪里做了内存分配。
为了让你的app可以做内存分配的追踪,最好在编译出来的共享库中保留好对应的symbols信息。这样才能使用类是ndk-stack等工具直接看到泄露的代码位置。
分析heap dumps
要分析dumpheap命令产生的数据,需要运行下面的脚本:
development/scripts/native_heapdump_viewer.py
为了能够使得脚本正确的分析出dump文件的堆栈信息,确保脚本脚本在源码树中执行
搜集,传输,分析dump命令如下:
adb shell am dumpheap -n <PID_TO_DUMP> /data/local/tmp/heap.txt adb shell pull /data/local/tmp/heap.txt . python development/scripts/native_heapdump_viewer.py --symbols /some/path/to/symbols/ heap.txt > heap_info.txt
目前为止,脚本会查找给定目录中的符号,所以如果你的so文件在终端设备上的目录是/data/app/.../lib/arm/libx.so,那命令中的symbols的路径应该是/some/path/to/symbols/data/app/.../lib/arm/libx.so. 也就是说: 你需要在symbols目录中镜像一个类是设备上的目录结构。
May 1, 2018, 2:41 p.m. 作者:zachary 分类:Android 阅读(6733) 评论(0)