Malloc実隓

画像



ご存知のように、珟代のx86_64およびARMアヌキテクチャでは、幞いなこずにchar near*



ずint huge*



char near*



時間が経過したため、プロセスの仮想メモリは線圢で連続的です。 仮想メモリはペヌゞに分割されたす。通垞のサむズは4 KiBであり、デフォルトでは物理メモリにマッピングされないためマッピング、それらの操䜜は機胜したせん。 プロセスの珟圚衚瀺されおいるアドレス範囲を確認するには、Linux look / proc / <pid> / maps、OS X vmmap <pid>で。 各アドレス間隔には、実行、曞​​き蟌み、読み取りの3皮類の保護がありたす。 ご芧のずおり、ロヌドアドレスLinuxのELFの.textセグメント、OS XのMach-Oの __TEXTに察応で始たる最初の間隔は読み取り可胜で実行可胜です-非垞に論理的です。 たた、スタックは他の間隔ず本質的に倉わらないこずがわかりたす。たた、最終アドレスから開始アドレスを枛算するこずで、サむズをすばやく蚈算できたす。 ペヌゞはmmap / munmapを䜿甚しお衚瀺され 、セキュリティはmprotectを䜿甚しお倉曎されたす。 「デヌタ」の単䞀の間隔のサむズを倉曎し、珟代のシステムでmmapによっお゚ミュレヌトされる過去の非掚奚の叀代の名残であるbrk / sbrkがただありたす。



mallocのすべおのPOSIX実装は、䞊蚘の関数に䜕らかの圢で䟝存しおいたす。 単玔にペヌゞを匷調衚瀺および解攟し、必芁なサむズを切り䞊げるこずに比べお、mallocには倚くの利点がありたす。





十分な理論 実際にmallocを感じたす。 3぀の実隓を実行したす。 䜜業はPOSIX互換OSで可胜になりたす。特に、䜜業はLinuxおよびOS Xでテストされおいたす。



mallocからNULL



ありきたりから始めたしょう。 コヌド内でlibcおよび他のラむブラリから関数を再定矩するず、リンカヌはlibcが動的に接続するかどうかを気にしたせんデフォルトではそうです、二重定矩を誓うこずはありたせん。 たずえば、次のようなコヌド



 #include <stdio.h> #include <stdlib.h> void* malloc(size_t size) { puts("malloc"); return NULL; } int main() { return (int)malloc(100500); }
      
      





「malloc」を出力し、戻りコヌドがnull echo $?



になりたす。 ただし、asprintfなど、mallocが呌び出される腞で関数を呌び出すずどうなるかを確認したしょう。



 // program.c #include <stdio.h> #include <stddef.h> void* malloc(size_t size) { puts("malloc"); return NULL; } int main() { char *s = NULL; asprintf(&s, "%d", 0); printf("%p\n", s); return 0; }
      
      





そしお、ここではリンカに倧きく䟝存したす。 ld / Linuxの堎合、印刷されたす



 malloc (nil)
      
      





mallocが呌び出され、再定矩され、printfのglibc実装ではmallocが䜿甚されないため。 glibcのmallocが匱い文字 __attribute __weakずしお宣蚀され、デフォルトの定矩が匷いELFに固有ため、再定矩が行われたした。 ただし、dyld / OS Xでは、動䜜が異なりたす。



 0x7fc1eb403230
      
      





実際、ケシのmallocは再定矩されおいたせん それは、マルチレベルdyld名前空間に関するものでなければなりたせん。 さあ、さあ...



 DYLD_FORCE_FLAT_NAMESPACE=1 ./program Segmentation fault: 11
      
      





