C / C ++プログラムのメモリリーク-いくつかのバグの歴史

いくつかのメモリリークの問題のストーリー。 これらの問題のほとんどは非常に簡単で、再現しやすく、適切なツールで簡単に検出され、修正されます。 しかし、時々、問題は珍しく、珍しいアプローチまたは解決策を必要とします...



コード損失メモリ


QAの別のテストでは、製品の新しいバージョンで負荷がかかっている状態で、わずかな永続的なメモリリークが明らかになりました。 再生は問題ではありませんでしたが、Valgrindでのプログラムの最初の実行ではリークは明らかになりませんでした。



少し考えてから、オプション--leak-check = fullがオンになり、Valgrindがリークの報告を開始しました。 しかし、意図的に解放されないことが多いさまざまな種類の静的変数の予想されるリークのうち、リークに似たものはありませんでした。 通常、数千回の反復を実行する場合、mallocによって割り当てられた同数の失われたメモリブロックを簡単に割り当てることができます。 この場合、10,000件のサーバー要求のリークは最小限でした。



メモリ割り当てのスタックを分析し、予想されるケースを除外した後、数十個の失われたブロックを担当した候補者は1人だけでした(10,000人の要求にまったく関連付けられていない数)。 しかし、これには説明がありました-STL文字列クラスでメモリ割り当てが発生し、メモリプールを積極的に使用してメモリ割り当ての数を減らしました。 したがって、メモリブロックが10,000個失われる代わりに、Valgrindは40以上を報告しました。 呼び出しスタックは次のようになりました。



==15882== 76,400 bytes in 8 blocks are definitely lost in loss record 2 of 3

==15882== at 0x401B007: operator new(unsigned int) (vg_replace_malloc.c:214)

==15882== by 0x40A40F0: std::__default_alloc_template<true, 0>::_S_chunk_alloc(unsigned int, int&) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x40A3FFC: std::__default_alloc_template<true, 0>::_S_refill(unsigned int) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x40A3B6B: std::__default_alloc_template<true, 0>::allocate(unsigned int) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x40A9B67: std::string::_Rep::_S_create(unsigned int, std::allocator<char> const&) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x40A9C98: std::string::_Rep::_M_clone(std::allocator<char> const&, unsigned int) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x40A7A05: std::string::reserve(unsigned int) (in /usr/lib/libstdc++.so.5.0.3)

==15882== by 0x8049826: std::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char,

std::char_traits<char>, std::allocator<char> >(char const*, std::basic_string<char, std::char_traits<char>,

std::allocator<char> > const&) (basic_string.tcc:619)

==15882== by 0x804956A: A::A(A const&) (class_a.cpp:20)

==15882== by 0x80491BC: foo(int) (test.cpp:23)

==15882== by 0x80492EA: main (test.cpp:32)








メモリリークの原因は見つかったようですが、コードはまったく無害に見えました-渡されたオブジェクトの一時コピーを使用した関数呼び出し:



doSomething( condition ? Object( params ) : getObject() );







