PVS-Studio melirik mesin Red Dead Redemption - Bullet

Gambar 4

Saat ini, misalnya, untuk mengembangkan game, tidak ada lagi kebutuhan untuk secara mandiri menerapkan fisika objek dari awal, karena ada banyak perpustakaan untuk ini. Bullet pernah digunakan secara aktif di banyak game AAA, proyek realitas virtual, berbagai simulasi dan pembelajaran mesin. Ya, dan masih digunakan sampai sekarang, misalnya, sebagai salah satu mesin Red Dead Redemption dan Red Dead Redemption 2. Jadi mengapa tidak memeriksa Bullet dengan PVS-Studio untuk melihat kesalahan apa yang dapat dideteksi oleh analisis statis dalam proyek berskala besar seperti itu, terkait dengan simulasi fisika.

Perpustakaan ini didistribusikan secara bebas , sehingga setiap orang dapat menggunakannya secara opsional dalam proyek mereka. Selain Red Dead Redemption, mesin fisika ini juga digunakan dalam industri film untuk menciptakan efek khusus. Misalnya, ia terlibat dalam pembuatan film Sherlock Holmes oleh Guy Ritchie untuk menghitung tabrakan.

Jika ini adalah pertemuan pertama Anda dengan artikel di mana PVS-Studio memeriksa proyek, maka saya akan melakukan penyimpangan kecil. PVS-Studio adalah penganalisa kode statis yang membantu menemukan kesalahan, kekurangan dan kerentanan potensial dalam kode sumber program C, C ++, C #, Java. Analisis statis adalah semacam proses peninjauan kode otomatis.

Untuk pemanasan


Contoh 1:

Mari kita mulai dengan kesalahan lucu:

V624 Mungkin ada kesalahan cetak dalam konstanta '3.141592538'. Pertimbangkan untuk menggunakan konstanta M_PI dari <math.h>. PhysicsClientC_API.cpp 4109

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

Kesalahan ketik kecil di nomor Pi (3.141592653 ...), hilang nomor "6" di posisi ke-7 di bagian fraksional.

Gambar 1
Mungkin kesalahan di tempat desimal kesepuluh juta tidak akan menghasilkan konsekuensi nyata, tetapi Anda harus tetap menggunakan konstanta perpustakaan yang ada tanpa kesalahan ketik. Untuk Pi, ada konstanta M_PI dari header math.h.

Salin tempel


Contoh 2:

Kadang-kadang analisa memungkinkan Anda untuk menemukan kesalahan secara tidak langsung. Jadi, misalnya, di sini tiga argumen terkait halfExtentsX, halfExtentsY, halfExtentsZ dilewatkan ke fungsi, tetapi yang terakhir tidak digunakan di mana pun dalam fungsi. Anda mungkin memperhatikan bahwa saat memanggil metode addVertex , variabel halfExtentsY digunakan dua kali. Jadi mungkin ini adalah kesalahan copy-paste dan di sini argumen yang dilupakan harus digunakan.

V751 Parameter 'halfExtentsZ' tidak digunakan di dalam fungsi body. 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], ....); .... } 

Contoh 3:

Penganalisa juga menemukan fragmen menarik berikut, saya akan membawanya pertama dalam bentuk aslinya.

Gambar 11

Lihat baris terakhir ini?

Gambar 12

Sangat aneh bahwa programmer memutuskan untuk menulis kondisi yang begitu lama dalam satu baris. Tetapi fakta bahwa kesalahan yang paling mungkin menyelinap ke dalamnya sama sekali tidak mengejutkan.

Penganalisa mengeluarkan peringatan berikut pada baris ini.

V501 Ada sub-ekspresi identik 'rotmat.Column1 (). Norm () <1.0001' di sebelah kiri dan di sebelah kanan operator '&&'. LinearR4.cpp 351

V501 Ada sub-ekspresi identik '0,9999 <rotmat.Column1 (). Norm ()' di sebelah kiri dan di sebelah kanan operator '&&'. LinearR4.cpp 351

