Debugging memori rusak

23

Pertama, saya menyadari ini bukan pertanyaan gaya Q&A yang sempurna dengan jawaban absolut, tapi saya tidak bisa memikirkan kata-kata untuk membuatnya bekerja lebih baik. Saya tidak berpikir ada solusi mutlak untuk ini dan ini adalah salah satu alasan mengapa saya mempostingnya di sini daripada Stack Overflow.

Selama bulan lalu saya telah menulis ulang sepotong kode server (mmorpg) yang cukup lama menjadi lebih modern dan lebih mudah untuk diperluas / mod. Saya mulai dengan bagian jaringan dan menerapkan perpustakaan pihak ke-3 (libevent) untuk menangani hal-hal untuk saya. Dengan semua perubahan faktor dan perubahan kode saya memperkenalkan kerusakan memori di suatu tempat dan saya telah berjuang untuk mencari tahu di mana itu terjadi.

Saya sepertinya tidak dapat mereproduksi dengan andal pada lingkungan dev / test saya, bahkan ketika menerapkan bot primitif untuk mensimulasikan beberapa beban, saya tidak mendapatkan crash lagi (saya memperbaiki masalah libevent yang menyebabkan beberapa hal)

Saya sudah mencoba sejauh ini:

Memurnikan neraka itu keluar - Tidak ada penulisan yang tidak valid sampai hal itu crash (yang mungkin membutuhkan 1+ hari dalam produksi .. atau hanya satu jam) yang benar-benar membingungkan saya, pasti pada titik tertentu itu akan mengakses memori yang tidak valid dan tidak menimpa barang oleh kesempatan? (Apakah ada cara untuk "menyebar" kisaran alamat?)

Alat Analisis Kode, yaitu cakupan dan cppcheck. Sementara mereka menunjukkan beberapa .. kasus-kasus buruk dan tepi dalam kode tidak ada yang serius.

Merekam proses sampai crash dengan gdb (via undodb) dan kemudian bekerja dengan cara saya mundur. Ini / terdengar / seperti itu seharusnya bisa dilakukan, tapi saya akhirnya menabrak gdb dengan menggunakan fitur lengkapi-otomatis atau saya berakhir di beberapa struktur libevent internal di mana saya tersesat karena ada terlalu banyak cabang yang mungkin (satu korupsi menyebabkan yang lain dan sebagainya di). Saya kira akan lebih baik jika saya bisa melihat apa pointer awalnya milik / di mana dialokasikan, yang akan menghilangkan sebagian besar masalah percabangan. Saya tidak bisa menjalankan valgrind dengan undodb, dan saya catatan gdb normal sangat lambat (jika itu bahkan bekerja dalam kombinasi dengan valgrind).

Ulasan kode! Sendiri (menyeluruh) dan memiliki beberapa teman memeriksa kode saya, meskipun saya ragu itu cukup menyeluruh. Saya sedang berpikir tentang mungkin mempekerjakan seorang dev untuk melakukan review kode / debugging dengan saya, tetapi saya tidak mampu untuk memasukkan terlalu banyak uang di dalamnya dan saya tidak akan tahu di mana mencari seseorang yang bersedia bekerja untuk sedikit- ke-tidak ada uang jika dia tidak menemukan masalah atau siapa pun yang memenuhi syarat sama sekali.

Saya juga harus mencatat: Saya biasanya mendapatkan backtraces yang konsisten. Ada beberapa tempat di mana crash terjadi, sebagian besar terkait dengan kelas soket entah bagaimana menjadi rusak. Baik itu pointer yang tidak valid yang menunjuk ke sesuatu yang bukan soket atau kelas soket itu sendiri menjadi ditimpa (sebagian?) Dengan omong kosong. Meskipun saya curiga itu paling banyak menabrak karena itu salah satu bagian yang paling banyak digunakan, jadi itu adalah memori rusak pertama yang digunakan.

Semua dalam semua masalah ini telah membuat saya sibuk selama hampir 2 bulan (hidup dan mati, lebih dari proyek hobi) dan benar-benar membuat saya frustasi ke titik di mana saya menjadi IRL pemarah dan berpikir tentang menyerah. Saya tidak bisa memikirkan apa lagi yang harus saya lakukan untuk menemukan masalah ini.

Apakah ada teknik berguna yang saya lewatkan? Bagaimana Anda menghadapinya? (Tidak mungkin itu biasa karena tidak ada banyak informasi tentang ini .. atau aku benar-benar buta?)

Edit:

Beberapa spesifikasi jika itu penting:

Menggunakan c ++ (11) via gcc 4.7 (versi disediakan oleh debian wheezy)

