Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

Visualizzazione post con etichetta GCC. Mostra tutti i post
Visualizzazione post con etichetta GCC. Mostra tutti i post

martedì 27 gennaio 2026

Warning Peaks
perché i warning sono importanti in C - pt.2

Dale Cooper: Harry, ti dirò un piccolo segreto. Ogni giorno, una volta al giorno, fatti un regalo. Non pianificarlo, non aspettarlo... fallo solo succedere.

Ok, è già ora di tornare sul pezzo. Dove eravamo rimasti? Ah, si: nell'ultimo articolo avevo parlato dell'importanza, spesso sottovalutata, dei warning, e avevo anche mostrato qualche (spero) interessante esempio. Avevo poi concluso dicendo che, nella seconda parte avremmo visto una panoramica dei flag consigliati per scrivere una applicazione impeccabile ("a prova di bomba"). E allora, come diceva (qui sopra) il buon Dale Cooper nel mitico Twin Peaks, oggi tocca auto-regalarsi qualcosa di molto utile... come avrete già intuito, anche in questo articolo parleremo di warning!

...Harry, regalati un warning e risolvilo. La tua applicazione ti ringrazierà...

Avevo già anticipato nello scorso articolo che alcuni warning appaiono di base, mente altri appaiono usando il flag di compilazione -Wall. È quindi venuto il momento di mostrare una lista degli altri flag che si possono usare, anche se vedremo solo i più significativi per lo scopo che ci siamo prefissi. I flag disponibili con GCC sono tantissimi, e alcuni sono dei "flag di gruppo" (come il -Wall) che raggruppano molte opzioni in una botta sola. Vedremo, quindi, i flag di gruppo più utili e anche alcuni flag semplici da usare separatamente. Vai con la lista!

Flag di gruppo

  • -Wall: abilita i warning più comunemente utili, tra cui variabili non inizializzate, parametri inutilizzati e costrutti sospetti. Include ben 81 flag semplici che si possono usare separatamente (-Waddress, -Warray-compare -Warray-parameter=2, ecc.), ed è il flag di gruppo "principe", quello assolutamente da usare.
  • -Wextra: aggiunge warning più completi rispetto a -Wall, come parametri di funzione inutilizzati, variabili oscurate e dichiarazioni di funzione implicite. Include ben 29 flag semplici che si possono usare separatamente (-Walloc-size, -Wcalloc-transposed-args, -Wcast-function-type, ecc.), ed è il flag di gruppo "principe" n.2, da usare sempre in coppia con -Wall (o anche da solo, ma in coppia è meglio... scusate il doppio senso, ah ah ah).
  • -Wformat=2: combina i flag semplici -Wformat, -Wformat-nonliteral, -Wformat-security e -Wformat-y2k per rilevare l'uso non sicuro delle stringhe di formato. È un flag utile ma solo per usi avanzati e restrittivi, normalmente è sufficiente usare il -Wformat=1 che è incluso nel -Wall.

Flag semplici

  • -pedantic (oppure -Wpedantic): impone la stretta conformità agli standard ISO C/C++, segnalando le estensioni non standard e il codice non conforme. È un flag altamente raccomandabile.
  • -Werror: tratta tutti i warning come errori, impedendo la compilazione fino alla risoluzione dei problemi, ideale per il codice di produzione. È un flag utile ma non strettamente indispensabile; comunque è importante sapere che è disponibile.
  • -Wshadow: avvisa quando una variabile locale oscura una variabile da uno scope esterno. È un flag altamente raccomandabile.
  • -Wnull-dereference: avvisa in merito a percorsi che potrebbero portare a referenze di puntatori nulli. È un flag utile ma solo per usi avanzati e restrittivi.
  • -Wcast-align=strict: rileva conversioni di pointer non sicure che potrebbero causare problemi di disallineamento su determinate architetture. È un flag utile ma solo per usi avanzati e restrittivi.

Che vi sembra la lista? È, ovviamente, una selezione molto ponderata, ma abbastanza completa; e comunque, se volete conoscere tutti il flag disponibili, potete consultarli qui (ma mettetevi comodi, è una lista lunghissima!).

A questo punto, magari, vi può anche interessare cosa usa, normalmente, il vostro C-blogger preferito (io, no? ah ah ah) quando scrive codice di produzione... ecco la linea!

gcc -Wall -Wextra -pedantic -Wshadow

a volte aggiungo, poi, anche alcuni flag semplici  in base alle esigenze del momento, ma normalmente la linea di compilazione che uso è questa, e permette di produrre applicazioni molto affidabili, ve lo garantisco! Provare per credere...

E direi di finire con una altro esempietto, perché un articolo di programmazione senza un esempio reale è un articolo incompiuto... Vediamo un po': il codice seguente ha un problema, ma compilando senza warning, o anche usando -Wall non segnala niente (!). Ci vorrà mica il -Wextra? Vai col codice!

// typelimits.c
#include <stdio.h>

