PVS-Studio olhou para o mecanismo Red Dead Redemption - Bullet

Quadro 4

Hoje em dia, por exemplo, no desenvolvimento de jogos, não há mais a necessidade de implementar independentemente a física dos objetos do zero, pois há um grande número de bibliotecas para isso. O Bullet já foi usado ativamente em muitos jogos AAA, projetos de realidade virtual, várias simulações e aprendizado de máquina. Sim, e ainda é usado hoje, sendo, por exemplo, um dos mecanismos Red Dead Redemption e Red Dead Redemption 2. Portanto, por que não verificar o Bullet com o PVS-Studio para ver quais erros a análise estática pode detectar em um projeto de grande escala, relacionados à simulação de física.

Essa biblioteca é distribuída gratuitamente , para que todos possam usá-la opcionalmente em seus projetos. Além do Red Dead Redemption, esse mecanismo de física também é usado na indústria cinematográfica para criar efeitos especiais. Por exemplo, ele esteve envolvido nas filmagens de Sherlock Holmes por Guy Ritchie para calcular colisões.

Se este é o seu primeiro encontro com um artigo em que o PVS-Studio verifica projetos, farei uma pequena digressão. O PVS-Studio é um analisador de código estático que ajuda a encontrar erros, falhas e possíveis vulnerabilidades no código fonte dos programas Java em C, C ++, C #. A análise estática é um tipo de processo automatizado de revisão de código.

Para aquecer


Exemplo 1:

Vamos começar com um erro engraçado:

V624 Provavelmente, há uma impressão incorreta na constante '3.141592538'. Considere usar a constante M_PI de <math.h>. PhysicsClientC_API.cpp 4109

B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... } 

Um pequeno erro de digitação no número Pi (3,141592653 ...), faltando o número "6" na 7ª posição na parte fracionária.

Quadro 1
Talvez o erro na décima-milionésima casa decimal não leve a consequências tangíveis, mas você ainda deve usar as constantes da biblioteca existentes sem erros de digitação. Para Pi, há uma constante M_PI no cabeçalho math.h.

Copiar e colar


Exemplo 2:

Às vezes, o analisador permite encontrar o erro indiretamente. Portanto, por exemplo, aqui três argumentos relacionados halfExtentsX, halfExtentsY, halfExtentsZ são passados ​​para a função, mas o último não é usado em nenhum lugar da função. Você pode perceber que, ao chamar o método addVertex , a variável halfExtentsY é usada duas vezes. Portanto, talvez este seja um erro de copiar e colar e aqui um argumento esquecido deve ser usado.

V751 O parâmetro 'halfExtentsZ' não é usado dentro do corpo da função. TinyRenderer.cpp 375

 void TinyRenderObjectData::createCube(float halfExtentsX, float halfExtentsY, float halfExtentsZ, ....) { .... m_model->addVertex(halfExtentsX * cube_vertices_textured[i * 9], halfExtentsY * cube_vertices_textured[i * 9 + 1], halfExtentsY * cube_vertices_textured[i * 9 + 2], cube_vertices_textured[i * 9 + 4], ....); .... } 

Exemplo 3:

O analisador também encontrou o seguinte fragmento interessante: eu o trago primeiro em sua forma original.

Quadro 11

Vê esta última linha?

Quadro 12

É muito estranho que o programador tenha decidido escrever uma condição tão longa em uma linha. Mas o fato de um erro muito provavelmente ter surgido nele não é de todo surpreendente.

O analisador emitiu os seguintes avisos nesta linha.

V501 Existem sub-expressões idênticas 'rotmat.Column1 (). Norm () <1.0001' à esquerda e à direita do operador '&&'. LinearR4.cpp 351

V501 Existem sub-expressões idênticas '0.9999 <rotmat.Column1 (). Norm ()' à esquerda e à direita do operador '&&'. LinearR4.cpp 351

Se escrevermos tudo em uma forma visual “tabular”, ficará claro que as mesmas verificações se aplicam à Coluna1 . As duas últimas comparações mostram que há Coluna1 e Coluna2 . Provavelmente, a terceira e quarta comparações deveriam ter verificado o valor da Coluna2 .

  Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() && Column1().Norm() < 1.0001 && 0.9999 < Column1().Norm() &&(Column1() ^ Column2()) < 0.001 && (Column1() ^ Column2()) > -0.001 

Nesta forma, a mesma comparação se torna muito mais perceptível.

Exemplo 4:

Erro de tipo semelhante:

V501 Existem subexpressões idênticas 'cs.m_fJacCoeffInv [0] == 0' à esquerda e à direita do operador '&&'. b3CpuRigidBodyPipeline.cpp 169

 float m_fJacCoeffInv[2]; static inline void b3SolveFriction(b3ContactConstraint4& cs, ....) { if (cs.m_fJacCoeffInv[0] == 0 && cs.m_fJacCoeffInv[0] == 0) { return; } .... } 

Nesse caso, o mesmo elemento da matriz é verificado duas vezes. Muito provavelmente, a condição deveria ter a seguinte aparência: cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Este é um exemplo clássico de erro de copiar e colar.

Exemplo 5:

Outra falha foi descoberta:

V517 O uso do padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 79, 112. main.cpp 79

 int main(int argc, char* argv[]) { .... while (serviceResult > 0) { serviceResult = enet_host_service(client, &event, 0); if (serviceResult > 0) { .... } else if (serviceResult > 0) { puts("Error with servicing the client"); exit(EXIT_FAILURE); } .... } .... } 

A função enet_host_service , cujo resultado é atribuído a serviceResult , retorna um se for bem - sucedido e -1 se falhar. Provavelmente, o ramo mais se deveria ter respondido ao valor negativo de serviceResult , mas a condição de verificação foi duplicada. Provavelmente, esse também é um erro de copiar e colar.

Um aviso do analisador semelhante, que não faz sentido descrever no artigo:

V517 O uso do padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 151, 190. PhysicsClientUDP.cpp 151

Além dos limites: indo além dos limites da matriz


Exemplo 6:

Uma das coisas desagradáveis ​​para procurar erros é sair da matriz. Geralmente, esse erro ocorre devido à indexação complexa no loop.

Aqui, na condição de loop, a variável dofIndex é limitada a 128 a partir de cima e dof a 4, inclusive. Mas m_desiredState também contém apenas 128 elementos. Como resultado, o índice [dofIndex + dof] pode levar à saída da matriz.

A saturação da matriz V557 é possível. O valor do índice 'dofIndex + dof' pode chegar a 130. PhysicsClientC_API.cpp 968

 #define MAX_DEGREE_OF_FREEDOM 128 double m_desiredState[MAX_DEGREE_OF_FREEDOM]; B3_SHARED_API int b3JointControl(int dofIndex, double* forces, int dofCount, ....) { .... if ( (dofIndex >= 0) && (dofIndex < MAX_DEGREE_OF_FREEDOM ) && dofCount >= 0 && dofCount <= 4) { for (int dof = 0; dof < dofCount; dof++) { command->m_sendState.m_desiredState[dofIndex+dof] = forces[dof]; .... } } .... } 

Exemplo 7:

Um erro semelhante, mas agora a soma leva a ele não ao indexar uma matriz, mas em uma condição. Se o nome do arquivo for o maior possível, o zero do terminal será gravado fora da matriz ( Erro Off-by-one ). Naturalmente, a variável len será igual a MAX_FILENAME_LENGTH apenas em casos excepcionais, mas isso não elimina o erro, mas simplesmente torna sua ocorrência rara.

A saturação da matriz V557 é possível. O valor do índice 'len' pode chegar a 1024. PhysicsClientC_API.cpp 5223

 #define MAX_FILENAME_LENGTH MAX_URDF_FILENAME_LENGTH 1024 struct b3Profile { char m_name[MAX_FILENAME_LENGTH]; int m_durationInMicroSeconds; }; int len = strlen(name); if (len >= 0 && len < (MAX_FILENAME_LENGTH + 1)) { command->m_type = CMD_PROFILE_TIMING; strcpy(command->m_profile.m_name, name); command->m_profile.m_name[len] = 0; } 

Meça uma vez - corte sete vezes


Exemplo 8:

Nos casos em que você precisa usar o resultado de uma determinada função várias vezes ou usar repetidamente uma variável que precisa acessar por meio de uma série de chamadas, use variáveis ​​temporárias para otimizar e ler melhor o código. O analisador encontrou mais de 100 lugares no código onde essa correção pode ser feita.

V807 desempenho reduzido. Considere criar um ponteiro para evitar o uso da expressão 'm_app-> m_renderer-> getActiveCamera ()' repetidamente. InverseKinematicsExample.cpp 315

 virtual void resetCamera() { .... if (....) { m_app->m_renderer->getActiveCamera()->setCameraDistance(dist); m_app->m_renderer->getActiveCamera()->setCameraPitch(pitch); m_app->m_renderer->getActiveCamera()->setCameraYaw(yaw); m_app->m_renderer->getActiveCamera()->setCameraPosition(....); } } 

Aqui, a mesma cadeia de chamadas é reutilizada, que pode ser substituída por um único ponteiro.