Basis kode adalah sekitar 150 ribu baris

Edit sebagai respons terhadap posting david.pfx: (maaf atas respons yang lambat)

Apakah Anda menyimpan catatan kerusakan dengan cermat, untuk mencari pola?

Ya, saya masih memiliki kesedihan dari kecelakaan baru-baru ini yang tergeletak di sekitar

Apakah beberapa tempat benar-benar mirip? Dengan cara apa?

Nah, dalam versi terbaru (mereka tampaknya berubah setiap kali saya menambah / menghapus kode atau mengubah struktur terkait) itu akan selalu terjebak dalam metode item timer. Pada dasarnya suatu item memiliki waktu tertentu setelah itu kedaluwarsa dan mengirimkan info yang diperbarui kepada klien. Pointer soket yang tidak valid akan berada di kelas pemain (masih valid sejauh yang saya tahu), sebagian besar terkait dengan itu. Saya juga mengalami banyak crash dalam fase pembersihan, setelah shutdown normal di mana ia menghancurkan semua kelas statis yang belum dihancurkan secara eksplisit ( __run_exit_handlersdi backtrace). Sebagian besar melibatkan std::mapsatu kelas, menebak itu hanya hal pertama yang muncul.

Seperti apa data korup itu? Nol? Ascii? Pola?

Saya belum menemukan pola apa pun, tampaknya agak acak bagi saya. Sulit dikatakan karena saya tidak tahu dari mana korupsi dimulai.

Apakah ini terkait tumpukan?

Ini sepenuhnya terkait dengan tumpukan (saya mengaktifkan stack guard gcc dan tidak menangkap apa pun).

Apakah korupsi terjadi setelah a free()?

Anda harus sedikit menguraikan yang itu. Apakah maksud Anda memiliki pointer benda yang sudah bebas tergeletak di sekitar? Saya mengatur setiap referensi ke null setelah objek dihancurkan, jadi kecuali saya melewatkan sesuatu di suatu tempat, tidak. Itu harus muncul di valgrind meskipun yang tidak.

Apakah ada sesuatu yang khas tentang lalu lintas jaringan (ukuran buffer, siklus pemulihan)?

Lalu lintas jaringan terdiri dari data mentah. Jadi char array, (u) intX_t atau packed (untuk menghapus padding) struct untuk hal-hal yang lebih kompleks, setiap paket memiliki header yang terdiri dari id dan ukuran paket itu sendiri yang divalidasi terhadap ukuran yang diharapkan. Mereka adalah sekitar 10-60bytes dengan paket (boot 'internal' terbesar, dipecat sekali saat startup) memiliki ukuran beberapa Mb.

Banyak dan banyak pernyataan produksi. Hancurkan lebih awal dan dapat diprediksi sebelum kerusakan menyebar.

Saya pernah mengalami crash terkait std::mapkorupsi, setiap entitas memiliki peta "view" -nya, setiap entitas yang dapat melihatnya dan sebaliknya di dalamnya. Saya menambahkan buffer 200byte di depan dan sesudahnya, mengisinya dengan 0x33 dan memeriksanya sebelum setiap akses. Korupsi yang baru saja lenyap secara ajaib, saya pasti telah memindahkan sesuatu yang membuatnya merusak sesuatu yang lain.

Pencatatan strategis, sehingga Anda tahu secara akurat apa yang terjadi sebelumnya. Tambahkan ke logging ketika Anda mendekati jawaban.

Ini berfungsi .. sampai batas tertentu.

Dalam keputusasaan, dapatkah Anda menyimpan status dan memulai kembali secara otomatis? Saya dapat memikirkan beberapa perangkat lunak produksi yang melakukan itu.

Saya agak melakukan itu. Perangkat lunak ini terdiri dari proses "cache" utama dan beberapa pekerja lain yang semuanya mengakses cache untuk mendapatkan dan menyimpan barang. Jadi per crash, saya tidak kehilangan banyak kemajuan, itu masih memutuskan semua pengguna dan sebagainya, itu jelas bukan solusi.

Konkurensi: threading, kondisi balapan, dll

Ada utas mysql untuk melakukan kueri "async", itu semua belum tersentuh dan hanya membagikan informasi ke kelas basis data melalui fungsi dengan semua kunci.

Terganggu

Ada penghenti waktu untuk mencegah penguncian yang hanya dibatalkan jika tidak menyelesaikan siklus selama 30 detik, kode itu harus aman:

if (!tics) {
    abort();
} else
    tics = 0;

Tics adalah volatile int tics = 0;yang meningkat setiap kali siklus selesai. Kode lama juga.