Jika kita menulis semuanya dalam bentuk "tabular" visual, akan menjadi jelas bahwa pemeriksaan yang sama berlaku untuk Column1 . Dua perbandingan terakhir menunjukkan bahwa ada Column1 dan Column2 . Kemungkinan besar, perbandingan ketiga dan keempat harus memeriksa nilai dari Kolom2 .

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

Dalam bentuk ini, perbandingan yang sama menjadi jauh lebih terlihat.

Contoh 4:

Kesalahan yang serupa:

V501 Ada sub-ekspresi identik 'cs.m_fJacCoeffInv [0] == 0' di sebelah kiri dan di sebelah kanan operator '&&'. 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; } .... } 

Dalam hal ini, elemen array yang sama diperiksa dua kali. Kemungkinan besar, kondisinya akan terlihat seperti ini: cs.m_fJacCoeffInv [0] == 0 && cs.m_fJacCoeffInv [1] == 0 . Ini adalah contoh klasik kesalahan salin-tempel.

Contoh 5:

Kekurangan lain ditemukan:

V517 Penggunaan pola 'jika (A) {...} else jika (A) {...}' terdeteksi. Ada kemungkinan kehadiran kesalahan logis. Periksa baris: 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); } .... } .... } 

Fungsi enet_host_service , yang hasilnya ditugaskan ke serviceResult , mengembalikan satu jika berhasil dan -1 jika gagal. Kemungkinan besar, yang lain jika cabang seharusnya menanggapi nilai negatif dari serviceResult , tetapi kondisi verifikasi digandakan. Kemungkinan besar ini juga merupakan kesalahan salin-tempel.

Peringatan penganalisa serupa, yang tidak masuk akal untuk dijelaskan dalam artikel:

V517 Penggunaan pola 'jika (A) {...} else jika (A) {...}' terdeteksi. Ada kemungkinan kehadiran kesalahan logis. Periksa baris: 151, 190. PhysicsClientUDP.cpp 151

Melampaui batas: melampaui batas array


Contoh 6:

Salah satu hal yang tidak menyenangkan untuk mencari kesalahan adalah keluar dari array. Seringkali kesalahan ini terjadi karena pengindeksan kompleks dalam loop.

Di sini, dalam kondisi loop, variabel dofIndex terbatas pada 128 dari atas, dan dof hingga 4, inklusif. Tetapi m_desiredState juga hanya berisi 128 elemen. Akibatnya, indeks [dofIndex + dof] dapat menyebabkan keluarnya array.

V557 Array overrun dimungkinkan. Nilai indeks 'dofIndex + dof' bisa mencapai 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]; .... } } .... } 

Contoh 7:

Kesalahan serupa, tapi sekarang penjumlahan mengarah ke itu bukan saat mengindeks array, tetapi dalam kondisi. Jika nama file sepanjang mungkin, maka terminal nol akan ditulis di luar array ( Kesalahan Off-by-one ). Secara alami, variabel len akan sama dengan MAX_FILENAME_LENGTH hanya dalam kasus luar biasa, tetapi ini tidak menghilangkan kesalahan, tetapi hanya membuatnya jarang terjadi.

V557 Array overrun dimungkinkan. Nilai indeks 'len' bisa mencapai 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; } 

Ukur sekali - potong tujuh kali


Contoh 8:

Dalam kasus di mana Anda perlu menggunakan hasil fungsi tertentu berkali-kali atau berulang kali menggunakan variabel yang perlu Anda akses melalui serangkaian panggilan, Anda harus menggunakan variabel sementara untuk mengoptimalkan dan membaca kode dengan lebih baik. Penganalisa menemukan lebih dari 100 tempat dalam kode di mana koreksi tersebut dapat dilakukan.

V807 Penurunan Kinerja. Pertimbangkan membuat pointer untuk menghindari penggunaan ekspresi 'm_app-> m_renderer-> getActiveCamera ()' berulang kali. 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(....); } } 

Di sini rantai panggilan yang sama digunakan kembali, yang dapat diganti dengan satu penunjuk.

Contoh 9:

