Gli strumenti opportuni fanno risparmiare tempo (ovvero: le cose, o si fanno bene, o non si fanno)

In informatifca, automatizzare le operazioni ripetitive fa risparmiare più tempo di quello che si perde ad autoconvincersi che “tanto devo farlo solo tot volte, faccio tutto a mano”. Perché immancabilmente il numero di volte raddoppia o triplica inaspettatamente.

Stessa cosa per l’uso di strumenti opportuni invece che improvvisati, che richiede del tempo per imparare a usarli, ma ne fa risparmiare molto di più, e questo trovo che si applichi anche ad altri campi della vita.

Detto ciò, ho un dannato programma in C che appena avviato su Windows istantaneamente crasha.

Tutto ciò che so sul C l’ho appreso all’università, perché c’è un corso dove ci è richiesto di produrre dei programmi C per dimostrare che abbiamo capito quello che si è fatto a lezione.
Lo scopo non è imparare il C, si usa giusto perché un qualche linguaggio bisogna usarlo, non è nemmeno richiesto fare error handling di alcun genere.

Questo per dire: non ho particolari conoscenze sulle best practice e sugli strumenti di sviluppo e debugging per C, l’importante è produrre il più in fretta possibile programmi che vagamente funzionino, e adesso il programma non mi sta funzionando.
Ma perché fare cose difficili e inutili quando si possono fare quelle semplici e che sicuramente torneranno utili?

Debugger

Ci è stato consigliato di usare per prima cose le printf() per capire cosa sta succedendo nel programma, e ricorrere al vero debugger solo quando strettamente necessario.

Ora, dopo anni passati a debuggare PHP con var_dump(), sono decisamente convinto che le printf() come debug siano una fatica inutile. Meglio qualche breakpoint ben piazzato, molto meglio.

I debugger sono più o meno tutti uguali, le funzioni sono sempre le stesse: posizionare breakpoint, “step into”, “step out”, “step over”, “continue”…
GDB non fa eccezione e praticamente tutti gli IDE per C che offrano un debugger in realtà offrono un’interfaccia grafica per GDB.

Io in due anni di saltuario uso di debugger a caso in PHP, JS, Java e VB.NET (sì, mi è capitato di usarlo, qualche tempo fa, e sto ancora cercando di rimuovere il ricordo) ho imparato a districarmi tra quelle funzioni base e acquisire un certo occhio clinico su dove piazzare i breakpoint. Come ce l’ho fatta io, può farcela chiunque ed è un’abilità che a un programmatore tornerà utile per tutto il resto della vita (lavorativa, si intende).

Uso saltuario, reitero. Nel corso di cui sto parlando, abbiamo passato 3 mesi a produrre 500 e fischia righe di C a settimana: ho debuggato molto più codice in quei 3 mesi che nel resto della mia vita.

Quindi qualche breakpoint qua, qualcuno là, continue, step into, step out, ecco un bug, bam, fixato, avanti il prossimo, mentre con le printf() sarei ancora lì ad aggiungerle in posti dove non servono, a dimentircarmi il \n e dover ricompilare tutto, etc, etc…

Breakpoint condizionali e watchpoint

Eh, ma «Debuggare le funzioni ricorsive con i breakpoint è difficile, usate le printf()».

L’ho notato, ma l’orrore che provo verso il metodo dei var_dump() in PHP mi ha spinto a imparare altre cose su GDB.
Come che ad esempio esistono i breakpoint condizionali e i watchpoint.

L’IDE che uso non permette di aggiungerli tramite l’interfaccia grafica: beh, non è un problema, adesso conosco anche qualche comando di GDB.
Ci vorrà qualche secondo in più a scrivere il comando, ma si risparmiano ore rispetto ad aggiungere printf() inutili o fare “continue” 200 volte esatte su un breakpoint normale.

Call Stack

Lo stack delle chiamate a funzione, quella finestra che avevo visto qualche volta nei dintorni dei debugger, con quell’inquietante lista di funzioni: ha sempre avuto un che di intimidatorio, cosa sarà mai? A che servirà? Via, è una roba per professionisti, a me basta vedere le variabili in memoria…

Beh, una volta appreso che le funzioni (più correttamente “i loro parametri, le variabili locali e l’indirizzo di ritorno”) vengono scaraventate sullo stack, mi è immediatamente divenuto chiaro cosa fosse.

Riuscire a leggere il call stack fa la differenza tra lo scrivere una funzione ricorsiva di 200 righe che funziona dopo al massimo 10 minuti di debugging, e una funzione ricorsiva di 200 righe che funziona solo durante le notti di luna piena, per miracolo, dopo averci sbattuto sopra la testa per 4 giorni, e piena di printf() che non si sa nemmeno più a cosa servono.

E se tutto ciò non basta?

