
Cerita, seperti yang sering terjadi, dimulai dengan fakta bahwa salah satu layanan di server jatuh. Lebih tepatnya, proses itu dimatikan dengan memonitor penggunaan memori berlebih. Stok seharusnya lebih dari satu, yang berarti kami memiliki kebocoran memori.
Ada dump memori lengkap dengan informasi debug, ada log, tetapi tidak dapat direproduksi. Entah kebocorannya sangat lambat, atau skenario tergantung pada cuaca di Mars. Singkatnya, bug lain yang tidak direproduksi oleh tes, tetapi ditemukan di alam liar. Masih ada satu-satunya petunjuk nyata - dump memori.
Ide
Layanan asli ditulis dalam C ++ dan Perl, meskipun ini tidak memainkan peran khusus. Segala sesuatu yang dijelaskan di bawah ini berlaku untuk hampir semua bahasa.
Proses kami dari pernyataan masalah adalah untuk menyesuaikan dengan beberapa ratus megabyte RAM, dan selesai untuk melebihi 6 gigabytes. Jadi sebagian besar memori proses adalah objek bocor dan datanya. Hanya perlu untuk mengetahui jenis objek apa yang paling banyak di memori. Tentu saja, tidak ada daftar objek dengan informasi tipe di dump. Melacak hubungan dan membuat grafik seperti yang dilakukan pengumpul sampah hampir tidak mungkin. Tetapi kita tidak perlu memahami hash biner ini, tetapi untuk menghitung objek mana yang lebih. Objek kelas non-sepele memiliki pointer ke tabel metode virtual, dan semua objek dari kelas yang sama memiliki pointer yang sama. Berapa kali pointer ke kelas vtbl ditemukan dalam memori - begitu banyak objek dari kelas ini telah dibuat.
Selain vtbl, ada urutan lain yang sering terjadi: konstanta yang menginisialisasi bidang, header HTTP dalam fragmen string, pointer ke fungsi.
Jika Anda cukup beruntung untuk menemukan pointer, maka kita dapat menggunakan gdb untuk memahami apa yang ditunjukkannya (kecuali tentu saja ada karakter debug). Dalam hal data, Anda dapat mencoba melihatnya dan memahami di mana ini digunakan. Ke depan, saya perhatikan bahwa hal itu terjadi baik itu dan yang lain dan, dari sebuah fragmen garis sangat mungkin untuk memahami apa yang menjadi bagian dari protokol ini, dan di mana perlu untuk menggali lebih jauh.
Idenya dimata-matai, dan implementasi pertama disalin dari stackoverflow. https://stackoverflow.com/questions/7439170/is-there-a-way-to-find-leaked-memory-using-a-core-file
hexdump core.10639 | awk '{printf "%s%s%s%s\n%s%s%s%s\n", $5,$4,$3,$2,$9,$8,$7,$6}' | sort | uniq -c | sort -nr | head
Script bekerja selama sekitar 15 menit pada dump kami, mengembalikan banyak baris, dan ... tidak ada. Bukan satu pointer, tidak ada yang berguna.
Diurutkan
Pengembangan Stackoverflow-driven memiliki kekurangannya. Anda tidak bisa hanya menyalin skrip dan berharap semuanya akan berfungsi. Dalam skrip khusus ini, beberapa jenis penataan byte segera menarik perhatian. Pertanyaannya juga muncul, mengapa permutasi oleh 4. Anda tidak perlu menjadi super-spesialis untuk memahami bahwa permutasi seperti itu tergantung pada platform: bitness dan byte order.
Untuk memahami persis bagaimana penampilannya, Anda perlu memahami format file dump memori, LITTLE- dan BIG-endian, atau Anda dapat mengatur ulang byte dalam potongan yang ditemukan dengan cara yang berbeda dan memberikan gdb. Astaga! Dalam urutan langsung, byte gdb melihat karakter dan mengatakan bahwa itu adalah pointer ke suatu fungsi!
Dalam kasus kami, itu adalah penunjuk ke salah satu fungsi baca dan tulis di buffer openssl. Untuk menyesuaikan input dan output digunakan pendekatan sistem OOP - struktur dengan seperangkat pointer ke fungsi, yang merupakan jenis antarmuka atau lebih tepatnya vtbl. Struktur dengan pointer ini ternyata sangat banyak. Melihat dari dekat pada kode yang bertanggung jawab untuk mengatur struktur ini dan membuat buffer memungkinkan kami menemukan kesalahan dengan cepat. Ternyata, di persimpangan C ++ dan C tidak ada objek RAII dan jika terjadi kesalahan, pengembalian awal tidak meninggalkan kesempatan untuk membebaskan sumber daya. Tidak ada yang menduga memuat layanan dengan jabat tangan ssl yang salah pada waktu yang tepat, sehingga mereka melewatkannya. Cara memutar 6 gigabytes ssl handshakes yang salah juga menarik, tetapi seperti yang mereka katakan, ini adalah kisah yang sama sekali berbeda. Masalahnya teratasi.
topleaked
Script ternyata bermanfaat, tetapi masih memiliki kelemahan serius untuk sering digunakan: sangat lambat, tergantung platform, kemudian ternyata file dump juga dengan offset yang berbeda, sulit untuk menafsirkan hasilnya. Tugas menggali di binary dump tidak cocok dengan bash, jadi saya mengubah bahasa pemrograman ke D. Pilihan bahasa sebenarnya karena keinginan egois untuk menulis dalam bahasa favorit Anda. Yah, rasionalisasi pilihannya adalah ini: kecepatan dan konsumsi memori sangat penting, jadi Anda memerlukan bahasa yang dikompilasi asli, dan itu biasa untuk menulis D lebih cepat daripada C atau C ++. Nanti dalam kode itu akan terlihat jelas. Maka proyek topleaked lahir.
Instalasi
Tidak ada rakitan biner, jadi dengan satu atau lain cara Anda perlu merakit proyek dari sumber. Untuk melakukan ini, Anda memerlukan kompiler D. Ada tiga opsi: dmd adalah kompiler referensi, ldc didasarkan pada llvm dan gdc, termasuk dalam gcc, mulai dari versi 9. Jadi Anda mungkin tidak perlu menginstal apa pun jika Anda memiliki gcc terbaru. Jika Anda menginstal, maka saya merekomendasikan ldc, karena mengoptimalkan lebih baik. Ketiganya dapat ditemukan di situs web resmi .
Manajer paket dub disediakan dengan kompiler. Menggunakannya, topleaked diinstal dengan satu perintah:
dub fetch topleaked
Di masa depan, kami akan menggunakan perintah untuk memulai:
dub run topleaked -brelease-nobounds -- <filename> [<options>...]
Agar tidak mengulangi dub run dan argumen compiler brelease-nobounds, Anda dapat mengunduh sumber dari github dan mengumpulkan file yang dapat dieksekusi:
dub build -brelease-nobounds
Di root folder proyek akan muncul topleaked.
Gunakan
Mari kita ambil program C ++ sederhana dengan kebocoran memori.
#include <iostream> #include <assert.h> #include <unistd.h> class A { size_t val = 12345678910; virtual ~A(){} }; int main() { for (size_t i =0; i < 1000000; i++) { new A(); } std::cout << getpid() << std::endl; sleep(200); }
Kami menyelesaikannya melalui kill -6, daripada kami mendapatkan memori dump. Sekarang Anda dapat menjalankan topleaked dan melihat hasilnya
./toleaked -n10 leak.core
Opsi -n adalah ukuran atas yang kita butuhkan. Biasanya, nilai antara 10 dan 200 masuk akal, tergantung pada seberapa banyak "sampah" yang ada. Format output default adalah top-by-line top dalam bentuk yang dapat dibaca manusia.
0x0000000000000000 : 1050347 0x0000000000000021 : 1000003 0x00000002dfdc1c3e : 1000000 0x0000558087922d90 : 1000000 0x0000000000000002 : 198 0x0000000000000001 : 180 0x00007f4247c6a000 : 164 0x0000000000000008 : 160 0x00007f4247c5c438 : 153 0xffffffffffffffff : 141
Tidak banyak gunanya, kecuali bahwa kita dapat melihat angka 0x2dfdc1c3e, yang juga 12345678910, yang terjadi sejuta kali. Ini sudah cukup, tetapi saya ingin lebih. Untuk melihat nama kelas dari objek yang bocor, Anda dapat mengirim hasilnya ke gdb hanya dengan mengarahkan aliran output standar ke input gdb dengan file dump terbuka. -ogdb - opsi mengubah format menjadi gdb yang bisa dimengerti.
$ ./topleaked -n10 -ogdb /home/core/leak.1002.core | gdb leak /home/core/leak.1002.core ...< gdb > #0 0x00007f424784e6f4 in __GI___nanosleep (requested_time=requested_time@entry=0x7ffcfffedb50, remaining=remaining@entry=0x7ffcfffedb50) at ../sysdeps/unix/sysv/linux/nanosleep.c:28 28 ../sysdeps/unix/sysv/linux/nanosleep.c: No such file or directory. (gdb) $1 = 1050347 (gdb) 0x0: Cannot access memory at address 0x0 (gdb) No symbol matches 0x0000000000000000. (gdb) $2 = 1000003 (gdb) 0x21: Cannot access memory at address 0x21 (gdb) No symbol matches 0x0000000000000021. (gdb) $3 = 1000000 (gdb) 0x2dfdc1c3e: Cannot access memory at address 0x2dfdc1c3e (gdb) No symbol matches 0x00000002dfdc1c3e. (gdb) $4 = 1000000 (gdb) 0x558087922d90 <_ZTV1A+16>: 0x87721bfa (gdb) vtable for A + 16 in section .data.rel.ro of /home/g.smorkalov/dlang/topleaked/leak (gdb) $5 = 198 (gdb) 0x2: Cannot access memory at address 0x2 (gdb) No symbol matches 0x0000000000000002. (gdb) $6 = 180 (gdb) 0x1: Cannot access memory at address 0x1 (gdb) No symbol matches 0x0000000000000001. (gdb) $7 = 164 (gdb) 0x7f4247c6a000: 0x47ae6000 (gdb) No symbol matches 0x00007f4247c6a000. (gdb) $8 = 160 (gdb) 0x8: Cannot access memory at address 0x8 (gdb) No symbol matches 0x0000000000000008. (gdb) $9 = 153 (gdb) 0x7f4247c5c438 <_ZTVN10__cxxabiv120__si_class_type_infoE+16>: 0x47b79660 (gdb) vtable for __cxxabiv1::__si_class_type_info + 16 in section .data.rel.ro of /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (gdb) $10 = 141 (gdb) 0xffffffffffffffff: Cannot access memory at address 0xffffffffffffffff (gdb) No symbol matches 0xffffffffffffffff. (gdb) quit
Membaca itu tidak terlalu sederhana, tetapi memungkinkan. Garis dalam bentuk $ 4 = 1.000.000 mencerminkan posisi di atas dan jumlah kemunculan yang ditemukan. Di bawah ini adalah hasil dari menjalankan x dan simbol info untuk nilai. Di sini kita dapat melihat bahwa vtable untuk A terjadi sejuta kali, yang sesuai dengan sejuta objek bocor dari kelas A.
Untuk menganalisis bagian file (jika terlalu besar), opsi offset dan batas ditambahkan - mulai dari mana dan berapa banyak byte untuk dibaca.
Hasil
Utilitas yang dihasilkan terasa lebih cepat daripada skrip. Anda masih harus menunggu, tetapi tidak pada skala kenaikan untuk teh, tetapi beberapa detik sebelum bagian atas muncul di layar. Saya benar-benar yakin bahwa algoritma dapat ditingkatkan secara signifikan, dan operasi input dan output yang berat dapat dioptimalkan secara signifikan. Tapi ini masalah perkembangan masa depan, sekarang semuanya bekerja dengan baik.
Berkat opsi -ogdb dan pengalihan di gdb, kami segera mendapatkan nama dan nilai, kadang-kadang bahkan nomor baris, jika kami beruntung mendapatkan fungsinya.
Konsekuensi yang jelas, tetapi sangat tak terduga, dari solusi frontal adalah cross-platform. Ya, topleaked tidak tahu tentang urutan byte, tetapi karena tidak mem-parsing format file, tetapi hanya membaca file byte demi byte, itu dapat digunakan pada Windows atau sistem apa pun dengan format dump memori apa pun. Hanya diperlukan agar data disejajarkan di dalam file.
Bahasa D.
Saya ingin secara terpisah mencatat pengalaman mengembangkan program semacam itu di D. Versi kerja pertama ditulis dalam hitungan menit. Saya harus mengatakan bahwa sejauh ini algoritma utama hanya membutuhkan tiga baris:
auto all = input.sort; ValCount[] res = new ValCount[min(all.length, maxSize)]; return all.group.map!((p) => ValCount(p[0],p[1])) .topNCopy!"a.count>b.count"(res, Yes.sortOutput);
Semua berkat rentang malas dan keberadaan algoritma yang siap pakai di atasnya di perpustakaan standar, seperti grup dan topN.
Kemudian, penguraian argumen baris perintah, format output, dan segala sesuatu yang bertele-tele, tetapi juga ditulis dengan cepat, tumbuh di atas. Kecuali jika pembacaan file tersebut ternyata aneh, karena gaya umum.
Pada versi terbaru saat ini, flag --find muncul untuk pencarian substring yang biasa, yang tidak berhubungan dengan frekuensi sama sekali. Karena hal sepele ini, kode ini terlihat semakin besar ukurannya, tetapi dengan peluang tinggi fitur tersebut akan dihapus dan kode tersebut akan kembali ke keadaan semula yang sederhana.
Secara total, biaya tenaga kerja sebanding dengan bahasa scripting, dan jauh lebih baik dalam kinerjanya. Secara potensial, Anda dapat membawanya semaksimal mungkin, karena kode yang sama dalam C dan D akan bekerja sama pada kecepatan yang sama.
Indikasi dan kontraindikasi untuk digunakan
- Topleaked diperlukan untuk mencari kebocoran ketika hanya ada dump memori proses saat ini, tetapi tidak ada cara untuk mereproduksi di bawah pembersih.
- Ini bukan valgrind lain dan tidak mengklaim sebagai analisis dinamis.
- Pengecualian menarik untuk komentar sebelumnya mungkin kebocoran sementara. Artinya, memori dibebaskan, tetapi terlambat (ketika server dihentikan, misalnya). Kemudian Anda dapat menghapus dump pada waktu yang tepat dan menganalisis. Valgrind atau asan, bekerja pada saat proses berakhir, dapat melakukan ini lebih buruk.
- Hanya mode 64-bit. Dukungan untuk bit dan urutan byte lainnya ditunda untuk masa depan.
Masalah yang Diketahui
Selama pengujian, file dump digunakan yang diterima dengan mengirimkan sinyal ke proses. Dengan file seperti itu, semuanya bekerja dengan baik. Ketika dump dihapus, perintah gcore menulis beberapa header ELF lainnya dan offset dengan jumlah byte yang tidak terbatas terjadi. Artinya, nilai-nilai pointer tidak selaras dengan 8 dalam file, sehingga hasil yang tidak berarti diperoleh. Untuk solusinya, opsi offset diperkenalkan - untuk membaca file bukan yang pertama, tetapi digeser oleh byte offset (biasanya 4).
Untuk mengatasi ini, saya berencana untuk menambahkan membaca hasil objdump -s dari stdin. Baik, sambungkan libelf dan parsing sendiri, tetapi itu akan mematikan "cross-platform", dan stdout lebih fleksibel dan lebih dekat dengan cara unix.
Referensi
Proyek Github
Penyusun D
Pertanyaan asli tentang stackoverflow