この行でメモリが失われていることはすでに確信しており、コンパイラがこの行で生成したコードを調べ始めました。 「basic_string :: length()」の呼び出し、条件の1つのブランチのパラメータークラスのコンストラクターの呼び出し、「Parent :: getB()」の呼び出し、もう1つのブランチのコピーコンストラクター、関数「A ::」の呼び出し作成」、一時オブジェクトのリリース-クラスの一時コピーのデストラクタを呼び出す以外のすべて-スタック上に作成されますが、結果として解放されなかった文字列のコピーが含まれています!



 110: return A::create(b1, b2, s.length() > 0 ? B(s) : getB()); 13d4: 83 ec 0c sub $0xc,%esp 13d7: 8d 45 d8 lea 0xffffffd8(%ebp),%eax 13da: 50 push %eax 13db: e8 fc ff ff ff call std::basic_string<wchar_t>::length() const <<========= 13e0: 83 c4 10 add $0x10,%esp 13e3: 85 c0 test %eax,%eax 13e5: 74 18 je 13ff <A::create+0x341> 13e7: 83 ec 08 sub $0x8,%esp 13ea: 8d 45 d8 lea 0xffffffd8(%ebp),%eax 13ed: 50 push %eax 13ee: 8d 85 f8 fe ff ff lea 0xfffffef8(%ebp),%eax 13f4: 50 push %eax 13f5: e8 fc ff ff ff call B::B( std::basic_string<wchar_t> const & ) <<========= 13fa: 83 c4 10 add $0x10,%esp 13fd: eb 21 jmp 1420 <A::create+0x362> 13ff: 83 ec 08 sub $0x8,%esp 1402: 83 ec 04 sub $0x4,%esp 1405: ff 75 0c pushl 0xc(%ebp) 1408: e8 fc ff ff ff call Parent::getB() <<========= 140d: 83 c4 08 add $0x8,%esp 1410: 50 push %eax 1411: 8d 85 f8 fe ff ff lea 0xfffffef8(%ebp),%eax 1417: 50 push %eax 1418: e8 fc ff ff ff call B::B( B const & ) <<========= 141d: 83 c4 10 add $0x10,%esp 1420: 83 ec 0c sub $0xc,%esp 1423: 8d 85 f8 fe ff ff lea 0xfffffef8(%ebp),%eax 1429: 50 push %eax 142a: 0f b6 45 f6 movzbl 0xfffffff6(%ebp),%eax 142e: 50 push %eax 142f: 0f b6 45 f7 movzbl 0xfffffff7(%ebp),%eax 1433: 50 push %eax 1434: ff 75 0c pushl 0xc(%ebp) 1437: ff 75 08 pushl 0x8(%ebp) 143a: e8 fc ff ff ff call A::create(bool, bool, B) <<========= 143f: 83 c4 1c add $0x1c,%esp 1442: 83 ec 0c sub $0xc,%esp 1445: 8d 85 68 ff ff ff lea 0xffffff68(%ebp),%eax 144b: 50 push %eax 144c: e8 fc ff ff ff call BS<100, char>::~BS() 1451: 83 c4 10 add $0x10,%esp 1454: 83 ec 0c sub $0xc,%esp 1457: 8d 45 d8 lea 0xffffffd8(%ebp),%eax 145a: 50 push %eax 145b: e8 fc ff ff ff call std::basic_string<wchar_t>::~basic_string() 1460: 83 c4 10 add $0x10,%esp 1463: eb 55 jmp 14ba <A::create+0x3fc> 1465: 89 85 f0 fe ff ff mov %eax,0xfffffef0(%ebp) 146b: 8b b5 f0 fe ff ff mov 0xfffffef0(%ebp),%esi 1471: 83 ec 0c sub $0xc,%esp 1474: 8d 85 68 ff ff ff lea 0xffffff68(%ebp),%eax 147a: 50 push %eax 147b: e8 fc ff ff ff call BS<100, char>::~BS() 1480: 83 c4 10 add $0x10,%esp 1483: 89 b5 f0 fe ff ff mov %esi,0xfffffef0(%ebp) 1489: eb 06 jmp 1491 <A::create+0x3d3> 148b: 89 85 f0 fe ff ff mov %eax,0xfffffef0(%ebp) 1491: 8b b5 f0 fe ff ff mov 0xfffffef0(%ebp),%esi 1497: 83 ec 0c sub $0xc,%esp 149a: 8d 45 d8 lea 0xffffffd8(%ebp),%eax 149d: 50 push %eax 149e: e8 fc ff ff ff call std::basic_string<wchar_t>::~basic_string() 14a3: 83 c4 10 add $0x10,%esp 14a6: 89 b5 f0 fe ff ff mov %esi,0xfffffef0(%ebp) 14ac: 83 ec 0c sub $0xc,%esp 14af: ff b5 f0 fe ff ff pushl 0xfffffef0(%ebp) 14b5: e8 fc ff ff ff call _Unwind_Resume
      
      







コードのデストラクタ呼び出しを見つけられなかったので、彼らは同様の問題を探し始め、ほとんどすぐに「 gcc 3.2 bug 9946-オブジェクトデストラクタが呼び出されず、メモリリークを引き起こす可能性がある 」を見つけました。



問題は「?:」演算子のコードを生成することであり、コンパイラを更新するか、「?:」演算子を単純なif()に変更することで解決しました。



// ,

const Object & getObject();



//

void doSomething( Object obj );



// , .

// ,

// .

doSomething( condition ? Object( params ) : getObject() );








