E novamente no espaço: como o unicórnio Stellarium visitou

Durante todo o período de sua existência, as pessoas fizeram um esforço tremendo para estudar quase toda a área do céu estrelado. Até o momento, examinamos centenas de milhares de asteróides, cometas, nebulosas e estrelas, galáxias e planetas. Para ver toda essa beleza, não é necessário sair de casa e comprar um telescópio. Você pode instalar o Stellarium - um planetário virtual no seu computador e olhar para o céu noturno, confortavelmente deitado no sofá ... Mas é confortável? Para descobrir a resposta a esta pergunta, verificaremos o Stellarium quanto a erros no código do computador.



Um pouco sobre o projeto ...


De acordo com a descrição no site da Wikipedia, o Stellarium é um planetário virtual de código aberto disponível para Linux, Mac OS X, Microsoft Windows, Symbian, Android e iOS, além do MeeGo. O programa usa as tecnologias OpenGL e Qt para criar um céu realista em tempo real. Com o Stellarium, você pode ver o que pode ver com um telescópio médio e até grande. O programa também fornece observações de eclipses solares e o movimento de cometas.

O Stellarium foi criado pelo programador francês Fabian Chereau, que lançou o projeto no verão de 2001. Outros desenvolvedores de destaque incluem Robert Spearman, Johannes Gadzhozik, Matthew Gates, Timothy Reeves, Bogdan Marinov e Johan Meeris, responsável pela obra de arte.

... e sobre o analisador


A análise do projeto foi realizada no analisador de código estático PVS-Studio. Esta é uma ferramenta para detectar erros e possíveis vulnerabilidades no código fonte dos programas escritos em C, C ++ e C # (em breve em Java!). É executado no Windows, Linux e macOS. Ele foi desenvolvido para aqueles que precisam melhorar a qualidade de seu código.

A análise foi bastante simples. Primeiro, baixei o projeto Stellarium do GitHub e instalei todos os pacotes necessários para a montagem. Como o projeto foi criado usando o Qt Creator, usei o sistema de rastreamento de inicialização do compilador, incorporado à versão autônoma do analisador. Lá, você pode visualizar o relatório de análise concluído.

Novos leitores e usuários do Stellarium podem ter se perguntado: por que o unicórnio aparece no título do artigo e como ele está relacionado à análise de código? Eu respondo: sou um dos desenvolvedores do PVS-Studio, e o unicórnio é nosso mascote travesso favorito. Então suba!


Espero que, graças a este artigo, os leitores aprendam algo novo por si mesmos, e os desenvolvedores do Stellarium possam eliminar alguns erros e melhorar a qualidade do código.

Traga seu café com um croissant de ar e sinta-se confortável, porque estamos indo para a parte mais interessante - uma visão geral dos resultados da análise e análise de erros!

Condições suspeitas


Para maior prazer de leitura, sugiro que não olhe diretamente para o aviso do analisador, mas tente aqui e mais adiante encontrar você mesmo os erros.