// prototipi locali
int isGreaterThan200(char c);

// funzione main
int main()
{
int i = 250;
printf("arg = %d --> %s\n",
i, isGreaterThan200(i) ? "è maggiore di 200" : "non è maggiore di 200");
return 0;
}

// isGreaterThan200 - valuta se l'argomento è più grande di 200
int isGreaterThan200(char arg)
{
if (arg > 200) // 200 è troppo grande per un char: la comparazione è sempre falsa
return 1;
else
return 0;
}

compiliamo con -Wall ed eseguiamo:

aldo@Linux $ gcc -Wall typelimits.c -o typelimits
aldo@Linux $ ./typelimits
arg = 250 --> non è maggiore di 200

Ma che strano, è un programma molto semplice ed eppure funziona così male! E il compilatore non ci segnala nulla! Proviamo allora a compilare con -Wextra:

aldo@Linux $ gcc -Wextra typelimits.c -o typelimits
typelimits.c: In function ‘isGreaterThan200’:
typelimits.c:20:13: warning: comparison is always false due to limited range of data type [-Wtype-limits]
20 | if (arg > 200) // 200 è troppo grande per un char: la comparazione è sempre falsa
| ^
aldo@Linux $ ./typelimits
arg = 250 --> non è maggiore di 200

Ok, l'errore di esecuzione è rimasto (e ci mancherebbe solo!), ma almeno sappiamo a cosa è dovuto e possiamo correggerlo facilmente. Questo è solo un esempio dei warning che può segnalare il -Wextra: spesso è veramente utile!

E, prima di chiudere, vorrei ricordarvi che tutti i flag semplici  hanno un flag parallelo che serve a negare (sopprimere) il warning in oggetto, ed è lo stesso flag però con un no- davanti. Quindi, se vogliamo sopprimere uno di quelli inclusi nel -Wall  (ad esempio il -Waddress) possiamo cambiare la linea di compilazione (che ho passato sopra) in questa maniera:

gcc -Wall -Wextra -pedantic -Wshadow -Wno-address

Questa linea modificata aggiungendo un -Wno-address ci permette di compilare segnalando tutti i warning utili ma senza mostrare quelli di tipo -Waddress; e a cosa serve questo? Beh, il compilatore GCC è un gran strumento, ma a volte può sbagliare anche lui offrendoci dei falsi negativi, oppure scambiando per errore cose che abbiamo fatto volutamente, oppure mostrandoci delle segnalazioni che riteniamo completamente ininfluenti: in questo caso possiamo "pulire" l'uscita della compilazione dai messaggi che non ci interessano.

E vabbè, penso che è giunto il momento di chiudere questa larga parentesi sui warning. Non so cosa ci riserverà il futuro... ma sarà sicuramente molto interessante! Restate sintonizzati!

Ciao, e al prossimo post!

venerdì 12 dicembre 2025

Warning Peaks
perché i warning sono importanti in C - pt.1

Dale Cooper: [parlando a un registratore] Diane, quando due eventi, due fatti riguardanti la stessa indagine si verificano simultaneamente, dobbiamo sempre considerarli con la massima attenzione.

Nella bellissima (e mitica) serie TV Twin Peaks il Maestro David Lynch (insieme al fido Mark Frost) ci spiegava che nulla è come appare a prima vista, e che in una investigazione bisogna analizzare tutti i dettagli, anche i più insignificanti, per giungere alla verità. Ebbene si, questo è valido anche nella nostra amata programmazione in C: il nostro strumento principale è il compilatore, che oltre a costruire l'eseguibile dell'applicazione su cui abbiamo lavorato ci fornisce un sacco di informazioni utili sul codice che abbiamo scritto, mostrandoci errori, vulnerabilità e avvisi, i famosi warning, che sono molto (ma molto) più importanti di quello che si pensa... Come avrete già intuito, in questo articolo parleremo di warning!

...ma l'hai letto l'ultimo warning?...

Nella mia lunga carriera di programmatore ho incontrato, a volte, colleghi che non davano la dovuta attenzione ai warning di compilazione, considerandoli semplici avvisi di scarsa importanza... e questo è un errore fondamentale! Anzi, ora che ci penso, un giorno assistei a un interessante dialogo tra mio cuggino e un suo collega. Si, credo che sia ora di far intervenire (sia pur indirettamente) mio cuggino, che manca da tempo da queste pagine. Il dialogo che intercettai era, più o meno, questo:

mio cuggino: Tu sei un programmatore professionista, vero?
programmatore: Si, è il mio mestiere. Anni e anni di esperienza...
mio cuggino: Ma dimmi una cosa: quando compili che linea di compilazione usi?
programmatore: Mah, qualcosa del tipo "gcc -o programma programma.c".
mio cuggino: Ma lo sai che con quella linea vedi solo i warning di base? E così te ne sfuggono alcuni molto importanti.
programmatore: Beh, ma i warning sono solo avvertenze, non servono a niente, già vedere quelli di base mi confonde abbastanza le idee...
mio cuggino: Guarda, se vuoi sviluppare del buon codice dovresti perlomeno attivare il flag -Wall e magari anche il -Wextra...
programmatore: -Wall? -Wextra? Mai sentiti, a cosa servono?. 
mio cuggino: Mai pensato di cambiare mestiere?

