跳到主要内容

Linux嵌入式查找内存泄漏

· 阅读需 9 分钟

LeakTracer

LeakTracer 是一个非常轻巧的内存泄露排查库。基本所有的内存泄露排查工具原理都一样,记录内存申请的现场信息,在内存释放处,将配对的现场记录删除掉。然后在程序结束或某个时间,打印出那些还尚未被移除的现场记录。

有时候有些程序,运行后,无法手动结束应用,即代码编写并不规范,没有考虑程序结束的处理情况。在使用 LeakTracer 的时候,可以选择带程序初始化完成延后一段时间,再开始记录内存申请释放。然后周期性的打印输出,查看是哪个位置内存泄漏可疑性非常大。

leaktracer_startMonitoringAllThreads();
while (!exit) {
std::this_thread::sleep_for(std::chrono::minutes(5)); // 这里最好可以更加细片化判断exit,以方便线程退出
leaktracer_writeLeaksToFile("leak_hhmmss.dump"); // 建议文件名添加时间戳便于分析和防止文件发生覆盖
}
leaktracer_stopAllMonitoring();

生成的 leak_hhmmss.dump 文件是一个文本文件,里面记录着内存申请时的函数调用栈,以及申请的内存大小,以及内存内容。LeakTracer 提供了一个 leak-analyze-addr2line perl 脚本,其就是利用 addr2line 将记录的调用栈结合带有调试信息的可执行文件得到函数调用的代码出处,顺便统计这个调用栈在这个文件中出现的次数以及总共还存有多少释放的内存。

在使用 addr2line 过程中,有可能出现 ??:0 的打印,即分析不出代码行。出现这个问题的一个很大可能性就是,程序记录的是函数调用栈的内存地址,而 addr2line 需要的是相对偏移地址。我们在下文提及(参考 Boost.Stacktrace 提供的实现)。

valgrind

Valgrind 是内存检测非常强大的工具,它在 Linux 具有包管理器下,使用非常友好。但是在嵌入式场景下,使用就不是那么方便了。

因为其需要带调试信息的 glibc 动态库文件 libc.so.6 的库。 在具有包管理器的环境下,我们可以安装 libc6-dbg。但是在嵌入式环境下就比较麻烦了,如果文件系统构建时,构建了调试版本的 libc.so.6,那么拷贝至系统即可。

危险

如果没有,那么需要我们自己找到对应版本的 glibc 源代码,然后交叉编译得到调试版本的 libc.so.6,再将编译好的 libc.so.6 动态库拷进系统。

这一步,如果 glibc 版本号对不上或者其他原因,一不注意就会导致系统崩溃。

所以下文 交叉编译valgrind交叉编译glibc 只是做记录使用。

交叉编译valgrind

sudo apt-get install automake
./configure --prefix=/opt/aarch64-v01c01-linux-gnu-gcc/lib/valgrind-3.22.0 --host=aarch64-linux-gnu --enable-only64bit

在将valgrind-3.22.0部署到板子上后,需要将 libexec/valgrind 路径导出到 VALGRIND_LIB 变量:

export VALGRIND_LIB=/data/sdcard/valgrind-3.22.0/libexec/valgrind

然后执行:

valgrind --leak-check=yes danki

然后大概率会有如下输出:

==1780== Memcheck, a memory error detector
==1780== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==1780== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==1780== Command: danki
==1780==

valgrind: Fatal error at startup: a function redirection
valgrind: which is mandatory for this platform-tool combination
valgrind: cannot be set up. Details of the redirection are:
valgrind:
valgrind: A must-be-redirected function
valgrind: whose name matches the pattern: strlen
valgrind: in an object with soname matching: ld-linux-aarch64.so.1
valgrind: was not found whilst processing
valgrind: symbols from the object with soname: ld-linux-aarch64.so.1
valgrind:
valgrind: Possible fixes: (1, short term): install glibc's debuginfo
valgrind: package on this machine. (2, longer term): ask the packagers
valgrind: for your Linux distribution to please in future ship a non-
valgrind: stripped ld.so (or whatever the dynamic linker .so is called)
valgrind: that exports the above-named function using the standard
valgrind: calling conventions for this platform. The package you need
valgrind: to install for fix (1) is called
valgrind:
valgrind: On Debian, Ubuntu: libc6-dbg
valgrind: On SuSE, openSuSE, Fedora, RHEL: glibc-debuginfo
valgrind:
valgrind: Note that if you are debugging a 32 bit process on a
valgrind: 64 bit system, you will need a corresponding 32 bit debuginfo
valgrind: package (e.g. libc6-dbg:i386).
valgrind:
valgrind: Cannot continue -- exiting now. Sorry.