Exemplo 9:

V810 desempenho reduzido. A função 'btCos (euler_out.pitch)' foi chamada várias vezes com argumentos idênticos. O resultado deve ser salvo em uma variável temporária, que pode ser usada ao chamar a função 'btAtan2'. btMatrix3x3.h 576

V810 desempenho reduzido. A função 'btCos (euler_out2.pitch)' foi chamada várias vezes com argumentos idênticos. O resultado deve ser salvo em uma variável temporária, que pode ser usada ao chamar a função 'btAtan2'. btMatrix3x3.h 578

 void getEulerZYX(....) const { .... if (....) { .... } else { .... euler_out.roll = btAtan2(m_el[2].y() / btCos(euler_out.pitch), m_el[2].z() / btCos(euler_out.pitch)); euler_out2.roll = btAtan2(m_el[2].y() / btCos(euler_out2.pitch), m_el[2].z() / btCos(euler_out2.pitch)); euler_out.yaw = btAtan2(m_el[1].x() / btCos(euler_out.pitch), m_el[0].x() / btCos(euler_out.pitch)); euler_out2.yaw = btAtan2(m_el[1].x() / btCos(euler_out2.pitch), m_el[0].x() / btCos(euler_out2.pitch)); } .... } 

Nesse caso, você pode criar duas variáveis ​​e armazenar os valores retornados pela função btCos para euler_out.pitch e euler_out2.pitch nelas , em vez de chamar a função quatro vezes para cada argumento.

Vazamento


Exemplo 10:

Muitos erros do seguinte tipo foram encontrados no projeto:

V773 O escopo de visibilidade do ponteiro 'importador' foi encerrado sem liberar a memória. É possível um vazamento de memória. SerializeSetup.cpp 94

 void SerializeSetup::initPhysics() { .... btBulletWorldImporter* importer = new btBulletWorldImporter(m_dynamicsWorld); .... fclose(file); m_guiHelper->autogenerateGraphicsObjects(m_dynamicsWorld); } 

Nenhuma memória foi liberada do ponteiro do importador aqui. Isso pode causar vazamento de memória. E para o mecanismo físico, isso pode ser uma tendência ruim. Para evitar vazamentos, basta que a variável não seja mais necessária para adicionar o importador de exclusão . Mas é claro que é melhor usar ponteiros inteligentes.

Aqui estão suas leis


Exemplo 11:

O erro a seguir aparece no código devido ao fato de que as regras C ++ nem sempre coincidem com as regras matemáticas ou com o "senso comum". Observe por si mesmo onde está o erro em um pequeno trecho de código?

 btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... } 

O analisador gera o seguinte aviso:

V709 Comparação suspeita encontrada: 'f0 == f1 == m_fractureBodies.size ()'. Lembre-se de que 'a == b == c' não é igual a 'a == b && b == c'. btFractureDynamicsWorld.cpp 483

Parece que a condição verifica se f0 é igual a f1 e igual ao número de elementos em m_fractureBodies . Parece que essa comparação deveria ter verificado se f0 e f1 estão no final da matriz m_fractureBodies , pois contêm a posição do objeto encontrado pelo método findLinearSearch () . No entanto, na realidade, essa expressão se transforma em uma verificação para ver se f0 e f1 são iguais e, em seguida, para verificar se m_fractureBodies.size () é igual ao resultado de f0 == f1 . Como resultado, o terceiro operando é comparado com 0 ou 1 aqui.

Belo erro! E, felizmente, bastante raro. Até agora, nós a encontramos apenas em dois projetos abertos e, curiosamente, todos eles eram apenas motores de jogos.

Exemplo 12:

Ao trabalhar com strings, geralmente é melhor usar os recursos fornecidos pela classe de strings . Portanto, para os dois casos a seguir, é melhor substituir strlen (MyStr.c_str ()) e val = "" por MyStr.length () e val.clear (), respectivamente.

V806 desempenho reduzido. A expressão do tipo strlen (MyStr.c_str ()) pode ser reescrita como MyStr.length (). RobotLoggingUtil.cpp 213

 FILE* createMinitaurLogFile(const char* fileName, std::string& structTypes, ....) { FILE* f = fopen(fileName, "wb"); if (f) { .... fwrite(structTypes.c_str(), strlen(structTypes.c_str()), 1, f); .... } .... } 

V815 Desempenho Diminuído. Considere substituir a expressão 'val = ""' por 'val.clear ()'. b3CommandLineArgs.h 40

 void addArgs(int argc, char **argv) { .... std::string val; .... val = ""; .... } 

