跳轉到內容

Linux 應用程式除錯技巧/洩漏

來自華夏公益教科書

尋找什麼

[編輯 | 編輯原始碼]

記憶體可以透過許多 API 呼叫分配

  1. malloc()
  2. calloc()
  3. realloc()
  4. memalign()
  5. posix_memalign()
  6. valloc()
  7. mmap()
  8. brk() / sbrk()

要將記憶體返回給作業系統

  1. free()
  2. munmap()


Valgrind 應該是任何與記憶體相關問題的首選工具。但是

  1. 它會至少將程式速度降低一個數量級。特別是伺服器 C++ 程式可能會被減慢 15-20 倍。
  2. 根據經驗,有些版本在跟蹤方面可能存在困難mmap()分配的記憶體。
  3. 在 amd64 上,vex 反彙編器很可能在比預期更早的時候失敗(v3.7),因此 valgrind 對於任何中等或密集使用都毫無用處。
  4. 你需要編寫抑制規則來過濾掉報告的問題。

如果這些是真正的缺點,那麼有更輕量級的解決方案可用。

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。

gcc/clang 標誌

[編輯 | 編輯原始碼]

參見 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.pyhttps://github.com/iovisor/bcc 中。

libmemleak

[編輯 | 編輯原始碼]

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

DIY:libmtrace

[編輯 | 編輯原始碼]

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。缺點是它會對程式的效能造成相當大的負擔。


DIY:libmemleak

[編輯 | 編輯原始碼]

基於 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


參考文獻
[編輯 | 編輯原始碼]


各種工具

[編輯 | 編輯原始碼]
  1. {https://gcc.gnu.org/gcc-4.9/changes.html}
華夏公益教科書