Debug del danneggiamento della memoria


23

Prima di tutto, mi rendo conto che questa non è una domanda perfetta in stile domande e risposte con una risposta assoluta, ma non riesco a pensare a nessuna formulazione per farlo funzionare meglio. Non penso che ci sia una soluzione assoluta a questo e questo è uno dei motivi per cui sto pubblicando qui invece di Stack Overflow.

Nell'ultimo mese ho riscritto un pezzo piuttosto vecchio di codice server (mmorpg) per essere più moderno e più facile da estendere / mod. Ho iniziato con la parte di rete e ho implementato una libreria di terze parti (libevent) per gestire le cose per me. Con tutte le modifiche al re-factoring e al codice ho introdotto la corruzione della memoria da qualche parte e ho avuto difficoltà a scoprire dove succede.

Non riesco a riprodurlo in modo affidabile sul mio ambiente di sviluppo / test, anche quando si implementano bot primitivi per simulare un carico non si verificano più arresti anomali (ho risolto un problema di libevent che causava alcune cose)

Ho provato finora:

Valgrinding the hell out of it it - No scritture non valide fino a quando la cosa non si blocca (che potrebbe richiedere più di 1 giorno in produzione ... o solo un'ora) il che mi sta davvero sconcertando, sicuramente a un certo punto accederebbe alla memoria non valida e non sovrascriverebbe cose da opportunità? (C'è un modo per "allargare" l'intervallo di indirizzi?)

Strumenti di analisi del codice, ovvero copertura e verifica. Mentre hanno sottolineato alcuni ... cattiverie e casi limite nel codice, non c'era nulla di grave.

Registrare il processo fino a quando non si arresta in modo anomalo con gdb (tramite undodb) e quindi tornare indietro. Questo / sembra / come dovrebbe essere fattibile, ma o finisco per mandare in crash gdb usando la funzione di completamento automatico o finisco in una struttura libevent interna dove mi perdo perché ci sono troppi rami possibili (una corruzione che ne causa un'altra e così su). Immagino che sarebbe bello se potessi vedere a che cosa appartiene originariamente un puntatore / dove è stato allocato, ciò eliminerebbe la maggior parte dei problemi di ramificazione. Tuttavia, non posso eseguire valgrind con undodb, e il normale record gdb è insolitamente lento (se funziona anche in combinazione con valgrind).

Revisione del codice! Da solo (accuratamente) e avendo alcuni amici a guardare il mio codice, anche se dubito che sia stato abbastanza approfondito. Stavo pensando di assumere un dev per fare un po 'di revisione del codice / debugging con me, ma non posso permettermi di metterci troppi soldi e non saprei dove cercare qualcuno che sarebbe disposto a lavorare per poco- senza soldi se non trova il problema o qualcuno qualificato.

Dovrei anche notare: di solito ricevo backtrace coerenti. Ci sono alcuni punti in cui si verifica l'arresto anomalo, principalmente legato alla corruzione della classe socket in qualche modo corrotta. Che si tratti di un puntatore non valido che punta a qualcosa che non è un socket o la classe socket stessa viene sovrascritta (parzialmente?) Con incomprensioni. Anche se sospetto che si blocchi di più lì poiché è una delle parti maggiormente utilizzate, quindi è la prima memoria corrotta che viene utilizzata.

Tutto sommato questo problema mi ha impegnato per quasi 2 mesi (acceso e spento, più di un progetto di hobby) e mi sta davvero frustrando al punto in cui divento IRL scontroso e penso di arrendermi. Non riesco proprio a pensare a cos'altro dovrei fare per trovare il problema.

Ci sono delle tecniche utili che mi sono perso? Come lo affronti? (Non può essere così comune dal momento che non ci sono molte informazioni su questo .. o sono davvero cieco?)

Modificare:

Alcune specifiche nel caso sia importante:

Utilizzo di c ++ (11) tramite gcc 4.7 (versione fornita da debian wheezy)

La base di codice è di circa 150.000 righe

Modifica in risposta a post david.pfx: (scusate la risposta lenta)

Tieni un registro accurato degli arresti anomali, per cercare schemi?

Sì, ho ancora discariche dei recenti incidenti in giro

I pochi posti sono davvero simili? In quale modo?

Bene, nella versione più recente (sembrano cambiare ogni volta che aggiungo / rimuovo codice o cambio strutture correlate) verrebbe sempre catturato in un metodo timer elemento. Fondamentalmente un articolo ha un tempo specifico dopo il quale scade e invia informazioni aggiornate al client. Il puntatore socket non valido sarebbe nella classe Player (ancora valida per quanto posso dire), principalmente correlata a quella. Inoltre sto sperimentando un sacco di crash nella fase di cleanup, dopo il normale arresto in cui sta distruggendo tutte le classi statiche che non sono state esplicitamente distrutte ( __run_exit_handlersnella backtrace). Per lo più coinvolgente std::mapdi una classe, supponendo che sia solo la prima cosa che viene fuori però.

Che aspetto hanno i dati corrotti? Zeros? Ascii? Modelli?

Non ho ancora trovato alcun modello, mi sembra in qualche modo casuale. È difficile da dire poiché non so dove sia iniziata la corruzione.

È legato all'heap?

È interamente legato all'heap (ho abilitato la protezione dello stack di gcc e questo non ha catturato nulla).

La corruzione si verifica dopo un free()?

Dovrai approfondire un po 'quello. Intendi avere puntatori di oggetti già liberi in giro? Sto impostando ogni riferimento su null una volta che l'oggetto viene distrutto, quindi a meno che non mi sia perso qualcosa da qualche parte, no. Ciò dovrebbe apparire in Valgrind anche se non è così.

Esiste qualcosa di distintivo nel traffico di rete (dimensione del buffer, ciclo di recupero)?

Il traffico di rete è costituito da dati non elaborati. Quindi, array di caratteri, (u) intX_t o pacchetti impacchettati (per rimuovere il riempimento) per cose più complesse, ogni pacchetto ha un'intestazione composta da un id e dalle dimensioni del pacchetto stesso che viene convalidato rispetto alla dimensione prevista. Sono circa 10-60 byte con il più grande (pacchetto 'bootup' interno, lanciato una volta all'avvio) con una dimensione di pochi Mb.

Molte affermazioni sulla produzione. Schiantarsi presto e prevedibilmente prima che il danno si propaghi.

Una volta ho avuto un incidente legato alla std::mapcorruzione, ogni entità ha una mappa della sua "vista", ogni entità che può vederlo e viceversa è in quello. Ho aggiunto un buffer da 200byte prima e dopo, riempito con 0x33 e controllato prima di ogni accesso. La corruzione è svanita magicamente, devo aver spostato qualcosa che ha reso corrotto qualcos'altro.

Registrazione strategica, in modo da sapere esattamente cosa stava succedendo poco prima. Aggiungi alla registrazione man mano che ti avvicini a una risposta.

Funziona .. fino ad un certo punto.

Nella disperazione, puoi salvare lo stato e il riavvio automatico? Posso pensare ad alcuni software di produzione che lo fanno.

Lo faccio in qualche modo. Il software consiste in un processo "cache" principale e in alcuni altri processi che accedono alla cache per ottenere e salvare elementi. Quindi per incidente non perdo molti progressi, disconnette ancora tutti gli utenti e così via, non è sicuramente una soluzione.

Concorrenza: threading, condizioni di gara, ecc

C'è un thread mysql per eseguire query "asincrone", che non è stato toccato e condivide solo le informazioni con la classe del database tramite funzioni con tutti i blocchi.

interrupt

C'è un timer di interruzione per impedirne il blocco che si interrompe se non ha completato un ciclo per 30 secondi, tuttavia quel codice dovrebbe essere sicuro:

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

il tic è volatile int tics = 0;aumentato ogni volta che un ciclo viene completato. Anche il vecchio codice.

eventi / callback / eccezioni: stato corrotto o stack imprevedibile

Vengono utilizzati molti callback (I / O di rete asincroni, timer), ma non dovrebbero fare nulla di male.

Dati insoliti: dati di input / tempistica / stato insoliti

Ho avuto alcuni casi limite legati a questo. La disconnessione di un socket mentre i pacchetti sono ancora in fase di elaborazione ha comportato l'accesso a nullptr e simili, ma quelli sono stati facili da individuare finora poiché ogni riferimento viene ripulito subito dopo aver detto alla classe stessa che è stato fatto. (La distruzione stessa è gestita da un ciclo che elimina tutti gli oggetti distrutti ogni ciclo)

Dipendenza da un processo esterno asincrono.

Ti interessa elaborare? Questo è un po 'il caso, il processo di cache sopra menzionato. L'unica cosa che potrei immaginare dalla cima della mia testa sarebbe che non finisse abbastanza velocemente e usando i dati della spazzatura, ma non è così dal momento che sta usando anche la rete. Stesso modello di pacchetto.


7
Purtroppo, questo è molto comune nelle app C ++ non banali. Se stai usando il controllo del codice sorgente, testare vari changeset per restringere la modifica del codice che ha causato il problema può essere d'aiuto, ma forse non è possibile in questo caso.
Telastyn,

Sì, davvero non è fattibile nel mio caso. Fondamentalmente sono passato dal lavoro completamente e completamente rotto per 2 mesi e poi alla fase di debug in cui ho un po 'di codice funzionante. Il vecchio sistema non mi permetteva davvero di implementare un mio nuovo codice di rete flessibile senza rompere tutto.
Robin,

2
A questo punto potresti dover provare a isolare ogni parte. Prendi ogni classe / sottoinsieme della soluzione, prendine in giro in modo che possa funzionare e testane l'inferno vivente fino a trovare la sezione che fallisce.
Appunto

iniziare commentando parti di codici fino a quando non si verifica più l'arresto anomalo.
cpp81,

1
Oltre a Valgrind, Coverity e cppcheck, dovresti aggiungere Asan e UBsan al tuo regime di test. Se il tuo codice è corss-platofrm, aggiungi anche le protezioni Enterprise Analysis ( /analyze) di Microsoft e Malloc e Scribble di Apple. È inoltre necessario utilizzare il maggior numero possibile di compilatori utilizzando il maggior numero possibile di standard poiché gli avvisi del compilatore sono diagnostici e migliorano nel tempo. Non esiste un proiettile d'argento e una taglia non va bene per tutti. Più strumenti e compilatori utilizzi, più completa la copertura perché ogni strumento ha i suoi punti di forza e di debolezza.

Risposte:


21

È un problema impegnativo, ma sospetto che ci siano molti più indizi negli incidenti che hai già visto.

  • Tieni un registro accurato degli arresti anomali, per cercare schemi?
  • I pochi posti sono davvero simili? In quale modo?
  • Che aspetto hanno i dati corrotti? Zeros? Ascii? Modelli?
  • C'è qualche multi-threading coinvolto? Potrebbe essere una condizione di gara?
  • È legato all'heap? La corruzione si verifica dopo un free ()?
  • È legato allo stack? Lo stack viene danneggiato?
  • Un riferimento penzolante è una possibilità? Un valore di dati che è cambiato misteriosamente?
  • Esiste qualcosa di distintivo nel traffico di rete (dimensione del buffer, ciclo di recupero)?

Cose che abbiamo usato in situazioni simili.

  • Molte affermazioni sulla produzione. Schiantarsi presto e prevedibilmente prima che il danno si propaghi.
  • Un sacco di guardie. Elementi di dati extra prima e dopo variabili locali, oggetti e malloc () impostati su un valore e quindi controllati spesso.
  • Registrazione strategica, in modo da sapere esattamente cosa stava succedendo poco prima. Aggiungi alla registrazione man mano che ti avvicini a una risposta.

Nella disperazione, puoi salvare lo stato e il riavvio automatico? Posso pensare ad alcuni software di produzione che lo fanno.

Sentiti libero di aggiungere dettagli se possiamo aiutarti.


Posso solo aggiungere che bug seriamente indeterminati come questo non sono poi così comuni e non ci sono molte cose che (di solito) possono causarli. Loro includono:

  • Concorrenza: threading, condizioni di gara, ecc
  • Interruzioni / eventi / callback / eccezioni: stato corrotto o stack imprevedibile
  • Dati insoliti: dati di input / tempistiche / stato insoliti
  • Dipendenza da un processo esterno asincrono.

Queste sono le parti del codice su cui concentrarsi.


+1 Tutti i buoni suggerimenti, in particolare le asserzioni, le guardie e il disboscamento.
andy256,

Ho modificato alcune ulteriori informazioni nella mia domanda come risposta alla tua risposta. Questo in realtà mi ha fatto pensare agli arresti anomali durante lo spegnimento che non ho ancora esaminato ampiamente, quindi immagino che per ora lo farò.
Robin,

5

Usa una versione di debug di malloc / free. Avvolgeteli e scrivete i vostri se necessario. Molto divertente!

La versione che uso aggiunge byte di guardia prima e dopo ogni allocazione e mantiene un elenco "allocato" che controlla liberamente i blocchi liberati. Ciò rileva la maggior parte dei sovraccarichi del buffer e errori "liberi" multipli o non autorizzati.

Una delle fonti più insidiose di corruzione continua a utilizzare un pezzo dopo che è stato liberato. Free dovrebbe riempire la memoria liberata con un modello noto (tradizionalmente, 0xDEADBEEF). Aiuta se le strutture allocate includono un elemento "numero magico" e includono liberamente i controlli per il numero magico appropriato prima di usare una struttura.


1
Valgrind dovrebbe catturare la doppia libertà / utilizzo di dati gratuiti, no?
Robin,

Scrivere questo tipo di sovraccarichi per new / delete mi ha aiutato a individuare numerosi problemi di corruzione della memoria. Soprattutto i byte di guardia che vengono verificati all'eliminazione e provoca un punto di interruzione attivato dal programma che mi fa cadere automaticamente nel debugger.
Emily L.,

3

Per parafrasare ciò che dici nella tua domanda, non è possibile darti una risposta definitiva. Il meglio che possiamo fare è dare suggerimenti su cose da cercare e strumenti e tecniche.

Alcuni suggerimenti appariranno ingenui, altri potrebbero sembrare più applicabili, ma si spera che si inneschi un pensiero che è possibile seguire. Devo dire che la risposta di david.pfx ha buoni consigli e suggerimenti.

Dai sintomi

  • a me sembra un sovraccarico di buffer.

  • un problema correlato sta usando i dati socket non convalidati come pedice o chiave, ecc.

  • è possibile che tu stia utilizzando una variabile globale da qualche parte, o abbia una globale e locale con lo stesso nome, o in qualche modo i dati di un giocatore interferiscono con un altro?

Come con molti bug, probabilmente stai facendo un'ipotesi non valida da qualche parte. O forse più di uno. Più errori di interazione sono difficili da rilevare.

  • Ogni variabile ha una descrizione? E puoi definire un'asserzione di validità?
    In caso contrario, esegui la scansione del codice per verificare che ciascuna variabile appaia utilizzata correttamente. Aggiungi questa affermazione ovunque abbia senso.

  • Il suggerimento di aggiungere molte asserzioni è buono: il primo posto per metterle è su ogni punto di ingresso della funzione. Convalida gli argomenti e qualsiasi stato globale rilevante.

  • Uso un sacco di registrazione per il debug di codici di lunga durata / asincroni / in tempo reale.
    Ancora una volta, inserire una scrittura del registro su ogni chiamata di funzione.
    Se i file di registro diventano troppo grandi, le funzioni di registrazione possono racchiudere / cambiare file / ecc.
    È molto utile se i messaggi di registro rientrano con la profondità della chiamata della funzione.
    Il file di registro può mostrare come si propaga un bug. Utile quando un pezzo di codice fa qualcosa di non proprio che agisce come una bomba ad azione ritardata.

Molte persone hanno il proprio codice di registrazione cresciuto in casa. Ho un vecchio sistema di registro macro C da qualche parte, e forse una versione C ++ ...


3

Tutto ciò che è stato detto nelle altre risposte è molto rilevante. Una cosa importante menzionata in parte da ddyer è che avvolgere malloc / free ha dei vantaggi. Ne menziona alcuni, ma vorrei aggiungere uno strumento di debug molto importante a questo: puoi registrare ogni malloc / gratuito in un file esterno insieme a poche righe di callstack (o al callstack completo se ti interessa). Se stai attento, puoi facilmente renderlo abbastanza veloce e usarlo in produzione se si tratta di esso.

Da quello che descrivi, la mia ipotesi personale è che potresti tenere un riferimento a un puntatore da qualche parte alla memoria liberata e potresti finire per liberare un puntatore che non ti appartiene più o scriverci. Se è possibile dedurre un intervallo di dimensioni da monitorare con la tecnica sopra descritta, si dovrebbe essere in grado di restringere notevolmente la registrazione. Altrimenti, una volta trovata la memoria danneggiata, puoi capire il modello malloc / free che lo ha portato abbastanza facilmente dai registri.

Una nota importante è che, come hai detto, la modifica del layout della memoria potrebbe nascondere il problema. È quindi molto importante che la tua registrazione non effettui allocazioni (se puoi!) O il minor numero possibile. Ciò aiuterà la riproducibilità se è legata alla memoria. Aiuterà anche se è il più veloce possibile se il problema è relativo al multi-threading.

È anche importante intercettare le allocazioni da librerie di terze parti in modo da poterle registrare anche correttamente. Non sai mai da dove potrebbe venire.

Come ultima alternativa, puoi anche creare un allocatore personalizzato in cui allocare almeno 2 pagine per ogni allocazione e annullare la mappatura quando libero (allineare l'allocazione a un limite di pagina, allocare una pagina prima e contrassegnarla come non accessibile o allineare il allocare alla fine di una pagina e allocare una pagina dopo e il segno non è accessibile). Assicurati di non riutilizzare quegli indirizzi di memoria virtuale per nuove allocazioni per almeno qualche tempo. Ciò implica che dovrai gestire tu stesso la tua memoria virtuale (prenotala e usala come preferisci). Si noti che ciò peggiorerà le prestazioni e potrebbe finire per utilizzare quantità significative di memoria virtuale in base al numero di allocazioni che le alimentano. Per mitigarlo, sarà utile eseguire 64 bit e / o ridurre la gamma di allocazioni che ne hanno bisogno (in base alle dimensioni). Valgrind potrebbe già farlo, ma potrebbe essere troppo lento per farti capire il problema. In questo modo solo per alcune dimensioni o oggetti (se sai quale, puoi utilizzare l'allocatore speciale solo per quegli oggetti) assicurerai che le prestazioni siano minimamente influenzate.


0

Prova a impostare un punto di controllo sull'indirizzo di memoria in cui si blocca. GDB si interromperà all'istruzione che ha causato la memoria non valida. Quindi con back trace puoi vedere il tuo codice che sta causando la corruzione. Questo potrebbe non essere la fonte della corruzione, ma ripetere il punto di controllo su ogni corruzione può portare alla fonte del problema.

A proposito, poiché la domanda è taggata in C ++, considera l'utilizzo di puntatori condivisi che si prendono cura della proprietà mantenendo un conteggio dei riferimenti ed eliminano la memoria in modo sicuro dopo che il puntatore è uscito dall'ambito. Ma usali con cautela in quanto possono causare deadlock in un raro uso della dipendenza circolare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.