void QZipReaderPrivate::scanFiles() { .... // find EndOfDirectory header int i = 0; int start_of_directory = -1; EndOfDirectory eod; while (start_of_directory == -1) { const int pos = device->size() - int(sizeof(EndOfDirectory)) - i; if (pos < 0 || i > 65535) { qWarning() << "QZip: EndOfDirectory not found"; return; } device->seek(pos); device->read((char *)&eod, sizeof(EndOfDirectory)); if (readUInt(eod.signature) == 0x06054b50) break; ++i; } .... } 

Aviso do PVS-Studio: V654 A condição 'start_of_directory == - 1' do loop é sempre verdadeira. qzip.cpp 617

Você conseguiu encontrar um erro? Se sim, então elogie.

O erro está na condição do loop while . É sempre verdade, pois a variável start_of_directory não muda no corpo do loop. Muito provavelmente, o ciclo não será eterno, pois contém retorno e quebra , mas esse código parece estranho.

Parece-me que no código eles esqueceram de fazer a atribuição start_of_directory = pos no local em que a assinatura está sendo verificada. Então a declaração de quebra é talvez supérflua. Nesse caso, o código pode ser reescrito assim:

 int i = 0; int start_of_directory = -1; EndOfDirectory eod; while (start_of_directory == -1) { const int pos = device->size() - int(sizeof(EndOfDirectory)) - i; if (pos < 0 || i > 65535) { qWarning() << "QZip: EndOfDirectory not found"; return; } device->seek(pos); device->read((char *)&eod, sizeof(EndOfDirectory)); if (readUInt(eod.signature) == 0x06054b50) start_of_directory = pos; ++i; } 

No entanto, não tenho certeza de como deve ser o código. É melhor que os próprios desenvolvedores do projeto analisem essa parte do programa e façam as alterações necessárias.

Outra condição estranha:

 class StelProjectorCylinder : public StelProjector { public: .... protected: .... virtual bool intersectViewportDiscontinuityInternal(const Vec3d& capN, double capD) const { static const SphericalCap cap1(1,0,0); static const SphericalCap cap2(-1,0,0); static const SphericalCap cap3(0,0,-1); SphericalCap cap(capN, capD); return cap.intersects(cap1) && cap.intersects(cap2) && cap.intersects(cap2); } }; 

PVS-Studio Warning: V501 Existem sub-expressões idênticas 'cap.intersects (cap2)' à esquerda e à direita do operador '&&'. StelProjectorClasses.hpp 175

Como você provavelmente já adivinhou, o erro está na última linha da função: o programador cometeu um erro de digitação e, no final, descobriu que a função retorna o resultado independentemente do valor de cap3 .

Esse tipo de erro é extremamente comum: em quase todos os projetos testados, encontramos erros de digitação relacionados a nomes do formulário nome1 e nome2 e similares. Normalmente, esses erros estão relacionados à copiar e colar.

Essa instância de código é um excelente exemplo de outro padrão de erro comum, para o qual realizamos um mini-estudo separado. Meu colega Andrei Karpov chamou o " efeito de última linha ". Se você não estiver familiarizado com este material, sugiro abrir uma guia no navegador para ler mais tarde, mas por enquanto, vamos continuar.

 void BottomStelBar::updateText(bool updatePos) { .... updatePos = true; .... if (location->text() != newLocation || updatePos) { updatePos = true; .... } .... if (fov->text() != str) { updatePos = true; .... } .... if (fps->text() != str) { updatePos = true; .... } if (updatePos) { .... } } 

Avisos do PVS-Studio:

  • V560 Uma parte da expressão condicional é sempre verdadeira: updatePos. StelGuiItems.cpp 732
  • A expressão V547 'updatePos' sempre é verdadeira. StelGuiItems.cpp 831
  • V763 O parâmetro 'updatePos' é sempre reescrito no corpo da função antes de ser usado. StelGuiItems.cpp 690

O valor do parâmetro updatePos é sempre substituído antes de ser usado, ou seja, a função funcionará da mesma forma, independentemente do valor passado para ela.

Parece estranho, não é? Em todos os lugares em que o parâmetro updatePos está envolvido , é verdade . Isso significa que as condições if (location-> text ()! = NewLocation || updatePos) e if (updatePos) sempre serão verdadeiras.

Outro trecho:

 void LandscapeMgr::onTargetLocationChanged(StelLocation loc) { .... if (pl && flagEnvironmentAutoEnabling) { QSettings* conf = StelApp::getInstance().getSettings(); setFlagAtmosphere(pl->hasAtmosphere() & conf->value("landscape/flag_atmosphere", true).toBool()); setFlagFog(pl->hasAtmosphere() & conf->value("landscape/flag_fog", true).toBool()); setFlagLandscape(true); } .... } 

Avisos do PVS-Studio:

  • V792 A função 'toBool' localizada à direita do operador '&' será chamada independentemente do valor do operando esquerdo. Talvez seja melhor usar '&&'. LandscapeMgr.cpp 782
  • V792 A função 'toBool' localizada à direita do operador '&' será chamada independentemente do valor do operando esquerdo. Talvez seja melhor usar '&&'. LandscapeMgr.cpp 783

O analisador detectou uma expressão suspeita nos argumentos para as funções setFlagAtmosphere e setFlagFog . De fato: nos dois lados do operador de bit e existem valores do tipo bool . Em vez do operador & , você deve usar o operador && , e agora vou explicar o porquê.

Sim, o resultado dessa expressão sempre estará correto. Antes de usar o “e” bit a bit, os dois operandos serão promovidos para int . No C ++, essa conversão é inequívoca : false é convertido em 0 e true é convertido em 1. Portanto, o resultado dessa expressão será o mesmo que se o operador && fosse usado.

Mas há uma nuance. Ao calcular o resultado da operação && , é utilizado o chamado "cálculo lento". Se o valor do operando esquerdo for falso , o valor correto nem será calculado, porque o "e" lógico, em qualquer caso, retornará falso . Isso é feito para economizar recursos de computação e permite que você escreva projetos mais complexos. Por exemplo, você pode verificar se o ponteiro não é nulo e, se houver, desreferenciá-lo para executar uma verificação adicional. Exemplo: if (ptr && ptr-> foo ()) .

Esse “cálculo preguiçoso” não é realizado usando o operador bit a bit & : expression conf-> value ("...", true) .toBool () será avaliado sempre, independentemente do valor pl-> hasAtmosphere () .

Em casos raros, isso é feito de propósito. Por exemplo, se a computação do operando certo tiver "efeitos colaterais", o resultado será usado posteriormente. Também não é muito bom fazê-lo, porque complica a compreensão do código e a manutenção dele. Além disso, a ordem de cálculo dos operandos e não está definida, portanto, em alguns casos, usando esses "truques", você pode obter um comportamento indefinido.

Se você precisar salvar efeitos colaterais - faça-o em uma linha separada e salve o resultado em uma variável separada. As pessoas que trabalharão com esse código no futuro serão gratas a você :)