Nonostante tutto questo, il programma continua a crashare.
Si compila perfettamente con i parametri -std=c11 -Wall -pedantic-errors di GCC, ma appena lo avvio crasha, prima di produrre qualsiasi output.

GDB mi avvisa che ho scritto fuori dalla zona a me assegnata nell’heap. Mi dà anche l’indirizzo. E attende istruzioni.

Il mio metodo

La prima cosa che mi è venuta in mente è stata vedere il contenuto della memoria a quell’indirizzo, per vedere se riconoscevo qualcuna delle 675263758 stringhe che il programma scaraventa dentro a regioni allocate dinamicamente leggendole da un file prima di produrre qualsiasi output.

Bene, ora so anche usare il comando x di GDB, ma in quella regione della memoria sembra esserci solo spazzatura indecifrabile e degli 0xabababab piazzati lì dal Windows Heap Manager come aiuto al debugging, ma sono meno dei 16 byte previsti e ci sono dei byte fuori posto in mezzo. Ok, chiaramente qualcosa ha scritto là dove non doveva al punto di sovrascrivere la tail checking pattern in punti casuali, ma cosa?

Il call stack mostra che tutto ha avuto inizio dalla funzione ??, e poi è entrato in una serie di funzioni che stanno in ntdll. Poi ha trovato un breakpoint. Se lo faccio andare avanti, il programma crasha.

Il problema del call stack incompleto è “noto”, ad esempio se ne parla in una risposta su Stack Overflow: Breakpoints out of nowhere when debugging with gdb, inside ntdll.

Sul momento non ho cercato su internet, ma ho piazzato qualche breakpoint strategico per seguire il programma quasi passo a passo. Dopo un minuto Windows aveva di nuovo sparato fuori quel breakpoint, per la stessa ragione, ma stavolta per qualche ragione avevo il call stack completo, evviva! Ed ecco la funzione incriminata, con dentro una malloc().

Eppure sembrava tutto a posto. Sono andato a guardare le funzione “gemella” che la fa la free() e ho trovato un possibile bug in cui free() poteva ricevere NULL come parametro, ma il problema non era lì.

Il metodo delle printf()

A questo punto mi sono detto: «che il metodo delle printf() possa essere in verità la retta via? Dopotutto finora ho fatto di testa mia, ho imparato una serie di cose interessanti (anche se dipende da quale sia la definizione di “interessante”) e utili, forse è tempo di adeguarsi alle regole auree non scritte». Quindi ho aggiunto un po’ di printf().

Dopo un’ora persa a non capire perché il programma crashasse in maniera casuale poco dopo una malloc() in mezzo alle altre 239857238952738 malloc() invocate da quella funzione, ho gettato tutto su Linux, compilato e… funzionava senza problemi.

Su Windows, per aggiungere il danno alla beffa, con le printf() e GDB attaccato, magicamente il programma funziona perfettamente, ma lanciarlo senza GDB produce il crash.

Ok, quindi, col deubgger ho risolto un bug che non c’entra niente in due minuti e con le printf() ho trasformato il bug in un heisenbug dopo un’ora di fatica. Probabilmente questo è uno dei casi da “ultima spiaggia”, in cui passare al debugger. Beh, l’ho già fatto, avrei potuto continuare lì e non perdere tutto questo tempo con le printf().

Ma ormai siamo andati troppo oltre, temevo il giorno che saremmo giunti a tanto punto, ma non vedo altra via, non vedo altra scelta, devo farlo.

Valgrind

Sapevo che esiste. Sapevo che serve a trovare i memory leak. Immaginavo che fosse uno strumento complicatissimo da usare, che non vale la pena di configurare per progetti così insulsi e “usa e getta”.

Invece non c’è nulla da configurare: valgrind --leak-check=yes /path/to/program arg1 arg2 e basta. È pure di una facilità disarmante da usare: semplicemente getta in mezzo all’output del programma dei messaggi quando qualcosa scrive fuori dalle regioni allocate, quando si fanno free() errate, quando si “perde” un puntatore restituito da malloc() senza fare free(), etc…
Tempo tre minuti, e ho localizzato cosa scriveva fuori dall’area assegnata. Perché avevo sbagliato la dimensione nella malloc(), nonostante l’avessi osservata attentamente più di un’ora prima.

Bene, problema risolto e un nuovo attrezzo aggiunto all’arsenale.
Le printf() sono uno strumento improvvisato tant’è che non ho concluso nulla, il debugger è uno strumento indubbiamente utile ma non il migliore in questo caso tant’è che ho non ho risolto ma non ho nemmeno perso tempo (e ho trovato altri bug), Valgrind in questa circostanza era lo strumento ottimale e si è visto.

Perché perdere tempo a fare cose difficili e inutili quando si possono fare quelle facili e utili?

Annunci

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione /  Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione /  Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione /  Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione /  Modifica )

Connessione a %s...