Linux 应用程序调试技巧/泄漏
内存可以通过许多 API 调用分配
- malloc()
- calloc()
- realloc()
- memalign()
- posix_memalign()
- valloc()
- mmap()
- brk() / sbrk()
要将内存返回给操作系统
- free()
- munmap()
Valgrind 应该是任何与内存相关问题的首选工具。但是
- 它会至少将程序速度降低一个数量级。特别是服务器 C++ 程序可能会被减慢 15-20 倍。
- 根据经验,有些版本在跟踪方面可能存在困难mmap()分配的内存。
- 在 amd64 上,vex 反汇编器很可能在比预期更早的时候失败(v3.7),因此 valgrind 对于任何中等或密集使用都毫无用处。
- 你需要编写抑制规则来过滤掉报告的问题。
如果这些是真正的缺点,那么有更轻量级的解决方案可用。
LD_LIBRARY_PATH=/path/to/valgrind/libs:$LD_LIBRARY_PATH /path/to/valgrind
-v \
--error-limit=no \
--num-callers=40 \
--fullpath-after= \
--track-origins=yes \
--log-file=/path/to/valgrind.log \
--leak-check=full \
--show-reachable=yes \
--vex-iropt-precise-memory-exns=yes \
/path/to/program program-args
注意:Mudflap 已从 GCC 4.9 及更高版本中移除 [1]
Memleax 通过附加到正在运行的进程来调试内存泄漏,无需重新编译程序或重新启动目标进程!
它非常方便,适用于生产环境。
它适用于 GNU/Linux-x86_64 和 FreeBSD-amd64。
参见 https://lemire.me/blog/2016/04/20/no-more-leaks-with-sanitize-flags-in-gcc-and-clang/
参见 https://github.com/iovisor/bcc/blob/master/tools/memleak.py 在 https://github.com/iovisor/bcc 中。
Libmemleak 查找导致进程缓慢增加其使用的内存量的内存泄漏,也不需要重新编译程序,因为它可以在启动测试程序时使用 LD_PRELOAD 加载。与 valgrind 不同,它几乎不会减慢测试程序的速度。泄漏报告以每个回溯为基础。这有时非常重要,因为通常更深层回溯中的调用者(而不是释放它)负责泄漏,而实际分配发生的位置不会告诉你任何信息。
它已经在 GNU/Linux-x86_64 上测试过。
hello: Now: 287; Backtraces: 77; allocations: 650036; total memory: 83,709,180 bytes.
backtrace 50 (value_n: 104636.00); [ 178, 238>( 60): 25957 allocations (1375222 total, 1.9%), size 3311982; 432.62 allocations/s, 55199 bytes/s
backtrace 50 (value_n: 104636.00); [ 55, 178>( 123): 52722 allocations (2793918 total, 1.9%), size 6734135; 428.63 allocations/s, 54749 bytes/s
backtrace 49 (value_n: 58296.00); [ 178, 238>( 60): 14520 allocations (1382814 total, 1.1%), size 1860716; 242.00 allocations/s, 31011 bytes/s
backtrace 49 (value_n: 58296.00); [ 55, 178>( 123): 29256 allocations (2794155 total, 1.0%), size 3744938; 237.85 allocations/s, 30446 bytes/s
libmemleak> dump 49
#0 00007f84b862d33b in malloc at src/memleak.c:1008
#1 00000000004014da in do_work(int)
#2 000000000040101c in thread_entry0(void*)
#3 00007f84b7e7070a in start_thread
#4 00007f84b7b9f82d in ?? at /build/glibc-Qz8a69/glibc-2.23/misc/../sysdeps/unix/sysv/linux/x86_64/clone.S:111
libmemleak> dump 50
#0 00007f84b862d33b in malloc at src/memleak.c:1008
#1 00000000004014da in do_work(int)
#2 0000000000401035 in thread_entry1(void*)
#3 00007f84b7e7070a in start_thread
#4 00007f84b7b9f82d in ?? at /build/glibc-Qz8a69/glibc-2.23/misc/../sysdeps/unix/sysv/linux/x86_64/clone.S:111
GNU C 库附带了内置功能,可帮助检测内存问题mtrace()。一个缺点是它不会记录它跟踪的内存分配的调用堆栈。我们可以构建一个插入库来增强mtrace().
GNU C 库中的 malloc 实现提供了一种简单但强大的方法来检测内存泄漏,并获取一些信息以找到发生泄漏的位置,并且这以对程序的最小速度损失为代价。
入门非常简单
- #include mcheck.h在您的代码中。
- 调用mtrace()安装malloc(), realloc(), free()和memalign()的挂钩。从现在开始,这些函数的所有内存操作都将被跟踪。请注意,还有其他未跟踪的内存分配方式。
- 调用muntrace()卸载跟踪处理程序。
- 重新编译。
#include <mcheck.h>
...
21 mtrace();
...
25 std::string* pstr = new std::string("leak");
...
27 char *leak = (char*)malloc(1024);
...
32 muntrace();
...
在后台,mtrace()安装了上面提到的四个挂钩。通过挂钩收集的信息将写入日志文件。
注意:还有其他内存分配方式,最显著的是mmap()。不幸的是,这些分配不会被报告。
接下来
- 设置MALLOC_TRACE环境变量,其中包含内存日志名称。
- 运行程序。
- 通过 mtrace 运行内存日志。
$ MALLOC_TRACE=logs/mtrace.plain.log ./dleaker
$ mtrace dleaker logs/mtrace.plain.log > logs/mtrace.plain.leaks.log
$ cat logs/mtrace.plain.leaks.log
Memory not freed:
-----------------
Address Size Caller
0x081e2390 0x4 at 0x400fa727
0x081e23a0 0x11 at 0x400fa727
0x081e23b8 0x400 at /home/amelinte/projects/articole/memtrace/memtrace.v3/main.cpp:27
其中一个泄漏(malloc()调用)被精确地跟踪到确切的文件和行号。但是,虽然检测到第 25 行的其他泄漏,但我们不知道它们发生在哪里。的两个内存分配std::string深埋在 C++ 库内部。我们需要这两个泄漏的堆栈跟踪才能查明我们代码中的位置。
我们可以使用 GDB(或 trace_call 宏)来获取分配的堆栈
$ gdb ./dleaker
...
(gdb) set env MALLOC_TRACE=./logs/gdb.mtrace.log
(gdb) b __libc_malloc
Make breakpoint pending on future shared library load? (y or [n])
Breakpoint 1 (__libc_malloc) pending.
(gdb) run
Starting program: /home/amelinte/projects/articole/memtrace/memtrace.v3/dleaker
Breakpoint 2 at 0xb7cf28d6
Pending breakpoint "__libc_malloc" resolved
Breakpoint 2, 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
(gdb) command
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just "end".
>bt
>cont
>end
(gdb) c
Continuing.
...
Breakpoint 2, 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#0 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#1 0xb7ebb727 in operator new () from /usr/lib/libstdc++.so.6
#2 0x08048a14 in main () at main.cpp:25 <== new std::string("leak");
...
Breakpoint 2, 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#0 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#1 0xb7ebb727 in operator new () from /usr/lib/libstdc++.so.6 <== mangled: _Znwj
#2 0xb7e95c01 in std::string::_Rep::_S_create () from /usr/lib/libstdc++.so.6
#3 0xb7e96f05 in ?? () from /usr/lib/libstdc++.so.6
#4 0xb7e970b7 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string () from /usr/lib/libstdc++.so.6
#5 0x08048a58 in main () at main.cpp:25 <== new std::string("leak");
...
Breakpoint 2, 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#0 0xb7cf28d6 in malloc () from /lib/i686/cmov/libc.so.6
#1 0x08048a75 in main () at main.cpp:27 <== malloc(leak);
有mtrace()本身转储分配堆栈并放弃 gdb 会很好。修改后的mtrace()必须用
- 每个分配的堆栈跟踪。
- 反解的函数名称。
- 文件名和行号。
此外,我们可以将代码放在一个库中,以使程序无需用mtrace(). 在这种情况下,我们所要做的就是在想要跟踪内存分配时插入库(并为此付出性能代价)。
注意:在运行时获取所有这些信息,尤其是在人类可读的格式中,会对程序的性能产生影响,这与普通的mtrace()随 glibc 提供的。
一个好的开始是使用另一个 API 函数backtrace_symbols_fd(). 这会将堆栈直接打印到日志文件。非常适合 C 程序,但 C++ 符号会被混淆
@ /usr/lib/libstdc++.so.6:(_Znwj+27)[0xb7f1f727] + 0x9d3f3b0 0x4
**[ Stack: 8
./a.out(__gxx_personality_v0+0x304)[0x80492c8]
./a.out[0x80496c1]
./a.out[0x8049a0f]
/lib/i686/cmov/libc.so.6(__libc_malloc+0x35)[0xb7d56905]
/usr/lib/libstdc++.so.6(_Znwj+0x27)[0xb7f1f727] <=== here
./a.out(main+0x64)[0x8049b50]
/lib/i686/cmov/libc.so.6(__libc_start_main+0xe0)[0xb7cff450]
./a.out(__gxx_personality_v0+0x6d)[0x8049031]
**] Stack
对于 C++,我们需要获取堆栈(backtrace_symbols()),解析每个地址(dladdr())并解混淆每个符号名称(abi::__cxa_demangle()).
- 内存分配是这些基本操作之一,其他所有操作都建立在其基础之上。需要分配内存来加载库和可执行文件;需要分配内存来跟踪内存分配;我们在进程生命周期的早期就挂钩到它:第一个预加载库是内存跟踪库。因此,我们在这种插入库中进行的任何 API 调用都可能出现意外情况,尤其是在多线程环境中。
- 我们用来跟踪堆栈的 API 函数可以分配内存。这些分配也通过我们安装的钩子进行。当我们跟踪新的分配时,钩子再次被激活,并且当我们跟踪这种新的分配时,会进行另一个分配。我们将在这种无限循环中耗尽堆栈。我们通过使用每个线程的标志来摆脱这种陷阱。
- 我们用来跟踪堆栈的 API 函数可能会死锁。假设我们在跟踪中使用锁。我们锁定跟踪锁,并调用dladdr()它反过来尝试锁定动态链接器内部锁。如果在另一个线程上dlopen()在我们跟踪时被调用,dlopen() 会锁定同一个链接器锁,然后分配内存:这将触发内存钩子,我们现在有了dlopen()线程等待跟踪锁,同时链接器锁被占用。死锁。
- 在某些平台(gcc 4.7.2 amd64)上,TLS 调用会触发 memalign 钩子。如果 memalign 钩子反过来访问 TLS 变量,这可能会导致无限递归。
让我们用新的库再试一次
$ MALLOC_TRACE=logs/mtrace.stack.log LD_PRELOAD=./libmtrace.so ./dleaker
$ mtrace dleaker logs/mtrace.stack.log > logs/mtrace.stack.leaks.log
$ cat logs/mtrace.stack.leaks.log
Memory not freed:
-----------------
Address Size Caller
0x08bf89b0 0x4 at 0x400ff727 <=== here
0x08bf89e8 0x11 at 0x400ff727
0x08bf8a00 0x400 at /home/amelinte/projects/articole/memtrace/memtrace.v3/main.cpp:27
显然,没有多少改进:摘要仍然没有让我们回到 main.cpp 中的第 25 行。但是,如果我们在跟踪日志中搜索地址 8bf89b0,我们会发现
@ /usr/lib/libstdc++.so.6:(_Znwj+27)[0x400ff727] + 0x8bf89b0 0x4 <=== here
**[ Stack: 8
[0x40022251] (./libmtrace.so+40022251)
[0x40022b43] (./libmtrace.so+40022b43)
[0x400231e8] (./libmtrace.so+400231e8)
[0x401cf905] __libc_malloc (/lib/i686/cmov/libc.so.6+35)
[0x400ff727] operator new(unsigned int) (/usr/lib/libstdc++.so.6+27) <== was: _Znwj
[0x80489cf] __gxx_personality_v0 (./dleaker+27f)
[0x40178450] __libc_start_main (/lib/i686/cmov/libc.so.6+e0) <=== here
[0x8048791] __gxx_personality_v0 (./dleaker+41)
**] Stack
这很好,但如果能有文件和行信息会更好。
这里我们有几个可能性
- 通过addr2line工具运行地址(例如上面的 0x40178450)。如果地址在程序加载的共享对象中,它可能无法正确解析。
- 如果我们有程序的核心转储,我们可以要求 gdb 解析地址。或者我们可以附加到正在运行的程序并解析地址。
- 使用此处描述的 API。缺点是它会对程序的性能造成相当大的负担。
基于 libmtrace,我们可以更进一步,让一个插入库跟踪程序进行的内存分配。该库按需生成报告,很像 Valgrind。
Libmemleak 比 valgrind 快得多,但也功能有限(只有内存泄漏检测)。
libmemtrace有两个缺点
- 日志文件会很快达到 GB 级
- 你只能用 grep 搜索日志来找出是什么泄漏了
更好的解决方案是让一个插入库来收集内存操作信息并按需生成报告。
对于mmap()/munmap()我们别无选择,只能直接挂钩它们。因此,应用程序中的调用将首先击中libmemleak中的钩子,然后转到libc. 对于malloc()realloc()/memalign()/free()我们有两个选择
- 使用mtrace()/muntrace()像以前一样,安装将在内部调用的钩子libc. 因此,一个malloc()调用将首先经过libc,然后调用libmemtrace中的钩子。这让我们处于libc.
- 第二个解决方案是像m(un)map.
第二个解决方案还释放了mtrace()/muntrace()用于按需报告生成
- 第一次调用mtrace()将启动数据收集。
- 后续调用mtrace()将生成报告。
- muntrace()将停止数据收集,并将生成最终报告。
- MALLOC_TRACE不需要。
应用程序随后可以在其代码中撒上mtrace()调用,这些调用位于战略位置以避免报告太多噪音。只要MALLOC_TRACE没有设置,这些调用在正常操作中不会做任何事情。或者,应用程序可以完全不了解正在进行的数据收集(应用程序代码中没有mtrace()调用),并且libmemleak可以在加载时就开始收集,并在卸载时生成一份报告。
为了控制libmemleak功能,必须在加载库之前设置一个环境变量 -MEMLEAK_CONFIG- 将指示库在加载时开始收集数据。默认情况下是关闭的,并且应用程序必须用
export MEMLEAK_CONFIG=mtraceinit
- mtraceinit调用进行检测。m(un)trace调用。
因此,所有钩子需要做的就是调用报告
extern "C" void *__libc_malloc(size_t size);
extern "C" void *malloc(size_t size)
{
void *ptr = __libc_malloc(size);
if ( _capture_on) {
libmemleak::alloc(ptr, size);
}
return ptr;
}
extern "C" void __libc_free(void *ptr);
extern "C" void free(void *ptr)
{
__libc_free(ptr);
if (_capture_on) {
libmemleak::free(ptr, 0); // Call to reporting
}
else {
serror(ptr, "Untraced free", __FILE__, __LINE__);
}
}
extern "C" void mtrace ()
{
// Make sure not to track memory when globals get destructed
static std::atomic<bool> _atexit(false);
if (!_atexit.load(std::memory_order_acquire)) {
int ret = atexit(muntrace);
assert(0 == ret);
_atexit.store(true, std::memory_order_release);
}
if (!_capture_on) {
_capture_on = true;
}
else {
libmemleak::report();
}
}
// Leaks since previous report
======================================
// Ordered by Num Total Bytes
// Stack Key, Num Total Bytes, Num Allocs, Num Delta Bytes
5163ae4c, 1514697, 5000, 42420
...
11539977 total bytes, since previous report: 42420 bytes
Max tracked: stacks=6, allocations=25011
// All known allocations
======================================
// Key Total Bytes Allocations
4945512: 84983 bytes in 5000 allocations
bbc54f2: 1029798 bytes in 10000 allocations
...
bbc54f2: 1029798 bytes in 10000 allocations
[0x4005286a] lpt::stack::detail::call_stack<lpt::stack::bfd::source_resolver>::call_stack() (binaries/lib/libmemleak_mtrace_hooks.so+0x66) in crtstuff.c:0
[0x4005238d] _pstack::_pstack() (binaries/lib/libmemleak_mtrace_hooks.so+0x4b) in crtstuff.c:0
[0x4004f8dd] libmemleak::alloc(void*, unsigned long long) (binaries/lib/libmemleak_mtrace_hooks.so+0x75) in crtstuff.c:0
[0x4004ee7c] ?? (binaries/lib/libmemleak_mtrace_hooks.so+0x4004ee7c) in crtstuff.c:0
[0x402f5905] ?? (/lib/i686/cmov/libc.so.6+0x35) in ??:0
[0x401a02b7] operator new(unsigned int) (/opt/lpt/gcc-4.7.0-bin/lib/libstdc++.so.6+0x27) in crtstuff.c:0
[0x8048e3b] ?? (binaries/bin/1001leakseach+0x323) in /home/amelinte/projects/articole/lpt/lpt/tests/1001leakseach.cpp:68
[0x8048e48] ?? (binaries/bin/1001leakseach+0x330) in /home/amelinte/projects/articole/lpt/lpt/tests/1001leakseach.cpp:74
[0x8048e61] ?? (binaries/bin/1001leakseach+0x349) in /home/amelinte/projects/articole/lpt/lpt/tests/1001leakseach.cpp:82
[0x8048eab] ?? (binaries/bin/1001leakseach+0x393) in /home/amelinte/projects/articole/lpt/lpt/tests/1001leakseach.cpp:90
[0x401404fb] ?? (/lib/i686/cmov/libpthread.so.0+0x401404fb) in ??:0
[0x4035e60e] ?? (/lib/i686/cmov/libc.so.6+0x5e) in ??:0
// Crosstalk: leaked bytes per stack frame
--------------------------------------
1029798 bytes: [0x8048e3b] ?? (binaries/bin/1001leakseach+0x323) in /home/amelinte/projects/articole/lpt/lpt/tests/1001leakseach.cpp:68
...
// Mem Address, Stack Key, Bytes
--------------------------------------
0x8ce7988, bbc54f2, 4
...
This report took 44 ms to generate.
该mallinfo()API 有传言说已弃用。但是,如果可用,它非常有用
#include <malloc.h>
namespace process {
class memory
{
public:
memory() : _meminfo(::mallinfo()) {}
int total() const
{
return _meminfo.hblkhd + _meminfo.uordblks;
}
bool operator==(memory const& other) const
{
return total() == other.total();
}
bool operator!=(memory const& other) const
{
return total() != other.total();
}
bool operator<(memory const& other) const
{
return total() < other.total();
}
bool operator<=(memory const& other) const
{
return total() <= other.total();
}
bool operator>(memory const& other) const
{
return total() > other.total();
}
bool operator>=(memory const& other) const
{
return total() >= other.total();
}
private:
struct mallinfo _meminfo;
};
} //process
#include <iostream>
#include <string>
#include <cassert>
int main()
{
process::memory first;
{
void* p = ::malloc(1025);
process::memory second;
std::cout << "Mem diff: " << second.total() - first.total() << std::endl;
assert(second > first);
::free(p);
process::memory third;
std::cout << "Mem diff: " << third.total() - first.total() << std::endl;
assert(third == first);
}
{
std::string s("abc");
process::memory second;
std::cout << "Mem diff: " << second.total() - first.total() << std::endl;
assert(second > first);
}
process::memory fourth;
assert(first == fourth);
return 0;
}
可以从 /proc 获取粗粒度信息
#!/bin/ksh
#
# Based on:
# http://stackoverflow.com/questions/131303/linux-how-to-measure-actual-memory-usage-of-an-application-or-process
#
# Returns total memory used by process $1 in kb.
#
# See /proc/PID/smaps; This file is only present if the CONFIG_MMU
# kernel configuration option is enabled
#
IFS=$'\n'
for line in $(</proc/$1/smaps)
do
[[ $line =~ ^Private_Clean:\s+(\S+) ]] && ((pkb += ${.sh.match[1]}))
[[ $line =~ ^Private_Dirty:\s+(\S+) ]] && ((pkb += ${.sh.match[1]}))
[[ $line =~ ^Shared_Clean:\s+(\S+) ]] && ((skb += ${.sh.match[1]}))
[[ $line =~ ^Shared_Dirty:\s+(\S+) ]] && ((skb += ${.sh.match[1]}))
[[ $line =~ ^Size:\s+(\S+) ]] && ((szkb += ${.sh.match[1]}))
[[ $line =~ ^Pss:\s+(\S+) ]] && ((psskb += ${.sh.match[1]}))
done
((tkb += pkb))
((tkb += skb))
#((tkb += psskb))
echo "Total private: $pkb kb"
echo "Total shared: $skb kb"
echo "Total proc prop: $psskb kb Pss"
echo "Priv + shared: $tkb kb"
echo "Size: $szkb kb"
pmap -d $1 | tail -n 1
- gdb-heap 扩展
- Dr. Memory
- Pin 工具
- heaptrack
- perf 的数据类型分析
- 类型保留堆分析器
- 有关工具列表,请参见 https://stackoverflow.com/questions/18455698/lightweight-memory-leak-debugging-on-linux。其中,memleax 值得特别一提,因为它采用的是进程外方法。