Houve outros avisos, mas acho que você pode parar. Como você pode ver, a análise de código estático pode revelar uma ampla gama de vários erros.

É interessante ler sobre verificações únicas de projetos, mas essa não é a maneira correta de usar analisadores de código estático. E sobre isso, falaremos abaixo.

Erros encontrados antes de nós


Foi interessante no espírito do artigo recente “ Erros que a análise de código estático não encontra porque não é usada ” para tentar encontrar bugs ou deficiências que já foram corrigidos, mas que o analisador estático poderia detectar.
Quadro 2

Não havia tantas solicitações de recebimento no repositório e muitas delas estão relacionadas à lógica interna do mecanismo. Mas também houve erros que o analisador pôde detectar.

Exemplo 13:

 char m_deviceExtensions[B3_MAX_STRING_LENGTH]; void b3OpenCLUtils_printDeviceInfo(cl_device_id device) { b3OpenCLDeviceInfo info; b3OpenCLUtils::getDeviceInfo(device, &info); .... if (info.m_deviceExtensions != 0) { .... } } 

O comentário de edição diz que era necessário verificar a matriz pelo fato de ela não estar vazia, mas, em vez disso, foi executada uma verificação sem ponteiro, que sempre retornava verdadeira. Isso é indicado pelo aviso PVS-Studio no tipo inicial de verificação:

V600 Considere inspecionar a condição. O ponteiro 'info.m_deviceExtensions' nem sempre é igual a NULL. b3OpenCLUtils.cpp 551

Exemplo 14:

Você pode descobrir imediatamente qual é o problema na próxima função?

 inline void Matrix4x4::SetIdentity() { m12 = m13 = m14 = m21 = m23 = m24 = m13 = m23 = m41 = m42 = m43 = 0.0; m11 = m22 = m33 = m44 = 1.0; } 

O analisador gera os seguintes avisos:

V570 O mesmo valor é atribuído duas vezes à variável 'm23'. LinearR4.h 627

V570 O mesmo valor é atribuído duas vezes à variável 'm13'. LinearR4.h 627

Tarefas repetidas com essa forma de gravação são difíceis de rastrear a olho nu e, como resultado, alguns dos elementos da matriz não receberam o valor inicial. Este erro foi corrigido no formato tabular do registro de atribuição:

 m12 = m13 = m14 = m21 = m23 = m24 = m31 = m32 = m34 = m41 = m42 = m43 = 0.0; 

Exemplo 15:

O erro a seguir em uma das condições da função btSoftBody :: addAeroForceToNode () levou a um erro explícito. De acordo com o comentário na solicitação de recebimento, forças foram aplicadas a objetos do lado errado.

 struct eAeroModel { enum _ { V_Point, V_TwoSided, .... END }; }; void btSoftBody::addAeroForceToNode(....) { .... if (....) { if (btSoftBody::eAeroModel::V_TwoSided) { .... } .... } .... } 

O PVS-Studio também pode encontrar esse erro e exibir o seguinte aviso:

V768 A constante de enumeração 'V_TwoSided' é usada como uma variável de um tipo booleano. btSoftBody.cpp 542

A verificação corrigida é a seguinte:

 if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided) { .... } 

Em vez da equivalência da propriedade do objeto a um dos enumeradores, o próprio enumerador V_TwoSided foi verificado.

É claro que eu não vi todas as solicitações de recebimento, pois não defini essa tarefa para mim. Eu só queria mostrar que o uso regular de um analisador de código estático pode detectar erros em um estágio muito inicial. Essa é a maneira correta de usar a análise de código estático. A análise estática deve ser incorporada ao processo do DevOps e ser o principal filtro de bugs. Tudo isso está bem descrito no artigo " Incorpore a análise estática ao processo e não procure por erros ".

Conclusão


Quadro 6

A julgar por algumas solicitações pull, às vezes um projeto é verificado por meio de várias ferramentas de análise de código, no entanto, as edições são feitas não gradualmente, mas em grupos e em grandes intervalos de tempo. Em algumas solicitações, um comentário indica que foram feitas alterações apenas para suprimir avisos. Essa abordagem ao uso da análise reduz significativamente sua utilidade, pois é uma verificação constante do projeto que permite corrigir erros imediatamente, e não espere até que ocorram erros óbvios.

Se você deseja estar sempre informado sobre as novidades e os eventos de nossa equipe, assine nossos serviços sociais. Redes: Instagram , Twitter , Vkontakte , Telegram .



Se você deseja compartilhar este artigo com um público que fala inglês, use o link da tradução: PVS-Studio Observou o Bullet Engine do Red Dead Redemption

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


All Articles