䞍機嫌なBABAG BIG このケヌスは、明らかにint mainにさえ達したせんでした。 その理由は䜕ですか



 lldb lldb ./program (lldb) target create "./program" Current executable set to './program' (x86_64). (lldb) env DYLD_FORCE_FLAT_NAMESPACE=1 (lldb) r Process 11956 launched: './program' (x86_64) Process 11956 stopped * thread #1: tid = 0x12e214, 0x00007fff9ebb9dcb libsystem_kernel.dylib`ioctl + 67, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) frame #0: 0x00007fff9ebb9dcb libsystem_kernel.dylib`ioctl + 67 libsystem_kernel.dylib`ioctl: -> 0x7fff9ebb9dcb <+67>: movq %rcx, -0xb8(%rbp) 0x7fff9ebb9dd2 <+74>: movq %rdx, -0xc0(%rbp) 0x7fff9ebb9dd9 <+81>: leaq -0xd0(%rbp), %rax 0x7fff9ebb9de0 <+88>: movq %rax, -0x10(%rbp) (lldb) bt * thread #1: tid = 0x12e214, 0x00007fff9ebb9dcb libsystem_kernel.dylib`ioctl + 67, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) * frame #0: 0x00007fff9ebb9dcb libsystem_kernel.dylib`ioctl + 67 frame #1: 0x00007fff9a20f2c8 libsystem_c.dylib`isatty + 43 frame #2: 0x00007fff9a222ac6 libsystem_c.dylib`__smakebuf + 60 frame #3: 0x00007fff9a237b4a libsystem_c.dylib`__swsetup + 155 frame #4: 0x00007fff9a221d52 libsystem_c.dylib`__sfvwrite + 73 frame #5: 0x00007fff9a2264c9 libsystem_c.dylib`puts + 144 frame #6: 0x0000000100000f0b program`malloc(size=4096) + 27 at program.c:6 frame #7: 0x00007fff9a222af6 libsystem_c.dylib`__smakebuf + 108 frame #8: 0x00007fff9a237b4a libsystem_c.dylib`__swsetup + 155 ... frame #130931: 0x0000000100000f0b program`malloc(size=4096) + 27 at program.c:6 frame #130932: 0x00007fff9a222af6 libsystem_c.dylib`__smakebuf + 108 frame #130933: 0x00007fff9a237b4a libsystem_c.dylib`__swsetup + 155 frame #130934: 0x00007fff9a221d52 libsystem_c.dylib`__sfvwrite + 73 frame #130935: 0x00007fff9a2264c9 libsystem_c.dylib`puts + 144 frame #130936: 0x0000000100000f0b program`malloc(size=8) + 27 at program.c:6 frame #130937: 0x00007fff5fc1d22e dyld`operator new(unsigned long) + 30 frame #130938: 0x00007fff5fc095a5 dyld`std::__1::vector >::insert(std::__1::__wrap_iter, char const* (* const&)(dyld_image_states, unsigned int, dyld_image_info const*)) + 343 frame #130939: 0x00007fff5fc04507 dyld`dyld::registerImageStateBatchChangeHandler(dyld_image_states, char const* (*)(dyld_image_states, unsigned int, dyld_image_info const*)) + 147 frame #130940: 0x00007fff8bb8089e libdyld.dylib`dyld_register_image_state_change_handler + 76 frame #130941: 0x00007fff8bb8065f libdyld.dylib`_dyld_initializer + 47 frame #130942: 0x00007fff982829fd libSystem.B.dylib`libSystem_initializer + 116 frame #130943: 0x00007fff5fc12feb dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 265 frame #130944: 0x00007fff5fc13164 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40 frame #130945: 0x00007fff5fc0f79d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 305 frame #130946: 0x00007fff5fc0f732 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 198 frame #130947: 0x00007fff5fc0f623 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 127 frame #130948: 0x00007fff5fc0f893 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 75 frame #130949: 0x00007fff5fc020f1 dyld`dyld::initializeMainExecutable() + 208 frame #130950: 0x00007fff5fc05e5d dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 3793 frame #130951: 0x00007fff5fc01276 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 512 frame #130952: 0x00007fff5fc01036 dyld`_dyld_start + 54
      
      





どんなフレヌム 130952nd はい、刀明したした、Stack Overflow たた、いく぀かの興味深いこずも孊びたした。dyldはC ++で蚘述されおおり、䜕らかの理由で同じmallocでメモリを割り圓お、再垰を䜜成したす。 stdoutバッファヌの初期化䞭に圌がこれを行うのは䞀床だけだず信じたい。 たあ、私たちはそれを眮き換えるこずを䜙儀なくされおいたす



 #include <stdio.h> #include <stddef.h> #include <unistd.h> void* malloc(size_t size) { write(STDOUT_FILENO, "malloc\n", 7); return NULL; } int main() { char *s = NULL; asprintf(&s, "%d", 0); printf("%p\n", s); return 0; }
      
      





開始しお確認したす。



 malloc malloc malloc Segmentation fault: 11
      
      





スタックフラグメント



 * thread #1: tid = 0x1309af, 0x00007fff5fc249ce dyld`_platform_bzero + 94, stop reason = EXC_BAD_ACCESS (code=1, address=0x8) * frame #0: 0x00007fff5fc249ce dyld`_platform_bzero + 94 frame #1: 0x00007fff5fc14045 dyld`calloc + 52 frame #2: 0x00007fff5fc0ce14 dyld`__cxa_get_globals + 100 frame #3: 0x00007fff5fc1ce7f dyld`__cxa_throw + 25 frame #4: 0x00007fff5fc1d267 dyld`operator new(unsigned long) + 87
      
      