Ecco, come sempre mio cuggino fu molto tranciante, ma quella volta io pensai la stessa cosa, ma non lo dissi (tanto l'aveva già detto lui, ah ah ah... si scherza, eh!).

Come dite? Sto esagerando? Ok, allora è il caso di fornire qualche piccolo esempietto... vai col codice!

// uninit.c
#include <stdio.h>

int main()
{
int index;
char *str = "salve, mondo";

printf("%c\n", str[index]);
return 0;
}

Ecco, questo è un codice semplicissimo che stampa il contenuto di una stringa. Proviamo a compilare ed eseguire questo uninit.c:

aldo@Linux $ gcc uninit.c -o uninit
aldo@Linux $ ./uninit
Errore di segmentazione (core dump creato)

Ma va! Un bel segmentation fault per un codice così semplice! E il compilatore non mi dice nulla? Proviamo a aggiunger il flag -Wall:

aldo@Linux $ gcc -Wall uninit.c -o uninit
uninit.c: In function ‘main’:
uninit.c:9:23: warning: ‘index’ is used uninitialized [-Wuninitialized]
9 | printf("%c\n", str[index]);
| ^
uninit.c:6:9: note: ‘index’ was declared here
6 | int index;
| ^~~~~

Oohh... e io che pensavo che i warning non servissero a niente (ah ah ah). Ecco, il codice mostrato è un semplice main di cinque righe, e quindi l'errore si vede facilmente, ma immaginate quando questo pezzetto di codice viene immerso in un grande progetto di centinaia di migliaia di linee: diventerà quasi invisibile, e per risolvere il problema sarà meglio dare un occhiata ai warning, no?

Ed ora un altro esempio con un warning che prima usciva solo attivando il flag -Wformat (che è incluso nel -Wall) e che ora esce anche a livello base (senza nessun flag), ma che molti non notano "perché leggere tutti i warning non serve" (ehm...) :

// format.c
#include <stdio.h>

int main()
{
int a = 12;
printf("%s\n", a);
return 0;
}

Ok, anche questo è un codice semplicissimo che stampa nel terminale una stringa. Proviamo a compilare ed eseguire questo format.c:

aldo@Linux $ gcc format.c -o format
format.c: In function ‘main’:
format.c:8:14: warning: format ‘%s’ expects argument of type ‘char *, but argument 2 has type ‘int[-Wformat=]
8 | printf("%s\n", a);
| ~^ ~
| | |
| | int
| char *
| %d
aldo@Linux $ ./format
Errore di segmentazione (core dump creato)

Ma va! Un altro bel segmentation fault per un codice così semplice! E il compilatore me lo dice già di base! Certo che se non dò peso al warning (magari perché ce ne sono così tanti che faccio finta di non vederli...) il problema verrà poi quando eseguo l'applicazione. Male, male, molto male...

E non parliamo dei problemi che non appaiono costantemente a run-time, perché dipendono, sfortunatamente, da come è occupata la memoria al momento dell'esecuzione (quindi, a volte appaiono e a volte no): costerà moltissimo risolvere questi errori, quando invece sarebbe stato sufficiente risolvere tutti i warning di compilazione man mano che si presentavano! Spero di essere stato chiaro: i warning sono importanti, e non solo quelli di base!

Ok, per oggi può bastare. Nella seconda parte dell'articolo vedremo una panoramica dei flag di compilazione consigliati per realizzare una applicazione a prova di bomba. Come sempre vi raccomando di non trattenere il respiro nell'attesa (può nuocere gravemente alla salute...).

Ciao, e al prossimo post! 

mercoledì 24 gennaio 2024

Valgrind e vengo da lontano
come usare Valgrind con il C (e C++)

Willy: ...quindi ora, se lei è d'accordo, se lei è d'accordo con me, io farei un articolo bellissimo, complesso, ardito anche, su questo argomento a tutta pagina dal titolo: "Con chi lo facciamo accoppiare il lucertolone del Sudan?... Con chi lo facciamo accoppiare?".
caporedattore: [non risponde e gli chiude la porta in faccia]
Willy: [parlando da solo] ...con la troia de la tu moglie lo facciamo accoppiare...

Il dialogo surreale qui sopra è tratto dal bel Willy Signori e vengo da lontano del compianto Francesco Nuti. Il Willy del film è la rappresentazione del "vero amico", quello che ti aiuta nel momento del bisogno, che ti accompagna in maniera disinteressata (vabbé, nel film Willy è guidato anche dai sensi di colpa, ma nel complesso è un vero "cuore d'oro"). E cosa centra Valgrind in tutto questo? Beh, Valgrind è un vero amico del programmatore C (e C++), uno che ti aiuta a scrivere programmi bug-free a prova di bomba, come vedremo tra poco.

...con chi lo facciamo accoppiare il Valgrind?...

Allora, prima di tutto bisogna spiegare cos'è Valgrind, perché non è detto che tutti lo sappiano. Valgrind è un analizzatore dinamico di comportamento del codice, cioè serve per testare il funzionamento di un programma in condizioni reali (o, perlomeno, il più possibile vicino alla realtà). Tanto per rinfrescare i concetti (nel caso che qualcuno ne abbia bisogno) ricordiamo che il primo passo di analisi si fa con gli analizzatori statici, e cioè:

  1. La analisi statica più statica che c'è è, ovviamente, quella della compilazione: attivando gli opportuni flag per il compilatore e interpretando i Warning proposti (errori, per definizione, non ce ne sono mai, ah ah ah) facendo in maniera che spariscano tutti, possiamo già dire di avere un codice sintatticamente corretto. Io consiglio sempre un alto livello di controllo, che con GCC significa attivare i flag Wall e pedantic.
  2. Si può poi passare il codice con un vero e proprio analizzatore statico, quelli della famiglia lint. Ce ne sono vari, ad esempio per C/C++ io uso l'ottimo Cppcheck; notare che un buon lint va oltre i suoi scopi originari, e può mostrare anche errori di tipo "dinamico", ma solo quelli evidenti anche staticamente (uhm, sembra strano ma è così...).
  3. Sia per il caso 1 che per il caso 2 la analisi deve essere fatta cum grano salis: non è detto che non ci siano casi di falsi positivi del lint o che qualche Warning di compilazione non sia così eccessivo e pedante che è opportuno eliminarlo: GCC in questo caso fornisce per ogni tipo di Warning un flag per ometterlo dai risultati.

E adesso veniamo al dunque: un programma che ha passato i 3 punti sopra potrebbe ancora, magicamente, schiantarsi durante l'uso reale o, ancora peggio, schiantarsi ogni tanto, anzi, per la legge di Murphy potrebbe non schiantarsi mai durante lo sviluppo e il testing e cominciare a dare problemi ogni tanto dopo la distribuzione sulle macchine di produzione (con grande felicità dei clienti...) il che rende molto difficile capire dove è il problema. Questo è dovuto, quasi sempre, a problemi nella gestione della memoria, che pregiudicano il comportamento dinamico.

(...apro una parentesi: in questi casi il programmatore inesperto e/o che non ha il controllo totale del codice scritto ricorre a un debugger per trovare l'errore; ecco, bisognerebbe ricordarsi che, come ho già scritto in un vecchio articolo, il debugger aiuta solo a risolvere problemi facili (e ripetibili), mentre per problemi difficili (o non ripetibili) non serve a nulla, anzi potrebbe dare informazioni fuorvianti. Riepiloghiamo: il debugger aiuta solo a risolvere i problemi semplici, ma questi problemi si possono risolvere, con competenza e un po' di intuito, senza usare il debugger: uhm... è una classica situazione da "gatto che si morde la coda". Chiudo la parentesi...)

E come si procede in questo caso? ma usando un analizzatore dinamico! Uno come Valgrind che analizza il comportamento del programma "mentre sta lavorando", e fornisce dati utilissimi di memoria occupata e non liberata, memoria non inizializzata, ecc. arrivando anche a dettagliare lo stack di chiamata delle istruzioni che hanno generato l'errore, numeri di linea inclusi. Un bell'aiuto, non c'è che dire! Ovviamente anche Valgrind deve essere usato correttamente, per evitare di farsi ingannare da eventuali falsi positivi e, soprattutto, la analisi deve essere effettuata in condizioni reali.

A questo riguardo vi faccio un esempio semplicissimo: supponiamo di analizzare il comportamento di un Server TCP: se lo lanciamo attraverso Valgrind e non gli facciamo fare nulla il risultato sarà: "Il programma è perfetto!", e in questo caso la considerazione successiva sarebbe "E grazie al..."! È evidente che il funzionamento dinamico di un Server TCP si deve analizzare in condizione di forte carico di rete, magari anche molto più alto di quello usuale, e con molteplici tipi di messaggi, perché i problemi (se ci sono) verranno fuori solo durante l'uso intensivo.

E, a questo punto ci vuole un po' di codice: vi propongo un semplicissimo programma pieno di errori, che si vedono a prima vista, ma pensate che questi errori potrebbero essere distribuiti, uno qua, uno la, su n-mila linee di codice scritte su n-file (quindi non più facilmente visibili). Questo programma passa bene la compilazione con tutti i Warning attivati, il Cppcheck trova qualche errore pseudo-dinamico (ma non tutti) e, dulcis in fundo, nell'uso reale apparentemente non si schianta (ma questo dipende un po' dalla fortuna del momento). Vai col codice!

// test.c - test per Valgrind
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LEN_ARRAY 16
#define NUM_STRINGS 8

// test - funzione main
int main(void)
{
// alloco memoria per un array di int di lunghezza LEN_ARRAY
int *myarray = malloc(LEN_ARRAY * sizeof(int));

// errore 1: scrivo una posizione non disponibile (write out-of-bound)
myarray[LEN_ARRAY] = 0;

// errore 2: leggo una posizione non disponibile (read out-of-bound)
printf("myarray[%d] = %d\n", LEN_ARRAY, myarray[LEN_ARRAY]);

// alloco memoria per le stringhe di un array di NUM_STRINGS char pointers
char *strings[NUM_STRINGS];
for (int i = 0; i < NUM_STRINGS; i++) {
strings[i] = malloc(LEN_ARRAY * sizeof(char));
memset(strings[i], 0, LEN_ARRAY * sizeof(char));
}

// errore 3: manca la free(3) delle stringhe e anche di myarray (memory leak)

return 0;
}

Come vedete sono pochissime linee di programma, con ben tre errori che, ripeto, qui si vedono immediatamente, ma se immersi in n-mila linee di codice sono difficili da trovare.

E come si usa Valgrind? Allora bisogna, semplicemente, compilare il programma (con l'opzione di mantenere i simboli, così avremo anche le informazioni dei numeri di linea) ed eseguire attraverso Valgrind. Vediamo, ad esempio cosa succede con GCC:

aldo@Linux $ gcc -g -Wall -pedantic test.c -o test
aldo@Linux $ ./test
myarray[16] = 0
aldo@Linux $ valgrind ./test --leak-check=full
==40693== Memcheck, a memory error detector
==40693== Copyright (C) 2002-2017, and GNU GPL d, by Julian Seward et al.
==40693== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==40693== Command: ./test --leak-check=full
==40693==
==40693== Invalid write of size 4
==40693== at 0x1091DA: main (test.c:16)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)
==40693==
==40693== Invalid read of size 4
==40693== at 0x1091E8: main (test.c:19)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)
==40693==
myarray[16] = 0
==40693==
==40693== HEAP SUMMARY:
==40693== in use at exit: 192 bytes in 9 blocks
==40693== total heap usage: 10 allocs, 1 frees, 1,216 bytes allocated
==40693==
==40693== LEAK SUMMARY:
==40693== definitely lost: 192 bytes in 9 blocks
==40693== indirectly lost: 0 bytes in 0 blocks
==40693== possibly lost: 0 bytes in 0 blocks
==40693== still reachable: 0 bytes in 0 blocks
==40693== suppressed: 0 bytes in 0 blocks
==40693== Rerun with --leak-check=full to see details of leaked memory
==40693==
==40693== For lists of detected and suppressed errors, rerun with: -s
==40693== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Visto? La compilazione non ha dato Warning (nonostante l'uso di Wall e pedantic) e anche l'esecuzione è andata apparentemente bene (che fortuna!). Poi Valgrind ha trovato tutti i problemi del programma, permettendoci di modificare a colpo sicuro il codice per evitarli. Ovviamente questo è un caso semplice e, come già detto sopra, possono esserci dei falsi positivi o informazioni un po criptiche (specialmente nei programmi multithread) che si devono interpretare. Comunque Valgrind è un aiuto preziosissimo e altamente raccomandabile anche (e soprattutto, direi) per programmi grandi e complessi, basta trovare la maniera di eseguirlo simulando carichi di lavoro reali.

Nell'esempio qui sopra l'interpretazione è abbastanza semplice: il numero ==40693== è il PID del processo eseguito, le prime linee seguenti sono, evidentemente, di presentazione, dopodiché vengono mostrati i veri e propri errori:

==40693== Invalid write of size 4
==40693== at 0x1091DA: main (test.c:16)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)

questo è, evidentemente, quello che nel codice è alla linea 14, come anticipato nel commento del codice:

// errore 1: scrivo una posizione non disponibile (write out-of-bound)
myarray[LEN_ARRAY] = 0;

Notare che viene fornito anche lo stack delle chiamate che finalizzano con la linea 16 di test.c. Dopodiché c'è il secondo errore:

==40693== Invalid read of size 4
==40693== at 0x1091E8: main (test.c:19)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)