V810 Menurunkan Performa. Fungsi 'btCos (euler_out.pitch)' dipanggil beberapa kali dengan argumen yang identik. Hasilnya mungkin harus disimpan ke variabel sementara, yang kemudian dapat digunakan saat memanggil fungsi 'btAtan2'. btMatrix3x3.h 576

V810 Menurunkan Performa. Fungsi 'btCos (euler_out2.pitch)' dipanggil beberapa kali dengan argumen yang identik. Hasilnya mungkin harus disimpan ke variabel sementara, yang kemudian dapat digunakan saat memanggil fungsi '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)); } .... } 

Dalam kasus ini, Anda dapat membuat dua variabel dan menyimpan nilai yang dikembalikan oleh fungsi btCos untuk euler_out.pitch dan euler_out2.pitch di dalamnya , alih-alih memanggil fungsi empat kali untuk setiap argumen.

Bocor


Contoh 10:

Banyak kesalahan dari jenis berikut ini ditemukan dalam proyek:

V773 Ruang lingkup visibilitas dari pointer 'importir' dikeluarkan tanpa melepaskan memori. Kebocoran memori dimungkinkan. SerializeSetup.cpp 94

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

Tidak ada memori yang dibebaskan dari pointer importir di sini. Ini dapat menyebabkan kebocoran memori. Dan untuk mesin fisik, ini mungkin tren yang buruk. Untuk menghindari kebocoran, cukup setelah variabel tidak lagi diperlukan untuk menambahkan importir hapus . Tapi tentu saja, lebih baik menggunakan pointer pintar.

Ini undang-undang Anda


Contoh 11:

Kesalahan berikut muncul dalam kode karena fakta bahwa aturan C ++ tidak selalu bertepatan dengan aturan matematika atau "akal sehat". Perhatikan sendiri di mana kesalahan ada dalam potongan kecil kode?

 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; .... } .... } 

Penganalisis menghasilkan peringatan berikut:

V709 Perbandingan mencurigakan ditemukan: 'f0 == f1 == m_fractureBodies.size ()'. Ingat bahwa 'a == b == c' tidak sama dengan 'a == b && b == c'. btFractureDynamicsWorld.cpp 483

Tampaknya kondisi memeriksa bahwa f0 sama dengan f1 dan sama dengan jumlah elemen di m_fractureBodies . Sepertinya perbandingan ini seharusnya memeriksa apakah f0 dan f1 berada di akhir array m_fractureBodies , karena mereka berisi posisi objek yang ditemukan oleh metode findLinearSearch () . Namun, pada kenyataannya, ungkapan ini berubah menjadi tes untuk melihat apakah f0 dan f1 sama, dan kemudian untuk memeriksa apakah m_fractureBodies.size () sama dengan hasil f0 == f1 . Akibatnya, operan ketiga dibandingkan dengan 0 atau 1 di sini.

Kesalahan indah! Dan, untungnya, sangat jarang. Sejauh ini kami hanya bertemu dengannya di dua proyek terbuka, dan yang menarik, semuanya hanyalah mesin game.

Contoh 12:

Ketika bekerja dengan string, seringkali lebih baik menggunakan fasilitas yang disediakan oleh kelas string . Jadi, untuk dua kasus berikut, lebih baik mengganti strlen (MyStr.c_str ()) dan val = "" dengan MyStr.length () dan val.clear (), masing-masing.

V806 Menurunkan Kinerja. Ekspresi jenis strlen (MyStr.c_str ()) dapat ditulis ulang sebagai 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 Menurunkan Performa. Pertimbangkan untuk mengganti ekspresi 'val = ""' dengan 'val.clear ()'. b3CommandLineArgs.h 40

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

Ada peringatan lain, tapi saya pikir Anda bisa berhenti. Seperti yang Anda lihat, analisis kode statis dapat mengungkapkan berbagai macam kesalahan.

Membaca tentang pemeriksaan proyek satu kali itu menarik, tetapi ini bukan cara yang tepat untuk menggunakan penganalisa kode statis. Dan tentang ini kita akan berbicara di bawah ini.