そのため、ldが静的割り圓おで䜜成されおいるGNU / Linuxずは異なり、OS Xでアプリケヌションを実行するずきに倧量のヒヌプが集䞭的に䜿甚されたす。 たた、new挔算子によるメモリの割り圓おの倱敗に関する䟋倖をスロヌするず、callocが発生するこずもわかりたす。これは、mallocずれロbzeroの埋め蟌みの組み合わせです。 calloc実装が远い぀き、nullポむンタヌをチェックしたせんでした。 この知識があれば、OS Xが実際に「最埌のバむトたで」メモリを䜿い果たした堎合に起こる考えは、もはや私に䌑息を䞎えたせん。 明らかに、正しい論理的な解決策は、事前にstd :: bad_allocにメモリを割り圓おるこずです。



OK Google、OS Xでmallocを再定矩しおクラッシュしないようにするにはどうすればよいですか 実装の詳现に飛び蟌む必芁がありたす。 ポピヌのMallocは、ゟヌンにメモリを割り圓おたす。 最初は、デフォルトで1぀のゟヌンのみがあり、vmmapは出力の最埌にそれを衚瀺したす。 各ゟヌンには、malloc、free、およびreallocぞのポむンタヌが栌玍されたす。これにより、メモリ管理を柔軟に構成できたす。 デフォルトゟヌンを䜿甚しお、その䞭のmallocポむンタヌを眮き換えるこずができたす。



 #include <stdio.h> #include <stddef.h> #include <unistd.h> #include <malloc/malloc.h> #include <sys/mman.h> void* zone_malloc(struct _malloc_zone_t *zone, size_t size) { write(STDOUT_FILENO, "malloc\n", 7); return NULL; } int main() { malloc_zone_t* zone = malloc_default_zone(); mprotect(zone, sizeof(*zone), PROT_READ | PROT_WRITE); zone->malloc = zone_malloc; mprotect(zone, sizeof(*zone), PROT_READ); char *s = NULL; asprintf(&s, "%d", 0); printf("%p\n", s); return 0; }
      
      





mprotectに泚意しおください。 最初に、malloc_default_zoneは、曞き蟌み保護されおいるメモリ領域ぞのポむンタを返したす。 これは、mprotectを䜿甚せずにプログラムを実行し、デバッガヌずvmmapでクラッシュを調べるこずで簡単に確認できたす。 このような保護は遊び心のある手から埗られたす... PROT_READに戻るず、厳密に蚀えば、保護は倉曎できたせんでした。それは順序のために远加されたした。 印刷されるもの



 malloc malloc 0x0
      
      





printfがmallocを䜿甚したこずがわかりたすが、動的メモリなしで実行できる匷床が芋぀かり、それでもヌルポむンタヌが出力されたした。



ずころで、ゟヌンに぀いお。 glibcのMallocは、obstacksず呌ばれる同様のトレッキングを䜿甚したす。 䞀方では、それらを操䜜するための倚くの関数がありたすが、他方では、異なるobstackで異なるメモリ割り圓おアルゎリズムを䜿甚するこずはできたせん。



結論 OS XブヌトロヌダヌであるdyldはC ++で蚘述されおおり、int mainのかなり前にこのシステム䞊のプログラムの束を凊理したす。 Linuxのldでは、これは発生せず、ヒヌプぞの呌び出しはありたせん。



無効なmalloc



次に、新しい目暙を蚭定したしょう。mallocのバヌゞョンを実装する動的ラむブラリを䜜成したす。



 // hack_malloc.c #define _GNU_SOURCE #include <stdio.h> #include <stddef.h> #include <unistd.h> #include <sys/mman.h> void* malloc(size_t size) { write(STDOUT_FILENO, "malloc... ", 10); size += sizeof(size_t); int page_size = getpagesize(); int rem = size % page_size; if (rem > 0) { size += page_size - rem; } void* addr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); if (addr == MAP_FAILED) { write(STDOUT_FILENO, "fail\n", 5); return NULL; } write(STDOUT_FILENO, "ok\n", 3); *(size_t*)addr = size; return (size_t*)addr + 1; } void free (void *ptr) { write(STDOUT_FILENO, "free... ", 8); size_t* real_ptr = (size_t*)ptr - 1; if (!munmap(real_ptr, *real_ptr)) { write(STDOUT_FILENO, "ok\n", 3); } else { write(STDOUT_FILENO, "fail\n", 5); } }
      
      





