android 如何分析应用的内存(六)
接上文,本系列文章,最重要的部分——————对native堆内存的分析,即将上演
分成六大板块:
- 手动实现,new和delete,以及malloc和freee,并统计内存分配情况
- 使用malloc hook
- 使用Malloc和libc回调,搭配使用DDMS
- 使用AddressSanitizer
- 使用HWASan/Asan工具,查找内存错误
- 使用perfetto工具,他也可以分析java部分
本篇文章,实现第一个板块,并作为后续内存分析的基础知识。
先对,用到的理论知识,做说明,然后再进行实现。
理论篇
我们先简单介绍操作系统的系统调用,然后简单说明android libc对其的封装,最后是我们自己对libc的分配函数的封装,以达到内存统计分析的目的
系统调用
对于内存而言,它属于系统资源,它的管理者是操作系统。而为了让用户能够使用系统的资源。有三种系统调用,提供给用户。分别是:
- brk系统调用:用于扩展和收缩进程的数据段
- sbrk系统调用:同brk
- mmap/munmap系统调用:用于在进程的地址空间,创建/取消内存映射
除了上面提供的以外,还有一些更高级的系统调用,此处不表
其中brk系统调用和sbrk系统调用的区别是:brk用于直接设置break地址。即修改进程数据段的结束地址。
sbrk用于在原有的break地址上,进行增加或减少
mmap和munmap则是,直接进行进程地址空间的映射。
libc库
从上面可以知道,操作系统只提供了一些非常简单的内存操作接口,
如果一个进程中,有多个线程,应该怎么管理这部分堆空间呢?
如果一个线程要反反复复频繁的分配空间怎么办呢?是否要频繁的调用系统调用呢?
如果分配的空间,一会大的要死,一会儿小的要死,应该怎么办呢?
此时,Android的libc库,对上面的内存做了进一步的管理。它使用了一种被称为jemalloc的分配器策略。
jemalloc,维护了大小不同的内存池。当应用程序请求分配内存时,jemalloc会选择合适的内存满足应用的请求。
jemalloc,还为每个线程维护本地缓存,当线程请求内存时,则尽可能返回本地缓存中的内存块。
jemalloc的所有这些内存管理,都是基于上面介绍的三种系统调用。
同时Android的libc库,也是一种轻量级的c库,因此对jemalloc的分配器策略,进行了一种标准接口的封装
即我们熟悉的:malloc,remalloc,calloc标准接口
当应用程序,调用malloc,remalloc,或者calloc函数请求分配内存时.libc库则根据jemalloc的分配器策略分配不同的内存快
自定义内存分配函数
那么我们还可以对,libc库的内存分配函数,做进一步的封装。在应用每次调用内存分配的时候,记录下调用分配的堆栈,和分配的大小。
在合适的时候,将其打印出来,以观察内存的分配情况,从而达到分析内存泄漏的问题。
malloc函数,free函数
malloc,free函数位于stdlib.h文件中,定义如下
void* malloc (size_t size);
void free (void* ptr);
下面是我自定义的malloc函数,我将其放在一个单独的命名为Memory.cpp的文件中
void* malloc(size_t size) {
void* ptr = NULL;
// 在这里,你可以添加自定义的逻辑,例如记录内存分配的信息,或者改变内存分配的行为。
// 调用系统的malloc函数进行实际的内存分配。
// 注意,这里需要使用函数指针来调用系统的malloc函数,以避免无限递归。
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
//查找下一个叫做malloc的符号
if (libc_malloc) {
ptr = libc_malloc(size);
//将ptr保存,以便于后续分析
addToAllptr(ptr,size);
}
return ptr;
}
为了保存,分配的地址和,大小,新增下面的函数
//保存所有指针的地方
AllPtr ptrBuffer;
static std::recursive_mutex mutex;
//为了在ptrBuffer.push的时候,防止循环调用
static bool * isPush = nullptr;
void addToAllptr(void *ptr,std::size_t sz){
std::unique_lock<std::recursive_mutex> _l(mutex);
//为什么不直接用一个bool变量?
//因为发现,在部分版本中,会被优化掉,因此通过使用单独的内存来存储这个bool值
if(!isPush){
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
if (libc_malloc) {
isPush =(bool *) libc_malloc(sizeof(bool));
*isPush = false;
}
}
if(isPush){
if(*isPush){
}else{
*isPush = true;
ptrBuffer.push(ptr,sz);
*isPush = false;
}
}
}
void popFromAllptr(void *ptr) {
std::unique_lock<std::recursive_mutex> _l(mutex);
ptrBuffer.pop(ptr);
}
其中。prtBuffer是类型为Allptr的一个自定义类,这个类负责管理,分配的内存。代码如下:
struct AllPtr{
//每次内存的分配,就会有一个PtrItem与之对应
struct PtrItem{
bool isEmpty = true;//表示该PtrItem是否为空
void * ptr = nullptr; //存储分配的指针
std::size_t size = 0;//存储分配的内存大小
struct timespec now;//存储分配内存的时间
char * stack = nullptr;//存储分配内存的调用栈
};
PtrItem * allptr = nullptr;//PtrItem数组头指针
std::size_t size = 0;//PtrItem数组大小
int line = 20;//辅助打印的行数
//判断PtrItem数组是否满
bool isFull() const{
bool full = true;
for(auto i =0;i<size;i++){
if(allptr[i].isEmpty){
full = false;
break;
}
}
return full;
}
//创建一个新的PtrItem数组,它的大小是老数组大小的2倍,并将老数组内容,复制到新数组中
void createAndCopy(){
std::size_t newSize = 0;
if(size > 0 ){
newSize = size * 2;
ALOG(LOG_INFO, __FUNCTION__ , "create and copy %lu",newSize);
}else{
newSize = 1;
}
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
if (!libc_malloc) {
ALOG(LOG_ERROR, __FUNCTION__ , "find lib malloc error");
return ;
}
auto newPtr = (PtrItem *)libc_malloc(sizeof(PtrItem) * newSize);
for(int i= 0;i<newSize;i++){
newPtr[i].isEmpty = true;
newPtr[i].ptr = nullptr;
newPtr[i].size = 0;
newPtr[i].now.tv_sec = 0;
newPtr[i].now.tv_nsec = 0;
newPtr[i].stack = nullptr;
}
//复制老数组内容
for(int i = 0; i < size;i++){
newPtr[i].isEmpty = allptr[i].isEmpty;
newPtr[i].ptr = allptr[i].ptr;
newPtr[i].size = allptr[i].size;
newPtr[i].now.tv_sec= allptr[i].now.tv_sec;
newPtr[i].now.tv_nsec= allptr[i].now.tv_nsec;
newPtr[i].stack = allptr[i].stack;
}
if(allptr){
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(allptr);
}
}
allptr = newPtr;
size = newSize;
}
//保存ptr,新建一个PtrItem
void push(void * ptr,std::size_t size) {
//是否为空//是否满
if (!allptr || isFull()) {
createAndCopy();
}
for (auto i = 0; i < size; i++) {
if (allptr[i].isEmpty) {
allptr[i].isEmpty = false;
allptr[i].ptr = ptr;
allptr[i].size = size;
if (clock_gettime(CLOCK_REALTIME, &(allptr[i].now)) == -1) {
perror("clock_gettime");
}
//initialize
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
if (libc_malloc) {
allptr[i].stack = (char * )libc_malloc( 1024*line);
//获取对应的调用栈
Find::Debug().printStackTrace(allptr[i].stack,1024*line);
}
return;
}
}
}
//弹出Ptr
void pop(void *ptr) const {
for(auto i=0;i<size;i++) {
if(allptr[i].ptr == ptr) {
allptr[i].isEmpty = true;
if (allptr[i].stack){
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(allptr[i].stack);
}
}
return;
}
}
}
~AllPtr(){
if(allptr){
for(auto i =0;i<size;i++){
if(!allptr[i].isEmpty){
if (allptr[i].stack){
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(allptr[i].stack);
}
}
break;
}
}
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(allptr);
}
}
}
//在合适的时候,dump出PtrItem中的内容,便于分析
//在这里,只是简单的将其输出到Log系统
//事实上,应该按照时间进行采样,然后输出到文件中,通过其他工具将其图形化
//这里为了简化远离,仅仅将其输出到log系统中
void dump(){
for(auto i=0;i<size;i++){
if(allptr[i].isEmpty == false){
time_t t = allptr[i].now.tv_sec;
struct tm *local;
local = localtime(&t);
if (local == NULL) {
perror("localtime");
}
ALOG(LOG_INFO, __FUNCTION__ , "%02d:%02d:%02d:%04d,ptr %p,size %lu",
local->tm_hour,local->tm_min,local->tm_sec,allptr[i].now.tv_nsec/1000000,
allptr[i].ptr,allptr[i].size);
for(int i=0;i<line;i++){
ALOG(LOG_INFO, __FUNCTION__ ,"%s",allptr[i].stack+(i*1024));
}
}
}
}
};
在上面的例子中,使用Find::Debug().printStackTrace来获取android的native调用栈
它的实现如下:
namespace Find {
//将调用栈信息打印到buffer中
void Debug::printStackTrace(char * buffer,int size){
const auto maxStackDeep = 10;
intptr_t stackBuf[maxStackDeep];
for(int i = 0; i < maxStackDeep; ++i){
stackBuf[i] = 0;
}
memset(buffer, 0, size);
dumpBacktraceIndex(buffer, stackBuf, captureBacktrace(stackBuf, maxStackDeep));
}
//捕获调用栈信息
size_t Debug::captureBacktrace(intptr_t *buffer, size_t maxStackDeep) {
BacktraceState state = {buffer, buffer + maxStackDeep};
_Unwind_Backtrace(unwindCallback, &state);
return state.current - buffer;
}
//解析调用栈信息
void Debug::dumpBacktraceIndex(char *out, intptr_t *buffer, size_t count) {
for (size_t idx = 0; idx < count; ++idx) {
intptr_t addr = buffer[idx];
const char *symbol = " ";
const char *dlfile = " ";
void * baseA = nullptr;
Dl_info info;
info.dli_fbase = nullptr;
if (dladdr((void *) addr, &info)) {
if (info.dli_sname) {
symbol = info.dli_sname;
}
if (info.dli_fname) {
dlfile = info.dli_fname;
}
if(info.dli_fbase){
baseA = info.dli_fbase;
}
} else {
strcat(out, "# \n");
continue;
}
char temp[50];
memset(temp, 0, sizeof(temp));
sprintf(temp, "%zu", idx);
strcat(out, "#");
strcat(out, temp);
strcat(out, ": ");
memset(temp, 0, sizeof(temp));
sprintf(temp, "%p", (void *) addr);
strcat(out, temp);
strcat(out, " ");
memset(temp, 0, sizeof(temp));
//尤其要注意,这里的两地址相减,这是为了让addr2line工具能够正常解析
sprintf(temp, "%p", (void *) ((long)addr - (long )baseA));
strcat(out, temp);
strcat(out, " ");
strcat(out, symbol);
strcat(out, " ");
strcat(out, dlfile);
strcat(out, "\n");
}
}
_Unwind_Reason_Code Debug::unwindCallback(struct _Unwind_Context *context, void *arg) {
BacktraceState *state = static_cast<BacktraceState *>(arg);
intptr_t ip = (intptr_t) _Unwind_GetIP(context);
if (ip) {
if (state->current == state->end) {
return _URC_END_OF_STACK;
} else {
state->current[0] = ip;
state->current++;
}
}
return _URC_NO_REASON;
}
}
注意:在Debug中,并没有使用backtrace函数,这是因为,backtrace函数是由GNUC库提供的。而android的c库,是一个被称为bionic的Google单独编写的c库,并没有提供backtrace函数。因此我们选择了libunwind库中的_Unwind_Backtrace。
然后使用dladdr函数进行地址的解析。
需要再次注意的是:addr地址为内存中的地址,并不能直接使用
addr2line进行直接转换。需要将addr和共享库的基址相减,得到相对地址,才能够被add2line进行转化。
有了上面的malloc,还需要有free函数,如下
//释放对应的ptr
void free (void* ptr){
//删除Allptr中的ptr
popFromAllptr(ptr);
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(ptr);
}
}
增加一个广播,用于测试
为了能够直观的观测到记录下来的内存,现在增加一个广播,如下:
application.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Timber.d(intent.action)
native_dumpCpluplusObjecs()
}
}, IntentFilter("find.dump.objects"))
当收到"find.dump.objects"时,会调用native_dumpCpluplusObjecs。它的实现如下:
extern AllPtr ptrBuffer;
extern "C"
JNIEXPORT void JNICALL
Java_cn_findpiano_romsdk_hardware_midi_MidiManager_native_1dumpCpluplusObjecs(JNIEnv *env,
jobject thiz) {
ptrBuffer.dump();
}
我们调用ptrBuffer的dump函数,将记录下来的内存信息输出到log系统中。
现在进入adb shell ,输入如下的命令
am broadcast -a find.dump.objects
那么将会在log系统看到如下的输出
注意:自定义malloc函数,并不会影响realloc和calloc,因为我目前的项目并没有使用这两函数,因此也没有做演示,但他们的核心思想同malloc一样
c++中的new和delete实现
对于c++这门语言而言,它的new和delete以及new[] delete[]都是直接调用对应的运算符函数,如void* operator new(std::size_t sz);
因此我们可以重载这些运算符函数,以达到监控内存的目的。事实上android的c++库,不管是来自LLVM项目的libc++库。还是,来自android自定义的libstdc++库,都会间接调用,android的libc库的分配函数和释放函数。
注意注意:Android的libstdc++.so和来自GNU项目的libstdc++.so是不一样的。具体原因可以参看
ndk的问题744:https://github.com/android/ndk/issues/744
即然会间接调用libc的分配函数,那么上面关于malloc和free的实现,也已经覆盖了c++的使用。
但是这里为了演示说明,将重新实现void* operator new(std::size_t sz),然后它使用系统提供的malloc,而不是我们自己实现的malloc,下面的代码来自c++中的例子,并增加了部分,前面介绍的内容,如使用libc中的malloc
void* operator new(std::size_t sz)
{
if (sz == 0)
++sz; // avoid std::malloc(0) which may return nullptr on success
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
if (libc_malloc) {
if (void *ptr = libc_malloc(sz)){
ALOGD("new an object %lu,ptr %p",sz,ptr);
addToAllptr(ptr,sz);
return ptr;
}
}
throw std::bad_alloc{};
}
// no inline, required by [replacement.functions]/3
void* operator new[](std::size_t sz)
{
if (sz == 0)
++sz; // avoid std::malloc(0) which may return nullptr on success
void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc");
if (libc_malloc) {
if (void *ptr = libc_malloc(sz)){
ALOGD("new an array object %lu,ptr %p",sz,ptr);
addToAllptr(ptr,sz);
return ptr;
}
}
throw std::bad_alloc{};
}
void operator delete(void* ptr) noexcept
{
ALOGD("delete an object %p",ptr);
popFromAllptr(ptr);
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(ptr);
}
}
void operator delete[](void* ptr) noexcept
{
ALOGD("delete an array object %p",ptr);
popFromAllptr(ptr);
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(ptr);
}
}
上面关于c++的new和delete仅仅是演示使用。c++提供了更加丰富的自定义分配函数的操作。
注意:在上面的代码中,我们使用了dlsym(RTLD_NEXT, “malloc”);来查找下一个叫做malloc的函数,如果有多个so库且都含有malloc,这样的行为,将是根据搜索路径中出现的先后顺序决定。
通过在一段时间中,多次dump,然后使用shell脚本,按照时间排序,可以非常轻松的观测到长期占据且没有释放的内存。然后使用对应的调用栈,找到源码进行分析。
至此,关于手动实现内存的分配和释放,并保存调用栈。已完成。
使用此种方法,基本上只会影响本so库中内容,这取决于编译链接的机制,那么怎样能够监控所有的内存分配呢,下一节molloc_hooks将会介绍这部分内容。敬请期待