1772364531
2026-03-01 09:27:00
2026-02-28
TLDR; C++ 標準ライブラリは、早期に例外処理インフラストラクチャをセットアップし、malloc がメモリ不足になった場合に例外用のメモリを割り当てられるように、「緊急プール」にメモリを割り当てます。
私は、独自の malloc 実装を使用してカスタム メモリ アロケーターをハッキングしたり実験したりすることに時間 (の一部) を費やすのが好きです。単体テストは正確さを知るために役立ちますが、究極のテストは、実際のプログラムでアロケーターがどのように動作するかを確認することです。 Linux では、デフォルトの malloc をオーバーライドするのは驚くほど簡単です。標準の割り当て関数 (malloc、calloc、realloc、free、および malloc_usable_size などのユーティリティ) をラップし、実装を共有ライブラリにコンパイルして、次を使用します。 LD_PRELOAD プログラムに最初にロードするように強制します。たとえば、次のような簡単なコマンドを使用してアロケータをテストできます。
LD_PRELOAD=/home/joel/mymalloc/libmymalloc.so ls
プログラムがメモリをどのように割り当てるかをより深く理解するために、すべての割り当てリクエストのサイズをファイルに記録するデバッグ ツールを構築しました。 malloc を実装するときにこのようなデバッグ ツールを作成する場合は、内部で malloc を使用して出力を記録しないように注意する必要があります。そうしないと、無限ループやクラッシュが発生する危険があります。これを解決するために、私はスタック割り当てバッファーと creat、write、snprintf などの低レベル関数を使用して、データを安全にキャプチャしています。
$ LOG_ALLOC=log.txt LD_PRELOAD=/home/joel/mymalloc/libmymalloc.so ls
さまざまなプログラムにわたる割り当てパターンを分析しているときに、異常なことに気づきました。最初の割り当ては常に 73728 バイト (72 KB) であるということです。デバッグ ログで確認されたように、テストしたすべてのプログラムで次のような動作が見られました。
$ head -n 1 log.txt
73728
malloc への最初の呼び出しを追跡するために、gdb を使用して独自の malloc 関数にブレークポイントを設定し、バックトレースを検査します。
簡単な補足: 「malloc」シンボルにブレークポイントを設定すると、独自の malloc だけでなく、ダイナミック リンカー (RTLD) の内部 malloc もトリガーされるため、より具体的にする必要があります。 RTLD は、libc (または独自の malloc) がロードされる前に、初期のメモリ割り当てに独自の最小限の malloc 実装を使用します。 glibc を参照することをお勧めします。 elf/dl-minimal-malloc.c、非常に親しみやすいです。
$ gdb --args ls
...
(gdb) set environment LD_PRELOAD=/home/joel/mymalloc/libmymalloc.so
(gdb) b MallocWrapper.cpp:malloc
...
(gdb) r
Starting program: /usr/bin/ls
Breakpoint 1, malloc (size=73728) at src/MallocWrapper.cpp:44
...
(gdb) bt
#0 malloc (size=73728) at src/MallocWrapper.cpp:44
#1 0x00007ffff78bd17f in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#2 0x00007ffff7fca71f in call_init (l=, argc=argc@entry=1, argv=argv@entry=0x7fffffffdce8, env=env@entry=0x7fffffffdcf8) at ./elf/dl-init.c:74
#3 0x00007ffff7fca824 in call_init (env=, argv=, argc=, l=) at ./elf/dl-init.c:120
#4 _dl_init (main_map=0x7ffff7ffe2e0, argc=1, argv=0x7fffffffdce8, env=0x7fffffffdcf8) at ./elf/dl-init.c:121
#5 0x00007ffff7fe45a0 in _dl_start_user () from /lib64/ld-linux-x86-64.so.2
# ...
libstdc++ の詳細
バックトレースにより、最初の 72 KB 割り当てが libstdc++ からのものであることが明らかになりました。デバッグ シンボルを追加すると、対象を少し絞り込むことができますが、インライン化が原因で、malloc 呼び出しの原因となっている関数を正確に特定するのは困難です。私たちが知っているのは、malloc 呼び出しがその後の何かから来ているということだけです。 __pool_alloc_base::_M_allocate_chunk。
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
...
0x00007ffff78aa600 0x00007ffff79eef42 Yes (*) /lib/x86_64-linux-gnu/libstdc++.so.6
...
(*): Shared library is missing debugging information.
(gdb) add-symbol-file /lib/x86_64-linux-gnu/debug/libstdc++.so.6 0x00007ffff78aa600
...
Reading symbols from /lib/x86_64-linux-gnu/debug/libstdc++.so.6...
(gdb) bt
#0 malloc (size=73728) at src/MallocWrapper.cpp:44
#1 0x00007ffff78bd17f in __gnu_cxx::__pool_alloc_base::_M_allocate_chunk (this=0x7ffff79ef08d <:filesystem::filesystem_error::_impl::>, __n=8, __nobjs=)
# at ../../../../../../src/libstdc++-v3/src/c++98/pool_allocator.cc:114
# ...
正確な呼び出し元を特定するには時間がかかりましたが、アセンブリ コード内の既知の関数と libstdc++ ソース コードを相互参照することで呼び出し元を絞り込みました。調査の結果、私はこうなった libstdc++-v3/libsupc++/eh_alloc.ccここで、「eh」は「例外処理」を表します。これには意味がありました。 _M_allocate_chunk おそらく例外がスローされる可能性がある最初のポイントであるため、例外処理インフラストラクチャを初期化する必要がありますが、これはおそらく遅延して行われます。
libstdc++-v3/src/c++98/pool_allocator.cc (読みやすいように簡略化してあります):
char* __pool_alloc_base::_M_allocate_chunk(size_t __n, int& __nobjs) {
// ...
__try {
_S_start_free = static_castchar*>(::operator new(__bytes_to_get));
} __catch(const std::bad_alloc&) { /* ... */ }
// ...
}
例外処理インフラストラクチャ(緊急プール)
私たちが確認している malloc への 72 KB の呼び出しは、いわゆる「緊急プール」用のメモリであり、プールのコンストラクターで割り当てられます。
libstdc++-v3/libsupc++/eh_alloc.cc (読みやすいように簡略化してあります):
pool::pool() noexcept {
// ...
arena_size = buffer_size_in_bytes(obj_count, obj_size);
if (arena_size == 0)
return;
arena = (char *)malloc (arena_size);
if (!arena) {
// If the allocation failed go without an emergency pool.
arena_size = 0;
return;
}
// Populate the free-list with a single entry covering the whole arena
first_free_entry = reinterpret_cast free_entry *> (arena);
new (first_free_entry) free_entry;
first_free_entry->size = arena_size;
first_free_entry->next = NULL;
}
通常、例外は malloc を介して直接割り当てられますが、malloc 呼び出しが失敗した場合、例外は代わりに緊急プールから割り当てられます。これにより、malloc が失敗した場合でも (緊急プールのサイズの範囲内で) 例外をスローできることが保証され、エラー処理の最後の防御線となります。緊急プールは、プログラムの起動時にメモリが利用できる可能性が高いため、遅延して割り当てられます。これが、この割り当てが一貫して見られる理由の説明になります。
libstdc++-v3/libsupc++/eh_alloc.cc (読みやすいように簡略化してあります):
extern "C" void *
__cxxabiv1::__cxa_allocate_exception(std::size_t thrown_size) noexcept {
// ..
void *ret = malloc (thrown_size);
#if USE_POOL
if (!ret)
ret = emergency_pool.allocate (thrown_size);
#endif
// ...
}
緊急時のプールのサイズ設定。なぜ 72 KB なのでしょうか?
ソース ファイルを見ると、緊急プールのサイズがどのように計算されるかについての簡単な説明があります。オブジェクトのサイズとオブジェクトの数は両方ともワードサイズに基づくため、64 ビット システムでは 8 バイトになります。
libstdc++-v3/libsupc++/eh_alloc.cc:
// The size of the buffer is N * (S * P + R + D), where:
// N == The number of objects to reserve space for.
// Defaults to EMERGENCY_OBJ_COUNT, defined below.
// S == Estimated size of exception objects to account for.
// This size is in units of sizeof(void*) not bytes.
// Defaults to EMERGENCY_OBJ_SIZE, defined below.
// P == sizeof(void*).
// R == sizeof(__cxa_refcounted_exception).
// D == sizeof(__cxa_dependent_exception).
// ...
#define EMERGENCY_OBJ_SIZE 6
#define EMERGENCY_OBJ_COUNT (4 * __SIZEOF_POINTER__ * __SIZEOF_POINTER__)
オブジェクトのサイズ (obj_size) とオブジェクトの数 (obj_count) は、 GLIBCXX_TUNABLES 環境変数。プール内のオブジェクトの数を変更することで、初期割り当てが実際に緊急プール用であることを経験的に検証できます。予想どおり、オブジェクトの数を変更すると、初期割り当てサイズが減少することがわかります。
$ GLIBCXX_TUNABLES=glibcxx.eh_pool.obj_count=10 \
LOG_ALLOC=log.txt \
LD_PRELOAD=/home/joel/mymalloc/libmymalloc.so \
ls
$ head -n 1 log.txt
2880
補足として、オブジェクトの数を 0 に設定することで、緊急プールを無効にする (つまり、割り当てない) こともできます。あるいは、構成することで、緊急プールに固定サイズの静的バッファーを使用するようにオプトインすることもできます。 --enable-libstdcxx-static-eh-pool libstdc++をビルドするとき。
私たちの調査結果と結びついているのは、特にメモリ管理のバグを検出できる人気のあるツールである Valgrind で観察された動作です。レディットユーザー ismbks r/cpp_questions への投稿 「なぜ私のプログラムは何もしていないのに最大 73kB のメモリを割り当てるのですか?」これは、緊急プールに無条件に割り当てられているのと同じバイト数です。
==1174489== HEAP SUMMARY:
==1174489== in use at exit: 0 bytes in 0 blocks
==1174489== total heap usage: 1 allocs, 1 frees, 73,728 bytes allocated
ただし、古い Valgrind バージョンでは、このメモリは適切に解放されるのではなく、「まだ到達可能」であるように見えました。 「まだ到達可能な」メモリは技術的にはリークではありません (プログラムはまだメモリへの参照を持っています) が、誤解を招く可能性があります。の投稿を参照してください スタックオーバーフロー この動作の詳細を説明します。興味深いことに、この人には 72 KB ではなく 71 KB の割り当てが表示されています。
==8511== HEAP SUMMARY:
==8511== in use at exit: 72,704 bytes in 1 blocks
==8511== total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8511==
==8511== LEAK SUMMARY:
==8511== definitely lost: 0 bytes in 0 blocks
==8511== indirectly lost: 0 bytes in 0 blocks
==8511== possibly lost: 0 bytes in 0 blocks
==8511== still reachable: 72,704 bytes in 1 blocks
==8511== suppressed: 0 bytes in 0 blocks
多くの開発者は、この動作をメモリ リークと誤って解釈し、不必要な混乱を引き起こしています。これに対処するために、新しい Valgrind バージョンではクリーンアップ中に緊急プールを明示的に解放し、より明確なレポートを提供するようになりました。これは、特に Valgrind などのツールのために追加された、以下に示すメカニズムを通じて実装されます。
/* g++ mangled __gnu_cxx::__freeres yields -> _ZN9__gnu_cxx9__freeresEv */
extern void _ZN9__gnu_cxx9__freeresEv(void) __attribute__((weak));
if (((to_run & VG_RUN__GNU_CXX__FREERES) != 0) &&
(_ZN9__gnu_cxx9__freeresEv != NULL)) {
_ZN9__gnu_cxx9__freeresEv();
}
libstdc++-v3/libsupc++/eh_alloc.cc (読みやすいように簡略化してあります):
namespace __gnu_cxx {
__attribute__((cold)) void __freeres() noexcept {
#ifndef _GLIBCXX_EH_POOL_STATIC
if (emergency_pool.arena) {
::free(emergency_pool.arena);
emergency_pool.arena = 0;
}
#endif
}
}
緊急プールに割り当てられたメモリは、カスタム アロケーターをテストする際に 72 KB の割り当てを一貫して観察できた理由を説明しています。カスタム アロケータを C++ で実装したため、プログラム呼び出しごとに緊急プールを初期化する libstdc++ に本質的に依存します。興味深いことに、アロケータを代わりに C で書いていたら、いくつかの一般的な malloc 実装は C で実装されています (ミマロック、 ジェマロック)、この初期割り当ては、libstdc++ に対して明示的にリンクする C++ バイナリをテストするときにのみ表示されます。
異なる割り当てサイズ (たとえば、72 KB ではなく 71 KB) が表示されたり、割り当てがまったく表示されない場合があります。 libstdc++ の異なるバージョン、代わりに libc++ を使用する、コンパイラ フラグなどの要因により、変動が生じる可能性があります。それでも、ほとんどの場合、緊急プール用のメモリが早期に割り当てられ、環境に応じてサイズや動作が異なる可能性があります。
メモリ割り当てを扱うとすぐにわかるように、ほとんどすべての作業でメモリを割り当てる必要があります。太古の昔から、RTLD では libc がまだロードされていないため独自の malloc が必要でした。また、独自のプール アロケータにメモリを割り当てるために malloc のみを使用する緊急プールの場合にも必要でした。
コードを調べてこれをつなぎ合わせるのはやりがいがあり、楽しかったです。私と同じようにあなたも旅を楽しんでいただければ幸いです。
#最初の #割り当てが常に #になるのはなぜですか