3 votes

Vérifier que les initialisateurs et les finaliseurs sont garantis d'être logiquement appariés dans une portée de bloc.

Existe-t-il une méthode ou un outil automatique permettant d'analyser le code C afin de déterminer si tous les appels de fonction initialisateur sont logiquement associés à un appel de fonction finalisateur correspondant dans la portée d'un bloc ?

Par initialisateur et finalisateur, j'entends une paire comme fopen() y fclose() .


Des modifications importantes ont été apportées à la question originale : L'expression "chemin de code" a été supprimée et les expressions "logiquement apparié" et "dans la portée d'un bloc" ont été ajoutées afin de clarifier l'intention initiale. Voir la discussion approfondie sous la réponse d'Eugene Sh. Par "logiquement apparié", je veux dire par opposition à "textuellement apparié", c'est-à-dire qu'il est sans importance que le texte du code source contienne un nombre égal d'appels à l'initialisateur et au finalisateur. Au contraire, lorsqu'on sort de la portée d'un bloc (si jamais), tout appel de fonction initialisateur est garanti avoir un appel de fonction finalisateur correspondant.

Exemples

Ok :

initialize();
mystery_function_that_may_never_return();
finalize();

Ok :

initialize();
if (cond)
   finalize();
else
   finalize();

Pas bien :

initialize();
initialize();
finalize();

Pas bien :

initialize();
finalize();
finalize();

Pas bien :

initialize();
if (cond)
    finalize();
else
    return;

Il pourrait également être judicieux de transférer la responsabilité de la finalisation/transfert de la "propriété" de la ressource gérée. Dans ce cas, le contrôle consisterait à vérifier que toutes les initialisations conduisent à une finalisation ou à un transfert de propriété.

3voto

Schwern Points 33677

Probablement pas.

Tout ce qui est du type "déterminer au moment de la compilation si X se produit au moment de l'exécution" ou "déterminer tous les chemins du code" en C se heurte à l'obstacle de l'utilisation de l'anglais. problème de l'arrêt . On peut peut-être s'en sortir avec un langage purement fonctionnel tel que Haskell .

Par exemple...

bool find_thing( FILE *fp, const char* thing ) {
    ...go searching through the fp for thing...

    if( found_the_thing ) {
        fclose(fp);
        return true;
    }
    else {
        rewind(fp);
        return false;
    }
}

FILE *fp = fopen( filename, "r");
while( !find_thing(fp, that_thing) ) {
    ...mess with the file...
}

C'est un peu artificiel, mais ça me vient à l'esprit, peut-être plus applicable aux prises de courant. Le fait est qu'une fois que votre initialisateur et votre finalisateur ne sont pas dans une séquence linéaire, une fois qu'ils sont transmis, ajoutés aux structures et aux tableaux, peut-être même assignés à un global, vous avez des problèmes.

Voici un autre exemple.

typedef struct {
    FILE *fp;
    const char *filename;
} FileHandle;

FileHandle fh = malloc(sizeof(FileHandle));
fh->filename = file;
fh->fp = fopen(file, "r");

do_something(fh);

fclose(fh);
free(fh->filename);
free(fh);

Cela semble assez simple ! Sauf que comment savez-vous que fh->fp contient le même pointeur de fichier que celui avec lequel il a été initialisé ? Et si do_something l'a changé ? Et s'il l'avait déjà fermée ? Et s'il en a ouvert une nouvelle ?

Voici un autre exemple.

FILE *fps[10];

...initialize fps...

/* Returns a file pointer from fps */
FILE *fp = highest_priority(fps);

...do something with fp...

fclose(fp);

Quel pointeur de fichier a été fermé ?

Comme mentionné dans les commentaires, il est peut-être plus facile de penser à cela en termes d'allocation et de libération de la mémoire, ce qui pose le même problème. Comment savons-nous fh->filename n'a pas été modifié ? Et si c'était NUL ? Et s'il a déjà été libéré ? Que se passe-t-il si fh a déjà été libéré ?

Un outil partiel ne ferait que les cas triviaux, ou bien il donnerait trop de faux positifs ou négatifs, au point d'être inutile ou si ennuyeux que vous l'éteindriez rapidement.

Si vous rencontrez souvent ce problème, envisagez de passer à un style qui gère toutes les opérations d'E/S en une seule et même fonction. Passez-lui un pointeur de fonction de ce qu'il faut faire avec chaque ligne. Par exemple, à titre indicatif...

int read_file( const char* filename, void *thunk,
               void(*handle_line)(const char *, void *));

Le mieux que vous puissiez faire est de fournir un runtime qui vous indique si des ressources n'ont pas été finalisées à la fin du programme, ou finalisées deux fois, ou utilisées alors qu'elles n'étaient pas initialisées. C'est ainsi que fonctionnent la plupart des vérificateurs de mémoire en C, tels que Valgrind .

2voto

Eugene Sh. Points 7587

Comme @Schwern l'a mentionné, ce problème est réductible à la Problème d'arrêt .

Considérons une fonction "initialisatrice". Start() et la fonction "finalisateur". Stop() .

Maintenant, pour résoudre le problème d'arrêt du programme P() il suffirait d'exécuter l'outil proposé sur le programme suivant :

Start();
P();
Stop();

et vérifier si oui ou non Start() est "jumelé" avec Stop() .

Plus formellement :

Supposons qu'il existe une fonction Paired(A) en retournant true si a il existe un chemin de code entre la fonction Start() y Stop() (en cas de présence dans A ) pour toute entrée donnée d'un programme. Alors, pour tout programme P() nous pouvons construire un programme P'() de telle sorte que P'() est donné par :

Start();
P();
Stop();

pour laquelle Paired(P') = Halting(P) . D'où le fait que Paired nous pouvons résoudre Halting pour tout programme P .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X