Si est谩s leyendo este texto, has pensado que algo andaba mal con el titular o has visto el nombre de un juego de computadora familiar. VVVVVV es un juego de plataformas independiente que ha robado los corazones de muchos jugadores por su simplicidad externa agradable y complejidad interna no menos agradable. Hace unos d铆as, VVVVVV cumpli贸 10 a帽os, y el autor del juego, Terry Cavanagh, celebr贸 estas vacaciones publicando su c贸digo fuente. 驴Qu茅 cosas alucinantes esconde? Lee la respuesta en este art铆culo.
Introduccion
Oh, VVVVVV ... Recuerdo haberlo encontrado poco despu茅s del lanzamiento y ser un gran fan谩tico de los juegos retro de p铆xeles, estaba tan emocionado de instalarlo en mi computadora. Recuerdo mis primeras impresiones: "驴Eso es todo? 驴Solo corriendo por las habitaciones cuadradas? Pens茅 despu茅s de unos minutos de jugar. No sab铆a lo que me esperaba en ese momento. Tan pronto como sal铆 de la ubicaci贸n inicial, me encontr茅 en un mundo bidimensional peque帽o pero confuso y florido lleno de paisajes inusuales y artefactos de p铆xeles desconocidos para m铆.
Me dej茅 llevar por el juego. Eventualmente, venc铆 por completo el juego a pesar de algunos desaf铆os, como la alta complejidad con el control del juego aplicado h谩bilmente, por ejemplo: el personaje principal no puede saltar, pero puede invertir la direcci贸n del vector de gravedad sobre s铆 mismo. No tengo idea de cu谩ntas veces muri贸 mi personaje, pero estoy seguro de que la cantidad de muertes se mide en decenas de cientos. Despu茅s de todo, cada juego tiene su propio entusiasmo 煤nico :)
De todos modos, volvamos al c贸digo fuente, publicado
en honor del aniversario del juego .
Por el momento, soy desarrollador del PVS-Studio, que es un analizador de c贸digo est谩tico para C, C ++, C # y Java. Adem谩s de desarrollar directamente, tambi茅n estamos comprometidos con nuestra promoci贸n de productos. Para nosotros, una de las mejores formas de hacerlo es escribir art铆culos sobre la verificaci贸n de proyectos de c贸digo abierto. Nuestros lectores obtienen art铆culos interesantes sobre temas de programaci贸n, y tenemos la oportunidad de demostrar las capacidades de PVS-Studio. Entonces, cuando escuch茅 sobre la apertura del c贸digo fuente VVVVVV, simplemente no pude pasarlo.
En este art铆culo, veremos algunos errores interesantes encontrados por el analizador PVS-Studio en el c贸digo VVVVVV, y veremos en detalle estos errores. Apunte el vector de gravedad hacia abajo y p贸ngase c贸modo: 隆estamos a punto de comenzar!
Descripci贸n general de las advertencias del analizador
Advertencia 1
V512 Una llamada de la funci贸n 'sprintf' provocar谩 el desbordamiento del b煤fer '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]; .... strcpy(oldDirectory, output); sprintf(fileSearch, "%s\\*.vvvvvv", oldDirectory); .... }
Como puede ver, las cadenas
fileSearch y
oldDirectory son del mismo tama帽o: 260 caracteres. Despu茅s de escribir el contenido de la cadena
oldDirectory en la cadena de formato (el tercer argumento
sprintf ), se ver谩 as铆:
<i>contents_oldDirectory\*.vvvvvv</i>
Esta l铆nea es 9 caracteres m谩s larga que el valor original de
oldDirectory . Es esta secuencia de caracteres que se escribir谩 en
fileSearch . 驴Qu茅 sucede si la longitud de la cadena
oldDirectory es superior a 251? La cadena resultante ser谩 m谩s larga de lo que podr铆a contener
fileSearch , lo que conducir谩 a violar los l铆mites de la matriz. Los datos en la RAM pueden da帽arse y el resultado al que dar谩 lugar es una cuesti贸n ret贸rica :)
Advertencia 2
V519 La variable 'fondo' tiene valores asignados dos veces seguidas. Quiz谩s esto sea un error. L铆neas de verificaci贸n: 1367, 1373. Map.cpp 1373
void mapclass::loadlevel(....) { .... case 4:
A la misma variable se le asigna un valor dos veces seguidas. Sin embargo, esta variable no se usa en ning煤n lugar entre asignaciones. Lo cual es extra帽o ... Esta secuencia puede no violar la l贸gica del programa, pero tales asignaciones en s铆 mismas indican cierta confusi贸n al escribir c贸digo. Si esto es un error de hecho, solo el autor podr谩 decirlo con certeza. Aunque hay ejemplos m谩s v铆vidos de este error en el c贸digo:
void Game::loadquick(....) { .... else if (pKey == "frames") { frames = atoi(pText); frames = 0; } .... }
En este caso, est谩 claro que un error se esconde en alg煤n lugar, ya sea en l贸gica o en una asignaci贸n redundante. Tal vez, la segunda l铆nea fue escrita temporalmente para la depuraci贸n, y luego qued贸 olvidada. En total, PVS-Studio emiti贸 8 advertencias sobre tales casos.
Advertencia 3
Se cre贸 el objeto V808 'pKey' del tipo 'basic_string' pero no se utiliz贸. 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());
Este c贸digo es muy extra帽o. El analizador advierte sobre la variable creada pero no utilizada
pKey , pero en realidad, el problema era m谩s interesante. Destaqu茅 intencionalmente que la l铆nea activ贸 la advertencia con una flecha, ya que esta funci贸n contiene m谩s de una definici贸n de cadena con el nombre
pKey . As铆 es, otra de esas variables se declara dentro del ciclo
for . Se superpone al que se declara fuera del bucle.
Por lo tanto, si hace referencia al valor de la cadena
pKey fuera del bucle
for , obtendr谩 el valor igual a
pElem-> Value () , pero al hacer lo mismo dentro del bucle, obtendr谩 el valor igual a
edEntityEl -> Valor () . La superposici贸n de nombres es un error bastante aproximado, que puede ser muy dif铆cil de encontrar por su cuenta durante la revisi贸n del c贸digo.
Advertencia 4
V805 Disminuci贸n del rendimiento. Es ineficiente identificar una cadena vac铆a usando la construcci贸n 'strlen (str)> 0'. Una forma m谩s eficiente es verificar: 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; }
El analizador encontr贸 un fragmento para una potencial micro optimizaci贸n. Utiliza la funci贸n
strlen para verificar si la cadena est谩 vac铆a. Esta funci贸n atraviesa todos los elementos de cadena y comprueba cada uno de ellos para un terminador nulo ('\ 0'). Si obtenemos una cadena larga, cada car谩cter se comparar谩 con un terminador nulo.
隆Pero solo necesitamos verificar que la cadena est茅 vac铆a! Todo lo que necesita hacer es averiguar si el primer car谩cter de cadena es un terminal nulo. Por lo tanto, para optimizar esta verificaci贸n dentro de la afirmaci贸n, vale la pena escribir:
str[0] != '\0'
Esa es la recomendaci贸n que nos da el analizador. Claro, la llamada de la funci贸n strlen est谩 en condici贸n de la macro de aserci贸n, por lo tanto, solo se ejecutar谩 en la versi贸n de depuraci贸n, donde la velocidad no es tan importante. En la versi贸n de lanzamiento, la llamada de la funci贸n y el c贸digo se ejecutar谩n r谩pidamente. A pesar de esto, quer铆a demostrar lo que nuestro analizador puede sugerir en t茅rminos de micro optimizaciones.
Advertencia 5
Para demostrar la esencia de otro error, tengo que citar dos fragmentos de c贸digo aqu铆: la declaraci贸n de clase
entclass y su constructor. Comencemos con la declaraci贸n:
class entclass { public: entclass(); void clear(); bool outside(); public:
Este constructor de clase tiene el siguiente aspecto:
entclass::entclass() { clear(); } void entclass::clear() {
Muchos campos, 驴no dir铆as? No es de extra帽ar, PVS-Studio emiti贸 una advertencia por un error, escondi茅ndose aqu铆:
V730 Es posible que no todos los miembros de una clase se inicialicen dentro del constructor. Considere inspeccionar: oldxp, oldyp. Ent.cpp 3
Como puede ver, las inicializaciones de dos campos de clase se perdieron en una lista tan larga. Como resultado, sus valores permanecieron indefinidos, por lo que pueden leerse incorrectamente y usarse en otro lugar del programa. Es muy dif铆cil detectar tal error simplemente revisando.
Advertencia 6
Mira este c贸digo:
void mapclass::loadlevel(....) { .... std::vector<std::string> tmap; .... tmap = otherlevel.loadlevel(rx, ry, game, obj); fillcontent(tmap); ....
Advertencia de PVS-Studio:
V688 La variable local 'tmap' posee el mismo nombre que uno de los miembros de la clase, lo que puede generar confusi贸n. Map.cpp 1192
De hecho, mirando dentro de la clase
mapclass , puede encontrar el mismo vector con el mismo nombre all铆:
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;
Desafortunadamente, la misma declaraci贸n de vector de nombre dentro de la funci贸n hace que el vector declarado en la clase sea invisible. Resulta que el vector
tmap se cambia solo dentro de la funci贸n de nivel de
carga . 隆El vector declarado en la clase sigue siendo el mismo!
Curiosamente, PVS-Studio ha encontrado 20 de esos fragmentos de c贸digo. En su mayor parte, se relacionan con variables temporales que se han declarado "por conveniencia" como miembros de la clase. El autor del juego (y su 煤nico desarrollador) escribi贸 sobre s铆 mismo que sol铆a tener este mal h谩bito. Puede leer sobre esto en la publicaci贸n: el enlace se encuentra al comienzo del art铆culo.
Tambi茅n se帽al贸 que tales nombres condujeron a errores da帽inos que eran dif铆ciles de detectar. Bueno, tales errores pueden ser realmente destructivos, pero atraparlos se vuelve menos dif铆cil si usa an谩lisis est谩tico :)
Advertencia 7
V601 El tipo entero se
convierte impl铆citamente en el tipo char. Game.cpp 4997
void Game::loadquick(....) { .... else if (pKey == "totalflips") { totalflips = atoi(pText); } else if (pKey == "hardestroom") { hardestroom = atoi(pText);
Para comprender lo que est谩 sucediendo, echemos un vistazo a las definiciones de las variables de la parte del c贸digo dada:
Las variables
totalflips y
hardestroomdeaths son enteras, por lo que es perfectamente normal asignarles el resultado de la funci贸n
atoi . Pero, 驴qu茅 sucede si asigna un valor entero a
std :: string ? Tal asignaci贸n resulta ser v谩lida desde la perspectiva del lenguaje. Como resultado, 隆se escribir谩 un valor poco claro en la variable
hardestroom !
Advertencia 8
V1004 El puntero 'pElem' se us贸 de forma insegura despu茅s de que se verific贸 contra nullptr. L铆neas de verificaci贸n: 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();
El analizador advierte que el puntero
pElem se usa de forma insegura justo despu茅s de verificar
nullptr . Para asegurarnos de que el analizador es correcto,
revisemos la definici贸n de la funci贸n
Element () que devuelve el valor que, a su vez, inicializa el
puntero pElem:
TiXmlElement *Element() const { return ToElement(); }
Como podemos ver en el comentario, esta funci贸n podr铆a devolver
nulo .
Ahora imagine que realmente sucedi贸. 驴Qu茅 pasar谩 en este caso? El hecho es que esta situaci贸n no se manejar谩 de ninguna manera. S铆, habr谩 un mensaje de que algo sali贸 mal, pero el puntero incorrecto se desreferenciar谩 solo una l铆nea debajo. Dicha desreferencia dar谩 como resultado el bloqueo del programa o un comportamiento indefinido. Este es un error bastante grave.
Advertencia 9
Este fragmento de c贸digo activ贸 cuatro advertencias del analizador PVS-Studio:
- V560 Una parte de la expresi贸n condicional siempre es verdadera: x> = 0. editor.cpp 1137
- V560 Una parte de la expresi贸n condicional siempre es verdadera: y> = 0. editor.cpp 1137
- V560 Una parte de la expresi贸n condicional siempre es verdadera: x <40. Editor.cpp 1137
- V560 Una parte de la expresi贸n condicional siempre es verdadera: 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; }
Todas las advertencias se refieren a la 煤ltima declaraci贸n
if . El problema es que las cuatro comprobaciones, realizadas en 茅l, siempre devolver谩n
verdadero . No dir铆a que es un error grave, pero es bastante divertido. El autor decidi贸 tomar esta funci贸n en serio y, por si acaso, volvi贸 a verificar cada variable :)
Podr铆a haber eliminado esta verificaci贸n, ya que el flujo de ejecuci贸n no llegar谩 a la expresi贸n "
return 0; " de todos modos. No cambiar谩 la l贸gica del programa, pero ayudar谩 a eliminar las verificaciones redundantes y el c贸digo muerto.
Advertencia 10
En su art铆culo sobre el aniversario del juego, Terry not贸 ir贸nicamente que uno de los elementos que controlaba la l贸gica del juego era el gran cambio de la funci贸n
Game :: updatestate () , responsable de una gran cantidad de estados diferentes del juego al mismo tiempo. . Y era bastante esperado que encontrara la siguiente advertencia:
V2008 Complejidad ciclom谩tica: 548. Considere refactorizar la funci贸n 'Game :: updatestate'. Game.cpp 612
S铆, lo entendiste bien: PVS-Studio le dio a la funci贸n la siguiente calificaci贸n de complejidad: 548. 隆Quinientos cuarenta y ocho! As铆 es como se ve el "c贸digo ordenado". Y esto a pesar del hecho de que, a excepci贸n de la instrucci贸n switch, no hay casi nada m谩s en la funci贸n. En el cambio en s铆, cont茅 m谩s de 300 expresiones de caso.
Ya sabes, en nuestra empresa tenemos una peque帽a competencia por el art铆culo m谩s largo. Me encantar铆a traer el c贸digo de funci贸n completo (3,450 l铆neas) aqu铆, pero tal victoria ser铆a injusta, por lo que me limitar茅 al
enlace al interruptor gigante. 隆Recomiendo que sigas el enlace y veas su longitud por ti mismo! Para el caso, adem谩s de
Game :: updatestate () , PVS-Studio tambi茅n ha encontrado 44 funciones con complejidad ciclom谩tica inflada, 10 de las cuales ten铆an un n煤mero de complejidad de m谩s de 200.
Conclusi贸n
Creo que los errores anteriores son suficientes para este art铆culo. S铆, hubo muchos errores en el proyecto, pero es una especie de caracter铆stica. Al abrir su c贸digo, Terry Cavanagh demostr贸 que no tienes que ser un programador perfecto para escribir un gran juego. Ahora, 10 a帽os despu茅s, Terry recuerda esos momentos con iron铆a. Es importante aprender de sus errores, y practicar es la mejor manera de hacerlo. Y si tu pr谩ctica puede dar lugar a un juego como VVVVVV, 隆es simplemente magn铆fico! Bueno ... ya es hora de jugarlo una vez m谩s :)
Estos no fueron todos los errores encontrados en el c贸digo del juego. Si desea ver por s铆 mismo qu茅 m谩s se puede encontrar, le sugiero que
descargue y pruebe PVS-Studio . Adem谩s, no olvide que
ofrecemos proyectos de c贸digo abierto con licencias gratuitas.