questo è, evidentemente, quello che nel codice è alla linea 19, come anticipato nel commento del codice:

// errore 2: leggo una posizione non disponibile (read out-of-bound)
printf("myarray[%d] = %d\n", LEN_ARRAY, myarray[LEN_ARRAY]);

A questo punto, visto che Valgrind ha già mostrato gli errori effettivi, viene mostrato un riepilogo del'uso della memoria, che evidenzia il terzo errore, che non si riferisce alle istruzioni errate ma, bensì, alle istruzioni mancanti, le free(3):

==40693== HEAP SUMMARY:
==40693== in use at exit: 192 bytes in 9 blocks
==40693== total heap usage: 10 allocs, 1 frees, 1,216 bytes allocated
==40693==
==40693== LEAK SUMMARY:
==40693== definitely lost: 192 bytes in 9 blocks
==40693== indirectly lost: 0 bytes in 0 blocks
==40693== possibly lost: 0 bytes in 0 blocks
==40693== still reachable: 0 bytes in 0 blocks
==40693== suppressed: 0 bytes in 0 blocks

Come si nota, il numero di allocs e frees non corrisponde, il che indica che mancano 9 free(3) (1 per myarray e 8 per strings), e i byte persi, che sono 192, corrispondono esattamente ai dati allocati e non liberati.