Prosseguimos para o próximo tópico.

Manuseio incorreto de memória


Vamos começar o tópico da memória dinâmica com este fragmento:

 /************ Basic Edge Operations ****************/ /* __gl_meshMakeEdge creates one edge, * two vertices, and a loop (face). * The loop consists of the two new half-edges. */ GLUEShalfEdge* __gl_meshMakeEdge(GLUESmesh* mesh) { GLUESvertex* newVertex1 = allocVertex(); GLUESvertex* newVertex2 = allocVertex(); GLUESface* newFace = allocFace(); GLUEShalfEdge* e; /* if any one is null then all get freed */ if ( newVertex1 == NULL || newVertex2 == NULL || newFace == NULL) { if (newVertex1 != NULL) { memFree(newVertex1); } if (newVertex2 != NULL) { memFree(newVertex2); } if (newFace != NULL) { memFree(newFace); } return NULL; } e = MakeEdge(&mesh->eHead); if (e == NULL) { return NULL; } MakeVertex(newVertex1, e, &mesh->vHead); MakeVertex(newVertex2, e->Sym, &mesh->vHead); MakeFace(newFace, e, &mesh->fHead); return e; } 

Avisos do PVS-Studio:

  • V773 A função foi encerrada sem liberar o ponteiro 'newVertex1'. É possível um vazamento de memória. mesh.c 312
  • V773 A função foi encerrada sem liberar o ponteiro 'newVertex2'. É possível um vazamento de memória. mesh.c 312
  • V773 A função foi encerrada sem liberar o ponteiro 'newFace'. É possível um vazamento de memória. mesh.c 312

A função aloca memória para várias estruturas e a passa para os ponteiros newVertex1 , newVertex2 (nomes interessantes, certo?) E newFace . Se um deles for zero, toda a memória reservada dentro da função será liberada, após o que o fluxo de controle sairá da função.

O que acontece se a memória das três estruturas for alocada corretamente e a função MakeEdge (& mesh-> eHead) retornar NULL ? O fluxo de controle alcançará o segundo retorno .

Como os ponteiros newVertex1 , newVertex2 e newFace são variáveis ​​locais, eles deixarão de existir após a saída da função. Mas a liberação da memória que lhes pertencia não acontecerá. Ele permanecerá reservado, mas não teremos mais acesso a ele.

Tais situações são chamadas de vazamento de memória. Um cenário típico com esse erro: com o uso prolongado do programa, ele começa a consumir mais e mais RAM, até o esgotamento.

Deve-se notar que, neste exemplo, o terceiro retorno não é errado. As funções MakeVertex e MakeFace transferem os endereços da memória alocada para outras estruturas de dados, delegando assim a responsabilidade por sua liberação.

A próxima falha está no método, que leva 90 linhas. Por conveniência, reduzi, deixando apenas áreas problemáticas.

 void AstroCalcDialog::drawAngularDistanceGraph() { .... QVector<double> xs, ys; .... } 

Apenas uma linha restante. Deixe-me dar uma dica: esta é a única menção aos objetos xs e ys .

Avisos do PVS-Studio:

  • O objeto V808 'xs' do tipo 'QVector' foi criado, mas não foi utilizado. AstroCalcDialog.cpp 5329
  • O objeto V808 'ys' do tipo 'QVector' foi criado, mas não foi utilizado. AstroCalcDialog.cpp 5329