ここでは、ペヌゞでメモリを割り圓おるずきに最も簡単なアプロヌチが実装されたす。 unmapに枡すものがあるように、ペヌゞの䞊郚にサむズを保存する必芁がありたす。 mmapフラグのMAP_ANONYMOUSは、実際のファむルをメモリにマップするのではなく、物理メモリをマップするこずを意味したす通垞、ファむルはmmapによっおメモリにマップしたす。これにより、䞀郚の操䜜が加速されたす。 ファむルの堎合、MAP_PRIVATEは個別のコピヌオンラむトコピヌを䜜成したすが、実際には䜕もしたせん。ドキュメントだけでMAP_PRIVATEたたはMAP_SHAREDが必芁です。 ちなみに、MAP_SHAREDを䜿甚するず、このコヌドもうたく機胜したす。



䟋で確認したす。



 // test.c #include <stdio.h> #include <stdlib.h> int main() { printf("start\n"); void* mem = malloc(100); printf("malloc() -> %p\n", mem); *(int*)mem = 0; free(mem); printf("end\n"); return 0; }
      
      





このように組み立おたす



 #Linux gcc -shared -o libhackmalloc.so -fPIC -std=c99 -O2 hack_malloc.c gcc test.c -std=c99 -L. -Wl,-rpath,. -lhackmalloc -O2 -o test # Mac OS X clang -dynamiclib -undefined suppress -flat_namespace -std=c99 -fPIC -O2 hack_malloc.c -o libhackmalloc.dylib clang test.c -std=c99 -L. -lhackmalloc -O2 -o test
      
      





起動時に、以䞋が衚瀺されたす。



 ./test start malloc... ok malloc() -> 0x10935b008 free... ok end
      
      





OS XずLinuxの出力は同じです。 OS Xの堎合、Windows 10むンタヌフェむスのように、dyld名前空間を考えおフラットにしたす。



 DYLD_FORCE_FLAT_NAMESPACE=1 ./test malloc... ok malloc... ok free... ok malloc... ok malloc... ok free... fail free... fail free... fail malloc... ok malloc... ok free... fail malloc... ok 70  free... fail 17  malloc... ok free... ok malloc... ok malloc... ok malloc... ok free... fail malloc... ok start malloc... ok malloc() -> 0x1035d9008 free... ok end
      
      





プログラムは機胜し、すでに良奜でした。 驚くべきこずは、int mainの前にmallocずfreeを呌び出す回数の明らかな矛盟です。 無料も䜕床も倱敗したした。 興味のある方は、デバッガでテストを実行し、ブレヌカヌを無料で䜿甚しお、dyldのダヌクラむフに぀いお倚くを孊ぶこずができたす。



結論 mallocの実装を30行で「盎接」曞くこずはかなり可胜です。



mallocのスパむ



DLLむンゞェクション手法を䜿甚しお、mallocを他の人のプログラムにむンゞェクトしおみたしょう。 Buddyなど、興味深いアルゎリズムは数倚くありたすが、ヒヌプの効果的な実装を独自に蚘述したくありたせん。 既補の実装を䜿甚するこずも可胜ですが、RTLD_NEXTトリックを適甚しおシステムmallocを参照したす。 次のコヌドを怜蚎しおください。



 // trace_malloc.c #define _GNU_SOURCE #include <dlfcn.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int fd = 0; void* (*__malloc)(size_t) = NULL; void* malloc(size_t size) { if (!__malloc) { __malloc = (void*(*)(size_t)) dlsym(RTLD_NEXT, "malloc"); } if (!fd) { fd = open("malloc.log", O_WRONLY | O_CREAT | O_TRUNC, 0666); } /* ... */ write(fd, record, sprintf(record, "%ld.%06ld\t%zu\n", sec, mcsec, size)); return __malloc(size); }
      
      





コヌドの完党版ぞのリンクは、蚘事の最埌に蚘茉されたす。 半分はclock_gettimeのクロスプラットフォヌム実装によっお消費され、2番目はclock_gettimeにアクセスするため、少し削枛しなければなりたせんでした。 すべおの魅力を1行で



 __malloc = (void*(*)(size_t)) dlsym(RTLD_NEXT, "malloc");
      
      





その䞭で「前の」mallocをロヌドしたす。 通垞、dlsymはロヌドされた動的ラむブラリから関数をプルするために䜿甚されたすが、マゞックRTLD_NEXTをラむブラリ蚘述子ずしお䜿甚したした。 厳密に蚀えば、POSIXにはありたせんが、実際には倚くのリンカヌでサポヌトされおいたす。 したがっお、真のmallocぞのポむンタヌを取埗し、保存しおから呌び出しお、結果を返したす。 途䞭で、すべおの呌び出しを蚘録したす。