Ok, per oggi può bastare. Vi sentite già esperti del Valgrind? Ecco, se lo provate su un programma grande, complesso e (magari) pieno di errori, il risultato potrebbe essere esteso e un po' preoccupante; in quel caso bisogna interpretare bene le informazioni e correggere uno per uno gli errori. Vi raccomando di farlo, vi eviterete molti mal di testa dovuti a problemi stranissimi segnalati dai clienti.

E poi mettiamo le cose in chiaro: l'obiettivo di un buon programmatore è produrre e distribuire Software bug-free... E si può! Si può'! Anzi in alcuni tipi di applicazioni, tipicamente quelle mission-critical e quelle business-critical, il Software distribuito deve essere sempre bug-free, ricordatelo! E Valgrind sarà un prezioso amico per raggiungere l'obiettivo, ve l'assicuro. Ovviamente dopo l'analisi statica vista all'inizio dell'articolo e la seguente analisi dinamica fatta con Valgrind mancherebbe una terza analisi, quella logica: cioè, se un programma è staticamente e dinamicamente perfetto ma non fa bene quello che dovrebbe fare non ci sono analizzatori che tengano: bisogna studiarlo e implementarlo bene! Tenetelo presente... ah ah ah.

Ciao, e al prossimo post!

lunedì 28 novembre 2022

