VVVVVV ??? VVVVVV !!! :)

Si vous lisez ce texte, cela signifie que vous avez pensé que quelque chose n'allait pas avec le titre de l'article, ou que vous y avez vu le nom d'un jeu informatique familier. VVVVVV est un jeu de plateforme indépendant qui a conquis le cœur de nombreux joueurs avec sa simplicité externe agréable et sa complexité interne tout aussi agréable. Il y a quelques jours, VVVVVV a eu 10 ans, et l'auteur du jeu - Terry Cavanagh - a célébré cette fête avec la publication de son code source. Qu'est-ce qui est «savoureux»? Lisez la réponse dans cet article.

Figure 1


Présentation


Oh, VVVVVV ... Je me souviens comment je l'ai rencontré peu de temps après la sortie, et, étant un grand fan de jeux rétro pixel, je l'ai heureusement installé sur mon ordinateur. Je me souviens de mes premières impressions: «Et alors, c'est tout? Il suffit de courir dans les pièces carrées? »J'ai pensé après quelques minutes de jeu. Je ne savais pas encore ce qui m'attendait. Dès que j'ai quitté le lieu de départ, je me suis retrouvé dans un monde bidimensionnel petit mais confus et orné, plein de paysages inhabituels et d'artefacts de pixels inconnus de moi.

Ce jeu m'a entraîné. Malgré la grande complexité, habilement battue par le contrôle inhabituel de l'époque (le personnage principal ne sait pas sauter, mais est capable d'inverser la direction du vecteur de gravité pour moi), je l'ai complètement dépassé. Je ne sais pas combien de fois mon personnage est mort à ce moment-là, mais je suis sûr que le nombre de morts se mesure en dizaines de centaines. Pourtant, il y a un charme unique dans ce jeu :)

Revenons au code source présenté en l'honneur de l'anniversaire du jeu .

En ce moment, je suis C ++ - le développeur de l'équipe PVS-Studio - un analyseur de code statique pour C, C ++, C # et Java. En plus du développement lui-même, nous sommes également engagés dans la promotion de notre produit. Pour nous, l'une des meilleures façons de le faire est d'écrire des articles sur la vérification des projets open source. Nos lecteurs reçoivent des articles intéressants sur des sujets de programmation, et nous avons l'occasion de démontrer clairement les capacités de PVS-Studio. Par conséquent, lorsque j'ai appris l'ouverture du code source VVVVVV, je ne pouvais tout simplement pas passer.

Dans cet article, nous examinerons quelques erreurs intéressantes trouvées par l'analyseur PVS-Studio dans le code VVVVVV, ainsi qu'une analyse détaillée de ces erreurs. Remettez le vecteur de gravité en position «basse» et asseyez-vous: nous commençons!

Aperçu des alertes émises par l'analyseur


Avertissement 1


V512 Un appel de la fonction 'sprintf' entraînera un débordement de la mémoire tampon 'fileSearch'. FileSystemUtils.cpp 307

#define MAX_PATH 260 .... void PLATFORM_migrateSaveData(char *output) { char oldLocation[MAX_PATH]; char newLocation[MAX_PATH]; char oldDirectory[MAX_PATH]; char fileSearch[MAX_PATH]; .... /* Same place, different layout. */ strcpy(oldDirectory, output); sprintf(fileSearch, "%s\\*.vvvvvv", oldDirectory); .... } 

Comme vous pouvez le voir, les lignes fileSearch et oldDirectory ont la même taille: 260 caractères. La chaîne de formatage (le troisième argument de sprintf ) après avoir remplacé le contenu de la chaîne oldDirectory en elle ressemblera à ceci:

 <i>_oldDirectory\*.vvvvvv</i> 

Cette chaîne fait 9 caractères de plus que l'ancien répertoire d' origine. C'est cette séquence de caractères qui sera ensuite écrite dans fileSearch . Que se passe-t-il si la chaîne oldDirectory dépasse 251? La chaîne résultante sera plus longue que fileSearch ne peut accepter, ce qui entraînera l'écriture en dehors du tableau. Quel type de données dans la RAM peut être endommagé et quel résultat cela conduira à une question rhétorique :)