hack_malloc.cず同じ方法でビルドし、Linuxで次のように䜿甚したす。



 LD_PRELOAD=/path/to/libtracemalloc.so program
      
      





パスは絶察パスでなければなりたせん。そうしないず、魔法は発生したせん。 LD_PRELOADは、プログラムがビルドされるメむンラむブラリの前に指定されたラむブラリを匷制的にロヌドする特別な環境倉数です。 この方法で、任意の関数を再定矩したり、誀っおリンクされたプログラム同じlib * .sonot foundメッセヌゞの起動に関する䞀時的な問題を解決できたす。



たずえば、Lsは玄2 KBのログを䜜成したす。 たた、whoamiは、未定矩のシンボルメッセヌゞdlsymを䌎いたす。これは、dlsymがlibdl.soで定矩されおいるためです。 たた、LD_PRELOADは挿入されたラむブラリの䟝存関係をロヌドしないため、-ldlを指定しおlibtracemallocをビルドするこずは意味がありたせん。 このようなこずをしなければなりたせん



 LD_PRELOAD=/usr/lib/.../libdl.so:/path/to/libtracemalloc.so whoami
      
      





たた、このような基本ナヌティリティの堎合でも、1 KBのメモリ割り圓おログが衚瀺されたす。



OK、OS Xはどうですか Dyldは、同様のこずを行うDYLD_INSERT_LIBRARIES環境倉数をサポヌトしおいたす。 私達は詊みたす



 DYLD_INSERT_LIBRARIES=/path/to/libtracemalloc.dylib ls
      
      





...機胜したせん、名前空間に぀いお芚えおおいおください



 DYLD_INSERT_LIBRARIES=/path/to/libtracemalloc.dylib DYLD_FORCE_FLAT_NAMESPACE=1 ls
      
      





...そしおたた残念。 すでに面癜い 重芁なのは、 System Integrity Protectionシステムプログラムを保護するこずです。 拡匵ファむル属性を䜿甚したこのメカニズムでは、ファむルの倉曎、コヌドの挿入、/ System、/ usrなどのパスのディベヌスは蚱可されたせん。幞いなこずに、/ usr / localは蚱されたした。



 lldb /bin/ls (lldb) target create "/bin/ls" Current executable set to '/bin/ls' (x86_64). (lldb) r error: process exited with status -1 (cannot attach to process due to System Integrity Protection)
      
      





SIPは無効にできたすが、簡単にできたす-興味のあるプログラムをディレクトリにコピヌしたす。



 cp $(which ls) . DYLD_INSERT_LIBRARIES=/path/to/libtracemalloc.dylib DYLD_FORCE_FLAT_NAMESPACE=1 ./ls
      
      





すでに機胜しおいたす。 結論ずしお、2皮類のメモリ割り圓おに関する有名な論文を蚌明したす。 mallocログを収集し、IPythonの基本コヌドを䜿甚しおサむズ分垃のヒストグラムを䜜成したしょう。



 %pylab import pandas, seaborn log = pandas.DataFrame.from_csv("malloc.log", sep='\t') log[log < 100].hist() log[log < 100].count() / len(log)
      
      





兞型的な画像は、サむズヒストグラムに衚瀺されたすY-呌び出し回数、X-サむズ、プログラム-clang







私は故意にテヌルを100バむトにカットしたした。これは、より倧きな割り圓おが非垞にたれであり、ヒストグラムに衚瀺されないためです。 したがっお、 ヒヌプ内のすべおのメモリ割り圓おの98は100バむト未満です。぀たり、優れたmallocは、少なくずも2぀の別個のドメむン倧きなオブゞェクト甚ずそれ以倖のナヌザヌ甚に察応する必芁がありたす。



プログラムを分析するために、䞊蚘のような自己アセンブルされたラむブラリを䜿甚するこずはできたせんが、既補のものを䜿甚するこずができたす。 たずえば、 tcmallocを䜿甚するず、束のプロファむルを䜜成したり、他の倚くの䟿利なこずを実行したりできたす 。



この蚘事のコヌドはgithubで入手できたす。 次回、実際の倧芏暡なプログラムを䜿甚しお、その動䜜䞭にメモリ割り圓おのログを収集し、再垰的ニュヌラルネットワヌクのLSTMモデルに基づいお予枬を詊みたす。



All Articles