Atomichunter
come usare le funzioni atomic built-in di GCC in C

Graham: Non è curioso di sapere se Lei è più furbo di quello che cerchiamo?
Lecktor: Stai scherzando? Tu sei più furbo, visto che mi hai preso...
Graham: No, non sono più furbo di Lei.
Lecktor: E allora come hai fatto a prendermi?
Graham: Lei era svantaggiato.
Lecktor: In che senso svantaggiato?
Graham: Lei è pazzo.

Per parlare delle funzioni atomic built-in di GCC ho deciso di fare un temerario abbinamento con il capolavoro Manhunter del Maestro Michael Mann, un film assolutamente imperdibile. Il fatto è che per risolvere un problema pratico (ebbene si, oggi parleremo del mondo reale) a volte un programmatore deve trasformarsi in un cacciatore (fortunatamente non di uomini), e quindi deve investigare, provare, cercare informazioni qui e là per risolvere un caso reale. Si, bisogna fare come il detective Graham del film: beh, magari senza arrivare ai suoi estremi, e chi ha visto il film sa di cosa parlo...

...sai, ti ho preso grazie alle atomic built-in di GCC...

Non so se ricordate un mio articolo di un annetto fa, Atomic: Endgame, in cui decantavo la bontà del nuovo type qualifier _Atomic introdotto con C11 (e, ovviamente, se non lo ricordate siete tenuti a rileggerlo...). Nell'articolo era (spero) ben descritta la nuova funzionalità e si evidenziava che, grazie a _Atomic, la scrittura di applicazioni multi-thread  diventava molto più agevole (era ora!). E perché oggi torniamo sull'argomento? E qui sopraggiunge il problema pratico citato sopra: supponiamo (true story) di dover aggiungere una nuova prestazione a una vecchia applicazione su Linux Embedded usando la piattaforma (e quindi il compilatore) originale, che, per la legge di Murphy, non supporta C11 (anzi supporta a stento C99): che facciamo? Dovremo mica riscrivere il codice (già scritto e testato su piattaforme più recenti) in modo di usare i classici meccanismi di sincronizzazione delle variabili (mutex, ecc.)? No, ci deve essere una soluzione più semplice!

E quindi mi sono Graham-izzato (vedi sopra) e ho scoperto che il nostro amato GCC, grande amico dei programmatori C (e non solo), ha da molto tempo delle funzioni built-in che hanno anticipato le funzionalità di _Atomic ben prima del supporto a C11. Anzi, abbiamo ben due famiglie di built-in denominate __sync_* e __atomic_* , aggiunte con a la seguente sequenza temporale:

  • GCC 4.1: __sync_* built-in (nel 02/2006)
  • GCC 4.4: __sync_* built-in (nel 04/2009,  con supporto ARM)
  • GCC 4.7: __atomic_* built-in (nel 03/2012)
  • GCC 4.9: _Atomic (nel 04/2014, supporto completo)