Os vetores xs e ys são criados, mas não são usados ​​em nenhum lugar. Acontece que toda vez que você usa o método drawAngularDistanceGraph , ocorre uma criação e exclusão extras de um contêiner vazio. Acho que esse anúncio permaneceu no código após a refatoração. Obviamente, isso não é um erro, mas você deve remover o código extra.

Moldes de tipo estranho


Outro exemplo depois de um pouco de formatação é assim:

 void SatellitesDialog::updateSatelliteData() { .... // set default buttonColor = QColor(0.4, 0.4, 0.4); .... } 

Para entender qual é o erro, você precisará examinar os protótipos dos construtores da classe Qcolor :


Avisos do PVS-Studio:

  • V674 O literal '0.4' do tipo 'double' está sendo implicitamente convertido para o tipo 'int' enquanto chama a função 'QColor'. Inspecione o primeiro argumento. SatellitesDialog.cpp 413
  • V674 O literal '0.4' do tipo 'double' está sendo implicitamente convertido para o tipo 'int' enquanto chama a função 'QColor'. Inspecione o segundo argumento. SatellitesDialog.cpp 413
  • V674 O literal '0.4' do tipo 'double' está sendo implicitamente convertido para o tipo 'int' enquanto chama a função 'QColor'. Inspecione o terceiro argumento. SatellitesDialog.cpp 413

A classe Qcolor não possui construtores que aceitam o tipo double ; portanto, os argumentos no exemplo serão implicitamente convertidos em int . Isso faz com que os campos r , g , b do objeto buttonColor tenham um valor 0 .

Se o programador pretendia criar um objeto a partir de valores do tipo double , ele deveria usar um construtor diferente.

Por exemplo, você pode usar um construtor que aceite Qrgb escrevendo:

 buttonColor = QColor(QColor::fromRgbF(0.4, 0.4, 0.4)); 

Poderia ter sido feito de maneira diferente. Qt usa valores reais no intervalo [0,0, 1,0] ou valores inteiros no intervalo [0, 255] para indicar cores RGB.

Portanto, o programador pode traduzir os valores de real para inteiro escrevendo assim:

 buttonColor = QColor((int)(255 * 0.4), (int)(255 * 0.4), (int)(255 * 0.4)); 

ou apenas

 buttonColor = QColor(102, 102, 102); 

Você está entediado? Não se preocupe: há erros mais interessantes pela frente.


"Unicórnio no espaço". Vista do Stellarium.

Outros erros


No final, deixei vocês mais deliciosos :) Vamos falar com um deles.

 HipsTile* HipsSurvey::getTile(int order, int pix) { .... if (order == orderMin && !allsky.isNull()) { int nbw = sqrt(12 * 1 << (2 * order)); int x = (pix % nbw) * allsky.width() / nbw; int y = (pix / nbw) * allsky.width() / nbw; int s = allsky.width() / nbw; QImage image = allsky.copy(x, y, s, s); .... } .... } 

Aviso do PVS-Studio: V634 A prioridade da operação '*' é maior que a da operação '<<'. É possível que parênteses sejam usados ​​na expressão. StelHips.cpp 271

Bem, você conseguiu detectar o erro? Considere a expressão (12 * 1 << (2 * ordem)) em mais detalhes. O analisador lembra que a operação ' * ' tem uma prioridade mais alta que a operação de troca de bits ' << '. É fácil ver que a multiplicação de 12 por 1 não faz sentido e os colchetes em torno de 2 * da ordem não são necessários.

  ,    : int nbw = sqrt(12 * (1 << 2 * order));     <i>12 </i>     . 

Nota Além disso, quero observar que, se o valor do operando direito ' << ' for maior ou igual ao número de bits do operando esquerdo, o resultado não será definido. Como literais numéricos são int por padrão, que leva 32 bits, o valor do parâmetro order não deve exceder 15 . Caso contrário, a avaliação da expressão pode resultar em comportamento indefinido.

Nós continuamos. O método abaixo é muito confuso, mas tenho certeza de que um leitor sofisticado lidará com a detecção de erros :)

 /* inherits documentation from base class */ QCPRange QCPStatisticalBox:: getKeyRange(bool& foundRange, SignDomain inSignDomain) const { foundRange = true; if (inSignDomain == sdBoth) { return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); } else if (inSignDomain == sdNegative) { if (mKey + mWidth * 0.5 < 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey < 0) return QCPRange(mKey - mWidth * 0.5, mKey); else { foundRange = false; return QCPRange(); } } else if (inSignDomain == sdPositive) { if (mKey - mWidth * 0.5 > 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey > 0) return QCPRange(mKey, mKey + mWidth * 0.5); else { foundRange = false; return QCPRange(); } } foundRange = false; return QCPRange(); } 