簡単なテストプログラムは、次を出力します(作成および解放されたクラスAインスタンスの数に注意してください)。



main() start

A::A( 'on stack' )

B::B()

A::A( 'static instance' )

A::A( 'copy of static instance' )

B::boo()

B::~B()

A::~A( 'on stack' )

main() end

A::~A( 'static instance' )

Class A created 3 times and destroyed 2 times

Class B created 1 times and destroyed 1 times








この問題は、gcc 3.2.3で32ビットコードを生成する場合にのみ発生し、64ビットコードまたはそれ以降のバージョンのコンパイラで生成されるコードでは発生しませんでした。



私は私ではない、私の記憶は私のものではない


かつて、データを収集してサーバーに転送するためのプログラムをサポートし、完成させました。 このプログラムは、Linuxを含む多数のプラットフォームで機能しました。 このプログラムは商用であったため、Linuxの最小サポートバージョン(この場合はgcc 3.3.x)に対応するコンパイラによってコンパイルされ、実行可能ファイルが提供されました。



ある時点で、私たちのQA部門は、メモリ不足によるプログラムクラッシュを登録し、管理することさえできました(長時間、数日間、高負荷でのテスト)-プロセスは3GBのメモリを消費し、正常にクラッシュして、同じサイズのコアダンプを作成しました。 さらに、メモリ使用量の爆発的な増加は、サッドエンドの10〜15分前に発生し、その時点でプロセッサの負荷は約12%でした(サーバー上に4つのデュアルコアプロセッサがあったため、1サイクルでスピンするスレッドは12.5%)。



ドロップされたスレッドの呼び出しスタックは、コピーコンストラクター内の完全に些細なコードを指していますが、これが何らかの形で例外的な状況に関連していることは明らかでした。



 (gdb) where #0 0xffffe410 in __kernel_vsyscall () #1 0xb7dbd8d0 in raise () from /lib/libc.so.6 #2 0xb7dbeff3 in abort () from /lib/libc.so.6 #3 0xb7f86da5 in dlopen () from /usr/lib/libstdc++.so.5 #4 0xb7f86de2 in std::terminate () from /usr/lib/libstdc++.so.5 #5 0xb7f85e89 in __cxa_allocate_exception () from /usr/lib/libstdc++.so.5 #6 0xb78f7f07 in Uuid::Uuid () from .../lib32/libourlibrary.so #7 0xb782409d in ...
      
      







メモリデバッガまたは実行可能ファイルのデバッグバージョンで問題を再現しようとしても失敗しました-問題はなくなりました。 はい、複製自体は高価でした-通常、問題が発生する前に2〜4日間負荷下でプロセスをテストする必要がありました。負荷下のテストに同じインフラストラクチャを必要とする他のコンポーネントのテストを遅らせました。



同様の呼び出しスタックを検索しても、結果はほとんど得られず、状況は行き詰まりました。 デバッガを使用せずに、何らかの方法でメモリリークの原因を見つける必要がありました。



私が出会ったC ++のメモリ割り当てライブラリの実装では、ライブラリのデバッグバージョンでクラスの新しいインスタンスに割り当てられた各ブロックは、このクラスの名前を含む行でマークされていました。 この方法により、コアダンプyによって、どのタイプのオブジェクトが割り当てられたかを簡単に判断できました。 最初に文字列プログラムを実行し、次にsortで並べ替えることにより、コアダンプファイルに含まれる文字列を検索しようとしました。



不思議なことに、ファイルには「++ CCUNG0o」という形式の32,123,751行が含まれていることがわかりました。これらの行だけが、サイズが3 GBのファイルで約275 MBを占めていました。 ファイル内のこれらの行を検索すると、そのような署名のそれぞれが96バイトブロック(96b * 32,000,000 = 3Gb !!!)を開始することが判明しました。



ブロックは、先頭がゼロの反転文字列「++ CCUNG0o」で始まり(反転しているため)、異なる場所で4バイトのペアのみが異なり、明らかにリンクリストを形成します。



0x248b818: 0x00 0x2b 0x2b 0x43 0x43 0x55 0x4e 0x47 <<== «++CCUNG0o»

0x248b820: 0x30 0x6f 0xf8 0xb7 0x00 0x00 0x00 0x00

0x248b828: 0x6c 0xa8 0x78 0xb7 0x00 0x00 0x00 0x00