交叉编译glibc

sudo apt install bison
./configure --prefix=/opt/aarch64-v01c01-linux-gnu-gcc/lib/glibc-2.34 --host=aarch64-linux-gnu

原理及自己实现

最先需要做的就是,我们需要能够在内存申请和释放的地方执行我们的逻辑。C 中内存申请释放的函数为:

void *malloc(size_t size);
void free(void *ptr);

C++ 中内存申请释放的函数为:

void *operator new(std::size_t count);
void operator delete(void *ptr) noexcept;

void *operator new[](std::size_t count);
void operator delete[](void *ptr) noexcept;

首先,我们先重载全局 new/ deletenew[]/delete[]操作符,让其经由 malloc()/free()进行内存释放。

void *operator new(std::size_t count) {
return malloc(size);
}

void operator delete(void *ptr) noexcept {
return free(p);
}

void *operator new[](std::size_t count) {
return malloc(count);
}

void operator delete[](void *ptr) noexcept {
return free(ptr);
}

然后我们需要利用 ld链接器 提供的的 --wrap symbol 选项,将程序中调用 malloc()/free() 的地方替换成我们自定义的函数:

target_link_libraries(App
-Wl,--wrap=malloc
-Wl,--wrap=free
)

它会将代码中 引用 malloc 符号的地方替换成 __wrap_malloc,而将 malloc 的实现替换成 __real_malloc。同理 free 也如此。

实际上 --wrap symbol 的作为用为:如果当前文件未定义 malloc 实现,就将其解析为引用 __wrap_symbol,如果当前文件未定义 __real_symbol 实现,就将其解析为引用 symbol

这里是为了好理解mallocfree的处理方式,才如上这么解释的。

有了可以在内存申请释放处执行我们自己的业务逻辑能力之后,接下来就是考虑如何记录现场。Boost.Stacktrace 提供了非常便利的实现,为了方便起见,我们直接使用它。

顺便提一嘴,boost/stacktrace/detail/addr_base.hpp 文件,实现了获取应用程序基地址的实现,刚好能够解决在 LeakTracer 遇到的问题。

自己实现原理和 LeakTracer 一样,只不过我们实现可更加利于后续扩展。

void MemoryAllocationStackTracer::push(uint64_t address, Frame &&stacktrace) {
Infomation info;
info.time = std::chrono::system_clock::now();
info.frame = std::move(stacktrace);
m_stacktraces.insert({address, info});
}

void MemoryAllocationStackTracer::pop(uint64_t address) {
if (m_stacktraces.count(address) > 0) {
m_stacktraces.erase(address);
}
}

在完整的实现过程中,还是要注意很多地方,一个就是我们实现的 MemoryAllocationStackTracer 记录内存申请释放的工具类,例如 std::unordered_mapboost::stacktrace::stacktrace 也会使用到堆内存,要注意其不能使用到堆内存,所以我们要而外自定义实现 std::allocator__real_malloc()__real_free() 申请释放内存。对于 MemoryAllocationStackTracer,我们可以重载它的 newdelete 操作符,也可以使用 new 的原地构造来生成 MemoryAllocationStackTracer 对象:

auto buffer = __real_malloc(sizeof(MemoryAllocationStackTracer));	// 构造
if (buffer != nullptr) {
m_instance = new (buffer) MemoryAllocationStackTracer();
}

if(m_instance != nullptr) { //析构
m_instance->~MemoryAllocationStackTracer();
__real_free(m_instance);
m_instance = nullptr;
}