events / callbacks / exception: kondisi rusak atau tumpukan tidak terduga

Banyak panggilan balik digunakan (jaringan I / O async, timer), tetapi mereka seharusnya tidak melakukan hal buruk.

Data yang tidak biasa: data input / timing / state yang tidak biasa

Saya punya beberapa kasus tepi yang terkait dengan itu. Melepaskan soket saat paket masih diproses menghasilkan akses nullptr dan semacamnya, tetapi hal itu mudah dikenali sejauh ini karena setiap referensi dibersihkan segera setelah memberi tahu kelas sendiri bahwa hal itu dilakukan. (Penghancuran itu sendiri ditangani oleh loop menghapus semua benda yang hancur setiap siklus)

Ketergantungan pada proses eksternal asinkron.

Mau menguraikan? Ini agak terjadi, proses cache yang disebutkan di atas. Satu-satunya hal yang dapat saya bayangkan dari atas kepala saya adalah penyelesaiannya tidak cukup cepat dan menggunakan data sampah, tetapi itu tidak terjadi karena itu menggunakan jaringan juga. Model paket yang sama.

Robin
sumber
7
Sayangnya, ini adalah hal umum dalam aplikasi C ++ yang tidak sepele. Jika Anda menggunakan kontrol sumber, menguji berbagai perubahan untuk mempersempit perubahan kode apa yang menyebabkan masalah dapat membantu, tetapi mungkin tidak layak dalam kasus ini.
Telastyn
Ya, itu benar-benar tidak masuk akal dalam kasus saya. Saya pada dasarnya beralih dari bekerja menjadi benar-benar rusak selama 2 bulan dan kemudian ke tahap debugging di mana saya memiliki sedikit kode yang berfungsi. Sistem lama benar-benar tidak memungkinkan saya untuk mengimplementasikan kode jaringan baru yang agak fleksibel tanpa merusak segalanya.
Robin
2
Pada titik ini Anda mungkin harus mencoba dan mengisolasi setiap bagian. Ambil setiap kelas / bagian dari solusi, buat tiruan di sekitarnya sehingga dapat berfungsi, dan uji neraka hidup keluar dari itu sampai Anda menemukan bagian yang gagal.
Ampt
mulai dengan mengomentari sebagian kode hingga Anda tidak lagi mengalami kerusakan.
cpp81
1
Selain Valgrind, Coverity, dan cppcheck, Anda harus menambahkan Asan dan UBsan ke rezim pengujian Anda. Jika kode Anda adalah corss-platofrm, maka tambahkan Microsoft's Enterprise Analysis ( /analyze) dan Apple Malloc and Scribble guards juga. Anda juga harus menggunakan banyak kompiler sebanyak mungkin menggunakan standar sebanyak mungkin karena peringatan kompiler adalah diagnostik dan mereka menjadi lebih baik dari waktu ke waktu. Tidak ada peluru perak, dan satu ukuran tidak cocok untuk semua. Semakin banyak alat dan kompiler yang Anda gunakan, semakin lengkap cakupannya karena setiap alat memiliki kelebihan dan kekurangannya.

Jawaban:

21

Ini masalah yang menantang, tetapi saya curiga ada banyak petunjuk yang bisa ditemukan dalam tabrakan yang sudah Anda lihat.

  • Apakah Anda menyimpan catatan kerusakan dengan cermat, untuk mencari pola?
  • Apakah beberapa tempat benar-benar mirip? Dengan cara apa?
  • Seperti apa data korup itu? Nol? Ascii? Pola?
  • Apakah ada multi-threading yang terlibat? Mungkinkah itu kondisi balapan?
  • Apakah ini terkait tumpukan? Apakah korupsi terjadi setelah bebas ()?
  • Apakah ini terkait tumpukan? Apakah tumpukan rusak?
  • Apakah referensi yang menggantung adalah suatu kemungkinan? Nilai data yang berubah secara misterius?
  • Apakah ada sesuatu yang khas tentang lalu lintas jaringan (ukuran buffer, siklus pemulihan)?

Hal-hal yang kami gunakan dalam situasi serupa.

  • Banyak dan banyak pernyataan produksi. Hancurkan lebih awal dan dapat diprediksi sebelum kerusakan menyebar.
  • Banyak dan banyak penjaga. Item data tambahan sebelum dan sesudah variabel lokal, objek dan mallocs () diatur ke nilai dan kemudian sering diperiksa.
  • Pencatatan strategis, sehingga Anda tahu secara akurat apa yang terjadi sebelumnya. Tambahkan ke logging ketika Anda mendekati jawaban.