0x248b830: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x248b838: 0xa8 0x0e 0xd3 0xb7 0x3c 0xa2 0xfa 0xb7

0x248b840: 0xe8 0xae 0x82 0xb7 0x65 0x00 0x00 0x00

0x248b848: 0xc0 0x0e 0xd3 0xb7 0x38 0x9c 0xc8 0xb7

0x248b850: 0xf4 0x9b 0x04 0x08 0xe4 0x9c 0x04 0x08

0x248b858: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x248b860: 0x03 0x00 0x00 0x00 0xbe 0x80 0x79 0xb7

0x248b868: 0x58 0x80 0x79 0xb7 0x54 0xa9 0x78 0xb7

0x248b870: 0x98 0xb8 0x48 0x02 0x00 0x00 0x00 0x00



0x248b878: 0x00 0x2b 0x2b 0x43 0x43 0x55 0x4e 0x47 <<== «++CCUNG0o»

0x248b880: 0x30 0x6f 0xf8 0xb7 0x00 0x00 0x00 0x00

0x248b888: 0x6c 0xa8 0x78 0xb7 0x00 0x00 0x00 0x00

0x248b890: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x248b898: 0xa8 0x0e 0xd3 0xb7 0x3c 0xa2 0xfa 0xb7

0x248b8a0: 0xff 0xff 0xff 0xff 0x65 0x00 0x00 0x00

0x248b8a8: 0xc0 0x0e 0xd3 0xb7 0x38 0x9c 0xc8 0xb7

0x248b8b0: 0xf4 0x9b 0x04 0x08 0xe4 0x9c 0x04 0x08

0x248b8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x248b8c0: 0x03 0x00 0x00 0x00 0xbe 0x80 0x79 0xb7

0x248b8c8: 0x58 0x80 0x79 0xb7 0x54 0xa9 0x78 0xb7

0x248b8d0: 0xf8 0xb8 0x48 0x02 0x00 0x00 0x00 0x00








最初はインターネットでこのような行を検索しても有用な結果は得られませんでしたが、次のスニペットでhttp://www.opensource.apple.com/へのリンクが見つかりました(残念ながらもう機能していません):



 // This is the exception class we report -- "GNUCC++\0". const _Unwind_Exception_Class __gxx_exception_class = ((((((((_Unwind_Exception_Class) 'G' << 8 | (_Unwind_Exception_Class) 'N') << 8 | (_Unwind_Exception_Class) 'U') << 8 | (_Unwind_Exception_Class) 'C') << 8 | (_Unwind_Exception_Class) 'C') << 8 | (_Unwind_Exception_Class) '+') << 8 | (_Unwind_Exception_Class) '+') << 8 | (_Unwind_Exception_Class) '\0');
      
      







その後、他のスレッドの呼び出しスタックに戻り、落下時にループで明確に動作していた非常に疑わしいスレッドを見つけました。このループも例外を処理します。



 (gdb) thread 20 [Switching to thread 20 (process 27635)]#0 0xb7e87921 in dl_iterate_phdr () from /lib/libc.so.6 (gdb) where #0 0xb7e87921 in dl_iterate_phdr () from /lib/libc.so.6 #1 0x0804e837 in _Unwind_Find_FDE (pc=0xb782409c, bases=0xb70209b4) at ../../gcc/unwind-dw2-fde-glibc.c:283 #2 0x0804c950 in uw_frame_state_for (context=0xb7020960, fs=0xb7020860) at ../../gcc/unwind-dw2.c:903 #3 0x0804cfbf in _Unwind_RaiseException_Phase2 (exc=0xbfde3f38, context=0xb7020960) at ../../gcc/unwind.inc:43 #4 0x0804d397 in _Unwind_Resume (exc=0xbfde3f38) at ../../gcc/unwind.inc:220 #5 0xb78f82b0 in Uuid::Uuid () from /home/'work/lib32/libourlibrary.so #6 0xb782409d in ...
      
      