PVS-Studio Warning: V779 Código inacessível detectado. É possível que haja um erro. qcustomplot.cpp 19512.

O fato é que todos os ramos if ... else têm um retorno . Portanto, o fluxo de controle nunca atinge as duas últimas linhas.

Em geral, este exemplo será executado normalmente e funcionará corretamente. Mas a presença de código inacessível por si só é um sinal. Nesse caso, indica a estrutura incorreta do método, o que complica bastante a legibilidade e a compreensibilidade do código.

Esse fragmento de código deve ser refatorado, obtendo uma função mais elegante na saída. Por exemplo, assim:

 /* inherits documentation from base class */ QCPRange QCPStatisticalBox:: getKeyRange(bool& foundRange, SignDomain inSignDomain) const { foundRange = true; switch (inSignDomain) { case sdBoth: { return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); break; } case sdNegative: { if (mKey + mWidth * 0.5 < 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey < 0) return QCPRange(mKey - mWidth * 0.5, mKey); break; } case sdPositive: { if (mKey - mWidth * 0.5 > 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey > 0) return QCPRange(mKey, mKey + mWidth * 0.5); break; } } foundRange = false; return QCPRange(); } 

O último em nossa análise será o erro que eu mais gostei. O código do local do problema é curto e simples:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { Plane(v1, v2, v3, SPolygon::CCW); } 

Você notou algo suspeito? Nem todo mundo pode :)

PVS-Studio Warning: V603 O objeto foi criado, mas não está sendo usado. Se você deseja chamar o construtor, 'this-> Plane :: Plane (....)' deve ser usado. Plane.cpp 29

O programador esperava que alguns dos campos do objeto fossem inicializados dentro do construtor aninhado, mas acabou assim: quando o construtor Plane (Vec3f & v1, Vec3f & v2, Vec3f & v3) é chamado, um objeto temporário sem nome é criado dentro dele, que é imediatamente excluído. Como resultado, uma parte do objeto permanece não inicializada.

Para que o código funcione corretamente, você deve usar o recurso conveniente e seguro do C ++ 11 - o construtor de delegação:

 Plane::Plane(Vec3f& v1, Vec3f& v2, Vec3f& v3) : Plane(v1, v2, v3, SPolygon::CCW) { distance = 0.0f; sDistance = 0.0f; } 

Mas se você usar o compilador para versões mais antigas da linguagem, poderá escrever assim:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { this->Plane::Plane(v1, v2, v3, SPolygon::CCW); } 

Ou então:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { new (this) Plane(v1, v2, v3, SPolygon::CCW); } 

Noto que os dois últimos métodos são muito perigosos . Portanto, você deve ter muito cuidado e entender bem como esses métodos funcionam.

Conclusão


Que conclusões podem ser tiradas sobre a qualidade do código do Stellarium? Honestamente, não houve muitos erros. Além disso, em todo o projeto, não encontrei um único erro no qual o código esteja vinculado a um comportamento indefinido. Para o projeto de código-fonte aberto, a qualidade do código acabou sendo de alto nível, para a qual tiro meu chapéu para os desenvolvedores. Vocês são ótimos! Fiquei satisfeito e interessado em revisar seu projeto.

E o próprio planetário - eu o uso com bastante frequência. Infelizmente, morando em uma cidade, raramente consigo desfrutar de um céu noturno claro, e o Stellarium me permite estar em qualquer lugar do mundo sem me levantar do sofá. É realmente confortável!

Eu gosto especialmente do modo "Constellation art". A visão das figuras enormes cobrindo o céu inteiro em uma dança estranha é de tirar o fôlego.


"Uma dança estranha." Vista do Stellarium.

Os terráqueos tendem a cometer erros, e não há nada de vergonhoso no fato de esses erros vazarem no código. Para isso, são desenvolvidas ferramentas de análise de código, como o PVS-Studio. Se você é um dos terráqueos - como ele, sugiro que faça o download e experimente você mesmo .

Espero que você esteja interessado em ler meu artigo e tenha aprendido algo novo e útil para si mesmo. E desejo aos desenvolvedores uma correção precoce dos erros encontrados.

Assine nossos canais e fique atento às novidades do mundo da programação!



Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: George Gribkov. Into Space Again: como o unicórnio visitou o Stellarium

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


All Articles