Considerando la data in cui _Atomic è entrato in GCC il supporto a queste operazioni è stato fornito con un buon anticipo, no? Nella lista qui sopra ho anche evidenziato quando è stata aggiunta la compatibilità con ARM, perché l'argomento di questo articolo è decisamente indirizzato alla programmazione con Linux Embedded, e ARM è una piattaforma molto usata in questo ambito. Tra l'altro si può evidenziare che anche le versioni recenti di GCC continuano a supportare questi built-in, e quindi un eventuale Software "datato" che le usa si può ancora compilare senza problemi con un GCC recente, e senza modificare nulla (questa si che è vera retro-compatibilità! Grazie gcc.gnu.org!).

Nella mia ricerca ho anche notato che la quantità di informazioni a livello base (tipicamente quella fornita da gcc.gnu.org) è notevole in quantità e qualità, però latitano un po' i veri esempi d'uso (il codice!), e quindi ho deciso di contribuire un po' con questo articolo, che interesserà a pochi (credo) ma per quei pochi potrebbe essere una manna dal cielo (vabbé, non esageriamo...).

Che ho fatto allora? Ho scritto una mini-libreria per semplificare l'uso dell funzioni atomic built-in, e ho strutturato la libreria in maniera che si comporti in modo "intelligente" riconoscendo la versione del GCC in uso, per passare automaticamente da _Atomic a __atomic_* oppure a __sinc_*. Ho scritto un header, atoint.h che definisce un nuovo tipo atoint (nome originalissimo che sta per atomic int). Vediamolo: vai col codice!

/* atoint.h - header della libreria atoint
versioni delle funzioni atomiche:
_Atomic: da GCC 4.9 in avanti (40900L)
__atomic: da GCC 4.7 in avanti (40700L)
__sync con ARM: da GCC 4.4 in avanti (40400L)
__sync: da GCC 4.1 in avanti (40100L) */

// GCC_VERSION contiene la versione di GCC come numero (e.g.: GCC4.9 = 40900)
#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__)

#if GCC_VERSION >= 40900L
// da GCC 4.9 in avanti è disponibile _Atomic di C11
#include <stdatomic.h>
typedef atomic_int atoint; // uso atomic_int per atoint
#else
// _Atomic di C11 non è disponibile
typedef int atoint; // uso int per atoint
#endif

#if GCC_VERSION >= 40900L
// uso le funzioni di _Atomic
#define ATOMIC_STORE(ptr, val) atomic_store((ptr), val)
#define ATOMIC_LOAD(ptr) atomic_load(ptr)
#define ATOMIC_INC(ptr) atomic_fetch_add((ptr), 1)
#define ATOMIC_DEC(ptr) atomic_fetch_sub((ptr), 1)
#elif GCC_VERSION >= 40700L
// uso le funzioni di GCC __atomic prefixed built-in
#define ATOMIC_STORE(ptr, val) __atomic_store_n((ptr), val, __ATOMIC_SEQ_CST)
#define ATOMIC_LOAD(ptr) __atomic_load_n((ptr), __ATOMIC_SEQ_CST)
#define ATOMIC_INC(ptr) __atomic_fetch_add((ptr), 1, __ATOMIC_SEQ_CST)
#define ATOMIC_DEC(ptr) __atomic_fetch_sub((ptr), 1, __ATOMIC_SEQ_CST)
#elif GCC_VERSION >= 40100L
// TODO: scrivere la versione con __sync prefixed built-in
// ...
#else
#error Spiacente, il tuo compilatore è troppo vecchio - aggiornalo, per favore.
#endif

// prototipi globali
atoint atoStore(atoint *val, atoint newval); // store
atoint atoLoad(atoint *val); // load
atoint atoPreInc(atoint *value); // prefix increment (++val)
atoint atoPostInc(atoint *value); // postfix increment (val++) (usa il prefix increment)
atoint atoPreDec(atoint *value); // prefix decrement (--val)
atoint atoPostDec(atoint *value); // postfix decrement (val--) (usa il prefix decrement)