その後、例外がスローされたソースコードをより詳しく調べ始め、コードのこのセクションで使用されている配置new演算子に問題があることを示唆しました。 単純なテストで仮定が確認されました-配置new演算子から呼び出されたオブジェクトのコンストラクターで例外が作成されると、無限ループと爆発的なメモリリークが発生しました。 テストは簡単でした-コードの問題のあるセクションの例外的な状況をシミュレートしましたが、残念ながら、アプリケーションのコンテキストから抜け出すのは簡単ではありませんでした-小さなテストプログラムでは問題を再現できませんでした。 また、この問題は、使用したコンパイラバージョンにのみ存在することが判明しました。つまり、gcc 3.4では問題はありませんでした。 すでにかなりの費用がかかっている特定の問題が解決されたことを祝うために、さらなる調査が削減されました。



かなりの漏れではない


一度、ソフトウェア製品の更新後、クライアントでの予備テストにより、以前には発生しなかったメモリリークが検出されました。



プログラムはSolarisで負荷がかかった状態で機能し、メモリ使用量は10〜100 MBの増分で急激に増加し、まれに-時には2〜3日ごと、時には2〜3回増加しました。 遅かれ早かれ、プロセスで使用されるメモリは2 GB以上に増加しました。 この場合、負荷がゼロに減少しても、使用されるメモリ(RSSおよびVSS値)は決して減少しません。



サーバーへの直接アクセスは取得できませんでしたが(たとえばlibumemを構成するため)、QAで問題を再現することはできませんでした。 良い点は、クロップされたコアダンプファイル(プロセスメモリのスナップショット)を取得できたことです。 しかし、コアダンプファイルの分析では実質的に何も得られませんでした。メモリの割り当て時に、コールスタックが不足しているため、コールスタックが低下しました。 同時に、ほとんどのメモリは使用されませんでした。コアダンプファイルのほぼ全体が、ゼロで詰まった4 KBの空白ページで占められていました。



状況は奇妙でしたが、徐々にシステムのさまざまなコンポーネントを削除し、ログファイルを思慮深く分析することで、イベントの状況が復元されました。



システムの更新時に、クライアントプログラムの1つが、システムの監視を目的としたメッセージの一部の送信を停止しました。 偶然、欠落したメッセージはトランザクションの終了を通知し、内部バッファーを消去する必要がありました。 したがって、負荷が増加すると、バッファリングされたメッセージが蓄積され始め、ペアを待機します。 偶然にも、この場合、メッセージは通常は大きく(数百バイトではなく数十キロバイト)、クライアントプロセスの数は比較的大きかった(通常の2-5の代わりに数十)。 しかし、実際には、これらのパラメーターはすべて、システムでサポートされる制限に完全に適合します(正常に機能することを条件とします)。 また、プログラムは内部バッファーのサイズを制限しませんでしたが、「失われた」トランザクションをクリアするメカニズムをサポートしました。10分以上経過したすべての不完全なメッセージは自動的に削除されました。 そして、このカウンターだけがエラーのため常に0を示しました-メカニズムはほとんど信じられないほどの例外的な状況のために設計され、ほとんど関与せず、十分にテストされていません。 クリーニング自体は適切に機能しましたが、多数のクライアントと大量の非標準メッセージによって引き起こされる大きなメモリ負荷のために、十分に効率的ではありませんでした。 問題の診断は、統計のカウンタに欠陥があるため非常に困難でした。



では、なぜ負荷が軽減されたときにメモリが戻らないのですか? 非常にシンプル-Solarisの標準プロセスメモリマネージャーは、プロセスのアドレス空間を増やすだけで、プロセスによって解放されたメモリを再利用のために予約し、サードパーティのオブザーバーの観点からプロセスでページを「ビジー」にします。



私たちの場合、問題のあるクライアントのアクティビティの単一のピークは、以前の最大値を超えていないため、ほとんどの場合、気付かないほど定期的に大きなメモリ割り当てにつながりました。 そして、最後の最大値を超えた場合にのみ、次のステップが取得され、それは決して減少しませんでした。 ピークの10分後、すべてのメモリが解放されましたが、外部からは見えませんでした。メモリのスナップショットのみが、メモリのほとんどがゼロで詰まっていて使用されていないことを示しました。



問題のあるクライアントを修正し、バッファオーバーフローから保護する前でも、ソリューションは簡単でした。「古い」トランザクションの経過時間は一時的に30秒に制限されていました。 ただし、主にログの統計が正しくないため、診断とトラブルシューティングにはかなりの時間がかかりました。



エピローグの代わりに


誰が実際に興味深いケースを持っていた-書いてください!



All Articles