Dalam keputusasaan, dapatkah Anda menyimpan status dan memulai kembali secara otomatis? Saya dapat memikirkan beberapa perangkat lunak produksi yang melakukan itu.

Jangan ragu untuk menambahkan detail jika kami dapat membantu sama sekali.


Bisakah saya menambahkan bahwa bug yang tak tentu serius seperti ini tidak terlalu umum, dan tidak ada banyak hal yang (biasanya) dapat menyebabkannya. Mereka termasuk:

  • Konkurensi: threading, kondisi balapan, dll
  • Interupsi / acara / panggilan balik / pengecualian: kondisi rusak atau tumpukan tidak terduga
  • Data tidak biasa: data input tidak masuk akal / timing / state
  • Ketergantungan pada proses eksternal asinkron.

Ini adalah bagian dari kode untuk fokus.

david.pfx
sumber
+1 Semua saran bagus, terutama pernyataan, penjaga, dan pencatatan.
andy256
Saya mengedit beberapa informasi lagi dalam pertanyaan saya sebagai jawaban atas jawaban Anda. Itu benar-benar membuat saya berpikir tentang crash ketika mematikan yang saya belum melihat secara luas, jadi saya akan mengatasinya untuk saat ini saya kira.
Robin
5

Gunakan versi debug dari malloc / gratis. Bungkus mereka dan tulis sendiri jika perlu. Banyak bersenang-senang!

Versi yang saya gunakan menambahkan byte penjaga sebelum dan sesudah setiap alokasi, dan memelihara daftar "yang dialokasikan" yang memeriksa bebas membebaskan potongan. Ini menangkap sebagian besar buffer overrun dan banyak kesalahan "bebas".

Salah satu sumber korupsi yang paling berbahaya adalah terus menggunakan bongkahan setelah dibebaskan. Bebas harus mengisi memori yang dibebaskan dengan pola yang diketahui (secara tradisional, 0xDEADBEEF) Ini membantu jika struktur yang dialokasikan menyertakan elemen "angka ajaib", dan secara bebas menyertakan pemeriksaan untuk angka ajaib yang sesuai sebelum menggunakan struktur.

ddyer
sumber
1
Valgrind harus menangkap dua kali lipat membebaskan / menggunakan data yang dibebani, bukan?
Robin
Menulis overload seperti ini untuk yang baru / hapus telah membantu saya menemukan banyak masalah kerusakan memori. Terutama byte penjaga yang diverifikasi pada delete dan menyebabkan program memicu break point yang secara otomatis menjatuhkan saya ke debugger.
Emily L.
3

Mengutip apa yang Anda katakan dalam pertanyaan Anda, tidak mungkin memberi Anda jawaban yang pasti. Yang terbaik yang bisa kita lakukan adalah memberikan saran tentang hal-hal yang harus dicari dan alat serta teknik.

Beberapa saran akan tampak naif, yang lain mungkin lebih cocok, tetapi mudah-mudahan seseorang memicu pemikiran yang dapat Anda tindak lanjuti. Saya harus mengatakan bahwa jawabannya oleh david.pfx memiliki saran dan saran yang bagus.

Dari gejalanya

  • bagi saya itu terdengar seperti buffer overrun.

  • masalah terkait adalah menggunakan data soket yang tidak divalidasi sebagai subskrip atau kunci, dll.

  • mungkinkah Anda menggunakan variabel global di suatu tempat, atau memiliki global dan lokal dengan nama yang sama, atau entah bagaimana data satu pemain mengganggu yang lain?

Seperti halnya banyak bug, Anda mungkin membuat asumsi yang tidak valid di suatu tempat. Atau mungkin lebih dari satu. Banyak kesalahan yang berinteraksi sulit dideteksi.

  • Apakah setiap variabel memiliki deskripsi? Dan bisakah Anda mendefinisikan pernyataan validitas?
    Jika tidak menambahkannya, pindai kode untuk melihat bahwa masing-masing variabel tampaknya digunakan dengan benar. Tambahkan pernyataan itu di mana pun hal itu masuk akal.

  • Saran untuk menambahkan pernyataan banyak adalah saran yang bagus: tempat pertama untuk menempatkannya adalah pada setiap titik masuk fungsi. Validasi argumen dan keadaan global yang relevan.

  • Saya menggunakan banyak logging untuk debug kode lama-berjalan / asinkron / real-time.
    Sekali lagi, masukkan menulis log pada setiap panggilan fungsi.
    Jika file log menjadi terlalu besar, fungsi logging dapat membungkus / mengganti file / dll.
    Ini sangat berguna jika pesan log indent dengan kedalaman panggilan fungsi.
    File log dapat menunjukkan bagaimana bug menyebar. Berguna ketika salah satu bagian dari kode melakukan sesuatu yang tidak benar yang bertindak sebagai bom aksi yang tertunda.