Kesalahan ditemukan sebelum kita


Sangat menarik dalam semangat artikel baru-baru ini " Kesalahan yang tidak ditemukan oleh analisis kode statis karena tidak digunakan " untuk mencoba menemukan bug atau kekurangan yang telah diperbaiki, tetapi yang dapat dideteksi oleh penganalisa statis.
Gambar 2

Tidak ada begitu banyak permintaan tarik dalam repositori, dan banyak dari mereka terkait dengan logika internal mesin. Tetapi ada juga kesalahan yang bisa dideteksi oleh penganalisa.

Contoh 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) { .... } } 

Edit komentar mengatakan bahwa itu perlu untuk memeriksa array untuk fakta bahwa itu tidak kosong, tetapi sebaliknya pemeriksaan pointer pointless dilakukan, yang selalu mengembalikan true. Ini ditunjukkan oleh peringatan PVS-Studio pada jenis cek awal:

V600 Pertimbangkan untuk memeriksa kondisinya. Pointer 'info.m_deviceExtensions' selalu tidak sama dengan NULL. b3OpenCLUtils.cpp 551

Contoh 14:

Bisakah Anda segera mencari tahu apa masalahnya di fungsi berikutnya?

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

Penganalisis menghasilkan peringatan berikut:

V570 Nilai yang sama diberikan dua kali ke variabel 'm23'. LinearR4.h 627

V570 Nilai yang sama diberikan dua kali ke variabel 'm13'. LinearR4.h 627

Tugas berulang dengan bentuk rekaman ini sulit dilacak dengan mata telanjang dan sebagai hasilnya, beberapa elemen dari matriks tidak menerima nilai awal. Kesalahan ini diperbaiki dalam bentuk tabel dari catatan penugasan:

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

Contoh 15:

Kesalahan berikut di salah satu kondisi fungsi btSoftBody :: addAeroForceToNode () menyebabkan bug eksplisit. Menurut komentar dalam permintaan tarikan, kekuatan diterapkan pada objek dari sisi yang salah.

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

PVS-Studio juga dapat menemukan kesalahan ini dan menampilkan peringatan berikut:

V768 Konstanta enumerasi 'V_TwoSided' digunakan sebagai variabel tipe-Boolean. btSoftBody.cpp 542

Cek terkoreksi adalah sebagai berikut:

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

Alih-alih kesetaraan properti objek ke salah satu enumerator, enumerator V_TwoSided sendiri diperiksa.

Jelas bahwa saya tidak melihat semua permintaan tarik, karena saya tidak mengatur sendiri tugas semacam itu. Saya hanya ingin menunjukkan bahwa penggunaan penganalisis kode statis secara teratur dapat mendeteksi kesalahan pada tahap yang sangat awal. Ini adalah cara yang benar untuk menggunakan analisis kode statis. Analisis statis harus dibangun ke dalam proses DevOps dan menjadi filter utama bug. Semua ini dijelaskan dengan baik dalam artikel " Masukkan analisis statis ke dalam proses, dan jangan mencari bug dengan itu ."

Kesimpulan


Gambar 6

Dilihat oleh beberapa permintaan tarik, suatu proyek kadang-kadang diperiksa melalui berbagai alat analisis kode, namun, pengeditan dilakukan tidak secara bertahap, tetapi dalam kelompok dan pada interval waktu yang besar. Dalam beberapa permintaan, komentar menunjukkan bahwa perubahan dilakukan hanya untuk menekan peringatan. Pendekatan ini untuk penggunaan analisis secara signifikan mengurangi kegunaannya, karena ini adalah pemeriksaan konstan proyek yang memungkinkan Anda untuk memperbaiki kesalahan dengan segera, dan tidak menunggu sampai muncul bug yang jelas.

Jika Anda ingin selalu mengetahui tentang berita dan acara tim kami, berlangganan layanan sosial kami. Jaringan: Instagram , Twitter , Vkontakte , Telegram .



Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan terjemahan: PVS-Studio Melihat ke dalam Mesin Peluru Red Dead Redemption's Bullet

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


All Articles