android 如何分析应用的内存(六)——自定义malloc

android 如何分析应用的内存(六)

接上文,本系列文章,最重要的部分——————对native堆内存的分析,即将上演

分成六大板块:

  1. 手动实现,new和delete,以及malloc和freee,并统计内存分配情况
  2. 使用malloc hook
  3. 使用Malloc debug和libc回调
  4. 使用malloc统计和libmemunreachable
  5. 使用HWASan/Asan工具,查找内存错误
  6. 使用perfetto工具,他也可以分析java部分

本篇文章,实现第一个板块,并作为后续内存分析的基础知识。

先对,用到的理论知识,做说明,然后再进行实现。

理论篇

我们先简单介绍操作系统的系统调用,然后简单说明android libc对其的封装,最后是我们自己对libc的分配函数的封装,以达到内存统计分析的目的

系统调用

对于内存而言,它属于系统资源,它的管理者是操作系统。而为了让用户能够使用系统的资源。有三种系统调用,提供给用户。分别是:

  1. brk系统调用:用于扩展和收缩进程的数据段
  2. sbrk系统调用:同brk
  3. 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将会介绍这部分内容。敬请期待

猜你喜欢

转载自blog.csdn.net/xiaowanbiao123/article/details/131171132
今日推荐