Banyak orang memiliki kode logging sendiri yang ditanam di rumah. Saya memiliki sistem log makro C lama di suatu tempat, dan mungkin versi C ++ ...

andy256
sumber
3

Segala sesuatu yang dikatakan dalam jawaban lain sangat relevan. Satu hal penting yang sebagian disebutkan oleh ddyer adalah bahwa pembungkus malloc / gratis memiliki manfaat. Dia menyebutkan beberapa tetapi saya ingin menambahkan alat debugging yang sangat penting untuk itu: Anda dapat login setiap malloc / gratis ke file eksternal bersama dengan beberapa baris callstack (atau callstack lengkap jika Anda peduli). Jika Anda berhati-hati, Anda dapat dengan mudah membuat ini dengan cepat dan menggunakannya dalam produksi jika itu yang terjadi.

Dari apa yang Anda jelaskan, tebakan pribadi saya adalah bahwa Anda mungkin menyimpan referensi ke pointer di suatu tempat untuk membebaskan memori dan mungkin akhirnya membebaskan pointer yang tidak lagi milik Anda atau menulisnya. Jika Anda dapat menyimpulkan kisaran ukuran untuk dipantau dengan teknik di atas, Anda harus dapat mempersempit logging. Jika tidak, setelah Anda menemukan memori apa yang sudah rusak, Anda dapat mengetahui malloc / pola bebas yang menyebabkannya cukup mudah dari log.

Catatan penting adalah bahwa seperti yang Anda sebutkan, mengubah tata letak memori mungkin menyembunyikan masalah. Karena itu sangat penting bahwa logging Anda tidak mengalokasikan (jika Anda bisa!) Atau sesedikit mungkin. Ini akan membantu reproduksibilitas jika terkait dengan memori. Ini juga akan membantu jika secepat mungkin jika masalah terkait multi-threading.

Penting juga bahwa Anda menjebak alokasi dari perpustakaan pihak ke-3 sehingga Anda dapat mencatatnya dengan benar. Anda tidak pernah tahu dari mana asalnya.

Sebagai alternatif terakhir, Anda juga dapat membuat pengalokasi khusus tempat Anda mengalokasikan setidaknya 2 halaman untuk setiap alokasi dan membatalkan pemetaannya saat Anda membebaskan (menyelaraskan alokasi ke batas halaman, mengalokasikan halaman sebelumnya dan menandainya sebagai tidak dapat diakses atau menyelaraskan alokasikan di akhir halaman dan alokasikan halaman setelah dan tandai tidak dapat diakses). Pastikan untuk tidak menggunakan kembali alamat memori virtual tersebut untuk alokasi baru setidaknya untuk beberapa saat. Ini berarti Anda harus mengelola sendiri memori virtual Anda (cadangan dan gunakan sesuka Anda). Perhatikan bahwa ini akan menurunkan kinerja Anda dan mungkin berakhir menggunakan sejumlah besar memori virtual tergantung pada berapa banyak alokasi yang Anda berikan. Untuk mengurangi ini akan membantu jika Anda dapat berjalan dalam 64bit dan / atau mengurangi rentang alokasi yang membutuhkan ini (berdasarkan ukuran). Valgrind mungkin sudah melakukan ini tetapi mungkin terlalu lambat bagi Anda untuk mengetahui masalahnya. Melakukan ini hanya untuk beberapa ukuran atau objek (jika Anda tahu yang mana, Anda dapat menggunakan pengalokasi khusus hanya untuk objek-objek itu) akan memastikan kinerja terkena dampak minimal.

Nicholas Frechette
sumber
0

Coba atur titik tontonan pada alamat memori tempat crash. GDB akan rusak pada instruksi yang menyebabkan memori tidak valid. Kemudian dengan jejak belakang Anda dapat melihat kode Anda yang menyebabkan korupsi. Ini mungkin bukan sumber korupsi tetapi mengulang titik pengawasan pada setiap korupsi dapat mengarah pada sumber masalahnya.

Ngomong-ngomong, karena pertanyaan diberi tag C ++, pertimbangkan untuk menggunakan pointer bersama yang menjaga kepemilikan dengan mempertahankan jumlah referensi dan menghapus memori dengan aman setelah pointer keluar dari ruang lingkup. Tetapi gunakan dengan hati-hati karena mereka dapat menyebabkan kebuntuan dalam penggunaan ketergantungan sirkular yang langka.

Mohammad Azim
sumber