Come si può ben vedere il file contiene la definizione del nuovo tipo e i prototipi delle funzioni della libreria. E, soprattutto, contiene degli alias (delle #define) delle funzioni che usano le appropriate atomic built-in in base alla versione del compilatore in uso, versione che è contenuta nella macro GCC_VERSION (è un numero identificativo abbastanza semplice da calcolare, ad esempio per GCC 4.9 è 40900). Con l'uso di alcune semplici direttive #if  per il preprocessore del C possiamo scegliere quali alias attivare automaticamente. In questa versione ho usato come requisito minimo per la famiglia __sync_* il GCC 4.1  (ver.40100), ma chi ha bisogno del supporto ARM dovrebbe usare come requisito minimo il GCC 4.4 (ver.40400). Usando un compilatore ancora più anziano del requisito minimo, la compilazione esce (miseramente) con questo errore: "Spiacente, il tuo compilatore è troppo vecchio - aggiornalo, per favore.".

Visto che non volevo mettere troppa carne al fuoco ho lasciato come TODO  la versione con __sync_*  che è un po' più complicata e avrebbe appesantito un po' il codice. Tra l'altro si dovrebbero/potrebbero aggiungere anche altre funzioni oltre a quelle presentate qui (e si può fare: io, mio malgrado, ho dovuto scrivere una versione full-optional perché mi serviva): magari ci torneremo in un prossimo articolo.

Grazie agli alias delle funzioni la implementazione della libreria è molto semplice e lineare. Vediamola:

// atoint.c - implementazione della libreria atoint
#include "atoint.h"

// store (assegna il valore)
atoint atoStore(atoint *val, atoint newval)
{
ATOMIC_STORE(val, newval);
return *val; // return del valore nuovo
}

// load (legge il valore)
atoint atoLoad(atoint *val)
{
return ATOMIC_LOAD(val);
}

// prefix increment (++val)
atoint atoPreInc(atoint *val)
{
ATOMIC_INC(val); // incrementa atomicamente
return *val; // return del valore nuovo
}

// postfix increment (val++) (usa il prefix increment)
atoint atoPostInc(atoint *val)
{
atoint old = *val; // salva il valore vecchio
atoPreInc(val); // incrementa atomicamente
return old; // return del valore vecchio
}

// prefix decrement (--val)
atoint atoPreDec(atoint *val)
{
ATOMIC_DEC(val); // decrementa atomicamente
return *val; // return del valore nuovo
}

// postfix decrement (val--) (usa il prefix decrement)
atoint atoPostDec(atoint *val)
{
atoint old = *val; // salva il valore vecchio
atoPreDec(val); // decrementa atomicamente
return old; // return del valore vecchio
}

È veramente molto semplice, no? E grazie ai molti commenti, credo che non sono necessarie ulteriori spiegazioni.

E, dulcis in fundo, bisogna provare se la libreria funziona, no? Si, ci vuole un bel programma di test, vai col codice!

#include <stdio.h>
#include <pthread.h>
#include "atoint.h"

// prototipi locali
void* updateCnt(void* arg);

// variabili globali (per un semplice test si possono usare!)
atoint ato_cnt; // un int atomico usato come contatore
int cnt; // un int normale usato come contatore

// funzione main
int main(void)
{
// init contatori
atoStore(&ato_cnt, 0);
cnt = 0;

// avvio 10 thread
pthread_t tid[10];
for (int n = 0; n < 10; ++n)
pthread_create(&tid[n], NULL, &updateCnt, NULL);

// attendo la fine dei 10 thread
for(int n = 0; n < 10; ++n)
pthread_join(tid[n], NULL);

// i risultati!
printf("Il contatore atomico vale: %u\n", ato_cnt);
printf("Il contatore normale vale: %u\n", cnt);

return 0;
}

// updateCnt() - funzione di update dei counter eseguita da ogni thread
void* updateCnt(void* arg)
{
// incremento i counter per 1000 volte
for (int n = 0; n < 1000; ++n) {
// uso prefix increment
atoPreInc(&ato_cnt);
++cnt;
}

return NULL;
}

I più attenti avranno notato che è esattamente lo stesso programma di test mostrato nell'articolo Atomic: Endgame, solo che in questo caso si usa il nuovo tipo atoint invece del tipo standard atomic_int: se il test funzionasse potremmo affermare che la libreria atoint è un buon sostituto della versione standard. Ci riusciremo? Proviamo: prima di tutto se stiamo usando un GCC recente (cosa probabile) dobbiamo forzare l'uso della versione senza _Atomic (ad esempio "truccando"  la define di GCC_VERSION), poi possiamo compilare ed eseguire. Il risultato è questo:

aldo@Linux $ ./test
Il contatore atomico vale: 10000
Il contatore normale vale: 4902

Ma funziona! Il contatore atomico atoint ha contato esattamente tutti gli incrementi e vale 10000 mentre il contatore normale ha perso molti degli incrementi a causa della concorrenza, e, quindi, vale molto meno (e ripetendo l'esecuzione si ottengono sempre valori inferiori a 10000). Se poi vogliamo fare anche "la prova del 9"  possiamo togliere il trucco che forza la versione antica e ricompilare con il nostro GCC recente: con _Atomic,  il risultato dovrebbe essere lo stesso (e se non lo è sarebbe il caso di dire "Houston, abbiamo un problema.").

E, se avete voglia di sperimentare, potete modificare leggermente il codice per usare anche le tre funzioni inutilizzate (atoPostInc(), atoPreDec() e atoPostDec()), vi assicuro che funzionano bene.

Ok, personalmente a questo punto sono soddisfatto e per oggi possiamo concludere qui. La libreria atoint funziona e spero vivamente che possa risultare utile a molti lettori. Nel prossimo articolo cambieremo argomento, ma, come promesso sopra, prima o poi torneremo su questa storia per completare la trattazione... e non trattenete il respiro nell'attesa, mi raccomando!

Ciao, e al prossimo post!