Avertissement 2


V519 La variable 'background' reçoit des valeurs successives deux fois. C'est peut-être une erreur. Vérifier les lignes: 1367, 1373. Map.cpp 1373

 void mapclass::loadlevel(....) { .... case 4: //The Warpzone tmap = warplevel.loadlevel(rx, ry, game, obj); fillcontent(tmap); roomname = warplevel.roomname; tileset = 1; background = 3; // <= dwgfx.rcol = warplevel.rcol; dwgfx.backgrounddrawn = false; warpx = warplevel.warpx; warpy = warplevel.warpy; background = 5; // <= if (warpy) background = 4; if (warpx) background = 3; if (warpx && warpy) background = 5; break; .... } 

Une même variable se voit attribuer une valeur deux fois de suite. De plus, entre les affectations, cette variable n'est utilisée nulle part. Étrange ... Peut-être que cette séquence ne viole pas la logique du programme, mais de telles affectations en elles-mêmes parlent d'une certaine confusion lors de l'écriture de code. Que ce soit réellement une erreur ne peut être dit que par l'auteur. Bien qu'il existe des exemples plus brillants de cette erreur dans le code:

 void Game::loadquick(....) { .... else if (pKey == "frames") { frames = atoi(pText); frames = 0; } .... } 

Il est déjà clair ici que quelque part ici se trouve soit une erreur de logique, soit au moins une affectation inutile. Peut-être que la deuxième ligne a été écrite temporairement pour le débogage, puis ils ont oublié de la supprimer. Au total, PVS-Studio a émis 8 avertissements concernant de telles situations.

Avertissement 3


V808 L'objet 'pKey' de type 'basic_string' a été créé mais n'a pas été utilisé. editor.cpp 1866

 void editorclass::load(std::string &_path) { .... std::string pKey(pElem->Value()); .... if (pKey == "edEntities") { int i = 0; for (TiXmlElement *edEntityEl = pElem->FirstChildElement(); edEntityEl; edEntityEl = edEntityEl->NextSiblingElement()) { std::string pKey(edEntityEl->Value()); // <= //const char* pText = edEntityEl->GetText() ; if (edEntityEl->GetText() != NULL) { edentity[i].scriptname = std::string(edEntityEl->GetText()); } edEntityEl->QueryIntAttribute("x", &edentity[i].x); edEntityEl->QueryIntAttribute("y", &edentity[i].y); edEntityEl->QueryIntAttribute("t", &edentity[i].t); edEntityEl->QueryIntAttribute("p1", &edentity[i].p1); edEntityEl->QueryIntAttribute("p2", &edentity[i].p2); edEntityEl->QueryIntAttribute("p3", &edentity[i].p3); edEntityEl->QueryIntAttribute("p4", &edentity[i].p4); edEntityEl->QueryIntAttribute("p5", &edentity[i].p5); edEntityEl->QueryIntAttribute("p6", &edentity[i].p6); i++; } EditorData::GetInstance().numedentities = i; } .... } 

Code très bizarre. L'analyseur met en garde contre la variable pKey créée mais non utilisée, mais en fait, le problème s'est avéré plus intéressant. J'ai spécifiquement marqué d'une flèche la ligne sur laquelle l'avertissement a été émis, car cette fonction contient plusieurs définitions de ligne avec le nom pKey . Oui, une autre variable de ce type est déclarée à l'intérieur de la boucle for et, par son nom, elle chevauche celle déclarée à l'extérieur de la boucle.

Ainsi, si vous accédez à la valeur de la chaîne pKey en dehors de la boucle for , vous obtiendrez une valeur égale à pElem-> Value () , mais si vous faites de même à l'intérieur de la boucle, vous obtiendrez une valeur égale à edEntityEl-> Value () . Le chevauchement des noms est une erreur plutôt grossière, qui peut être très difficile à trouver par vous-même lors de la révision du code.

Avertissement 4


Performances réduites du V805 . Il est inefficace d'identifier une chaîne vide en utilisant la construction 'strlen (str)> 0'. Un moyen plus efficace consiste à vérifier: str [0]! = '\ 0'. physfs.c 1604

 static char *prefDir = NULL; .... const char *PHYSFS_getPrefDir(const char *org, const char *app) { .... assert(strlen(prefDir) > 0); ... return prefDir; } /* PHYSFS_getPrefDir */ 

L'analyseur a trouvé une place pour une microoptimisation potentielle. Il utilise la fonction strlen pour vérifier une chaîne de style C pour void. Cette fonction "parcourt" tous les éléments de la chaîne et vérifie chacun pour leur égalité au terminal zéro ('\ 0'). Si une grande chaîne est entrée, chacun de ses caractères sera toujours comparé à une chaîne zéro.

Mais il suffit de vérifier que la chaîne n'est pas vide! Pour ce faire, découvrez simplement si le premier caractère de la chaîne est un zéro terminal. Par conséquent, pour optimiser cette vérification à l'intérieur de l'assertion, vous devez écrire:

 str[0] != '\0' 

C'est la recommandation que l'analyseur nous donne. Bien sûr, l'appel de la fonction strlen est dans la condition de macro assert , il ne sera donc exécuté que dans la version de débogage, où la vitesse n'est pas si importante. Dans la version finale, l'appel de fonction disparaîtra et le code fonctionnera rapidement. Néanmoins, j'ai voulu démontrer les capacités de l'analyseur à proposer des microoptimisations.

Avertissement 5


Pour afficher l'erreur suivante, j'ai besoin de joindre deux morceaux de code ici: la déclaration de la classe entclass et son constructeur. Commençons par l'annonce:

 class entclass { public: entclass(); void clear(); bool outside(); public: //Fundamentals bool active, invis; int type, size, tile, rule; int state, statedelay; int behave, animate; float para; int life, colour; //Position and velocity int oldxp, oldyp; float ax, ay, vx, vy; int cx, cy, w, h; float newxp, newyp; bool isplatform; int x1, y1, x2, y2; //Collision Rules int onentity; bool harmful; int onwall, onxwall, onywall; //Platforming specific bool jumping; bool gravity; int onground, onroof; int jumpframe; //Animation int framedelay, drawframe, walkingframe, dir, actionframe; int yp; int xp; }; 

Le constructeur de cette classe ressemble à ceci:

 entclass::entclass() { clear(); } void entclass::clear() { // Set all values to a default, // required for creating a new entity active = false; invis = false; type = 0; size = 0; tile = 0; rule = 0; state = 0; statedelay = 0; life = 0; colour = 0; para = 0; behave = 0; animate = 0; xp = 0; yp = 0; ax = 0; ay = 0; vx = 0; vy = 0; w = 16; h = 16; cx = 0; cy = 0; newxp = 0; newyp = 0; x1 = 0; y1 = 0; x2 = 320; y2 = 240; jumping = false; gravity = false; onground = 0; onroof = 0; jumpframe = 0; onentity = 0; harmful = false; onwall = 0; onxwall = 0; onywall = 0; isplatform = false; framedelay = 0; drawframe = 0; walkingframe = 0; dir = 0; actionframe = 0; } 

Assez de champs, non? Il n'est pas surprenant qu'une erreur se cache ici, à laquelle PVS-Studio a émis un avertissement:

V730 Il est possible que tous les membres d'une classe ne soient pas initialisés à l'intérieur du constructeur. Pensez à inspecter: oldxp, oldyp. Ent.cpp 3

Comme vous pouvez le voir, dans une liste si grande, l'initialisation de deux champs de la classe a été perdue. Ainsi, leurs valeurs sont restées indéfinies, à cause de quoi, ailleurs dans le programme, une valeur inconnue peut être lue et utilisée à partir d'eux. Il est très difficile de détecter une telle erreur à travers les yeux.

Figure 2



Avertissement 6


Considérez le code:

 void mapclass::loadlevel(....) { .... std::vector<std::string> tmap; .... tmap = otherlevel.loadlevel(rx, ry, game, obj); fillcontent(tmap); .... //  tmap    . } 

Avertissement PVS-studio: V688 La variable locale 'tmap' possède le même nom que l'un des membres de la classe, ce qui peut entraîner une confusion. Map.cpp 1192

En effet, si vous regardez à l'intérieur de la classe mapclass , vous pouvez y trouver le même vecteur avec le même nom:

 class mapclass { public: .... std::vector <int> roomdeaths; std::vector <int> roomdeathsfinal; std::vector <int> areamap; std::vector <int> contents; std::vector <int> explored; std::vector <int> vmult; std::vector <std::string> tmap; // <= .... }; 

Malheureusement, déclarer un vecteur du même nom dans une fonction rend le vecteur déclaré dans la classe invisible. Il s'avère que dans toute la fonction de niveau de charge, le vecteur tmap ne change qu'à l'intérieur de la fonction. Le vecteur déclaré dans la classe reste inchangé!

Fait intéressant, PVS-Studio a découvert jusqu'à 20 fragments de code de ce type! Pour la plupart, ils sont associés à des variables temporaires, qui, par commodité, ont été déclarées membres de la classe. L'auteur du jeu (et son seul développeur) a lui-même écrit qu'il avait cette mauvaise habitude. Vous pouvez lire à ce sujet dans un article auquel j'ai lié au début de cet article.

Il note également qu'une telle dénomination a conduit à des bogues nuisibles et difficiles à attraper. Eh bien, de tels bugs peuvent vraiment être dangereux, mais utiliser une analyse statique pour les détecter ne sera pas difficile :)

Avertissement 7


V601 Le type entier est implicitement converti en type char. Game.cpp 4997

 void Game::loadquick(....) { .... else if (pKey == "totalflips") { totalflips = atoi(pText); } else if (pKey == "hardestroom") { hardestroom = atoi(pText); // <= } else if (pKey == "hardestroomdeaths") { hardestroomdeaths = atoi(pText); } .... } 

Pour comprendre quel est le problème, regardons les définitions des variables de la section de code donnée:

 //Some stats: int totalflips; std::string hardestroom; int hardestroomdeaths; 

Les variables totalflips et hardestroomdeaths sont de type entier, et donc leur attribuer le résultat à la fonction atoi est tout à fait normal. Mais que se passe-t-il si vous affectez une valeur entière à std :: string ? Il s'avère que du point de vue de la langue, une telle mission est tout à fait valable. En conséquence, une valeur incompréhensible sera écrite dans la variable hardestroom !

Avertissement 8


V1004 Le pointeur 'pElem' a été utilisé de manière non sécurisée après avoir été vérifié par rapport à nullptr. Vérifiez les lignes: 1739, 1744. editor.cpp 1744

 void editorclass::load(std::string &_path) { .... TiXmlHandle hDoc(&doc); TiXmlElement *pElem; TiXmlHandle hRoot(0); version = 0; { pElem = hDoc.FirstChildElement().Element(); // should always have a valid root // but handle gracefully if it does if (!pElem) { printf("No valid root! Corrupt level file?\n"); } pElem->QueryIntAttribute("version", &version); // <= // save this for later hRoot = TiXmlHandle(pElem); } .... } 

L'analyseur avertit que le pointeur pElem est utilisé de manière non sécurisée immédiatement après avoir été vérifié pour nullptr . Pour vous assurer que l'analyseur a raison, jetez un œil à la définition de la fonction Element () , dont la valeur de retour initialise le pointeur pElem :

 /** @deprecated use ToElement. Return the handle as a TiXmlElement. This may return null. */ TiXmlElement *Element() const { return ToElement(); } 

Comme vous pouvez le voir dans le commentaire, cette fonction peut retourner null .

Imaginez maintenant que cela s'est vraiment produit. Que se passera-t-il dans ce cas? Le fait est qu'une telle situation ne sera en aucun cas gérée. Oui, un message s'affiche indiquant que quelque chose s'est mal passé, mais littéralement une ligne sous le pointeur incorrect sera déréférencée. Un tel déréférencement, à son tour, entraînera soit un plantage du programme, soit un comportement indéfini. C'est une erreur assez grave.

Avertissement 9


Dans la section suivante du code, PVS-Studio a émis quatre avertissements:
  • V560 Une partie de l'expression conditionnelle est toujours vraie: x> = 0. editor.cpp 1137
  • V560 Une partie de l'expression conditionnelle est toujours vraie: y> = 0. editor.cpp 1137
  • V560 Une partie de l'expression conditionnelle est toujours vraie: x <40. editor.cpp 1137
  • V560 Une partie de l'expression conditionnelle est toujours vraie: y <30. editor.cpp 1137

 int editorclass::at( int x, int y ) { if(x<0) return at(0,y); if(y<0) return at(x,0); if(x>=40) return at(39,y); if(y>=30) return at(x,29); if(x>=0 && y>=0 && x<40 && y<30) { return contents[x+(levx*40)+vmult[y+(levy*30)]]; } return 0; } 

Tous les avertissements s'appliquent à la dernière instruction if . Le problème est que les quatre vérifications qui y sont effectuées renverront toujours true . Je ne dirais pas que c'est une grave erreur, mais cela s'est avéré assez drôle. L'auteur a décidé de prendre cette fonction au sérieux et, au cas où, a de nouveau vérifié chaque variable :)

Cette vérification peut être supprimée, car le thread d'exécution n'atteindra jamais l'expression " return 0; " de toute façon. Bien que cela ne change pas la logique du programme, cela le libérera des contrôles inutiles et du code mort.

Avertissement 10


Dans son article sur l'anniversaire du jeu, Terry dit avec ironie que l'un des éléments qui contrôlent la logique du jeu était un énorme changement de la fonction Game :: updatestate () , qui est immédiatement responsable d'un grand nombre d'états de jeu différents. Et il était assez attendu que je trouve l'avertissement suivant:

V2008 Complexité cyclomatique: 548. Envisagez de refactoriser la fonction 'Game :: updatestate'. Game.cpp 612

Oui, vous avez bien compris: PVS-Studio a estimé la complexité cyclomatique d'une fonction à 548 unités. Cinq cent quarante huit !!! Je comprends cela - "code soigné". Et cela malgré le fait qu'en fait, il n'y a rien de plus qu'une expression de commutateur dans une fonction. Dans le commutateur lui-même, j'ai compté plus de 300 expressions de cas.

Dans notre bureau, il y a une petite compétition entre les auteurs pour l'article le plus long. J'apporterais volontiers tout le code de fonction ici (3450 lignes), mais une telle victoire serait malhonnête, donc je me limiterai à me référer simplement à l'énorme commutateur. Je recommande de le suivre et d'évaluer toute l'échelle vous-même! Soit dit en passant, en plus de Game :: updatestate () , PVS-Studio a trouvé jusqu'à 44 fonctions avec une complexité cyclomatique excessive, dont 10 ont une complexité de plus de 200.

Figure 3



Conclusion


Je pense que les erreurs écrites suffisent pour l'article. Oui, il y a eu beaucoup d'erreurs dans le projet, mais c'est exactement l'astuce: après avoir présenté son code, Terry Cavanagh a montré qu'il n'était pas nécessaire d'être un bon programmeur pour faire un bon jeu. Maintenant, après 10 ans, Terry se souvient de ces moments avec ironie. Il est important d'apprendre de vos erreurs et la pratique est la meilleure façon de le faire. Et si votre pratique peut encore donner naissance à un jeu comme VVVVVVV, alors c'est généralement magnifique! Ehh ... je vais y aller et je vais probablement y jouer à nouveau :)

Ce ne sont pas toutes les erreurs trouvées dans le code du jeu. Si vous voulez voir par vous-même ce que vous pouvez trouver d'autre, je vous suggère de télécharger et d'essayer PVS-Studio ! N'oubliez pas non plus que pour les projets open source, nous fournissons une licence gratuite.



Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: George Gribkov. VVVVVV ??? VVVVVV !!! :) .

Source: https://habr.com/ru/post/fr484166/


All Articles