Para penulis game 0 AD - dilakukan dengan baik

PVS-Studio & 0 A.D.

0 AD adalah permainan tiga dimensi dalam genre strategi historis secara real time, yang dikembangkan oleh komunitas sukarelawan. Ukuran basis kode kecil dan saya memutuskan untuk mengecek permainan sebagai istirahat dari proyek-proyek besar seperti Android dan XNU Kernel. Jadi, sebelum kita adalah proyek yang berisi 165.000 baris kode dalam C ++. Mari kita lihat apa yang menarik di dalamnya yang dapat ditemukan menggunakan analisa statis PVS-Studio.

0 AD


0 AD (0 A.D.) adalah game tiga dimensi gratis dalam genre strategi historis secara real time, dikembangkan oleh komunitas sukarelawan (pengembang utama dipersatukan dalam tim Wildfire Games). Permainan ini memungkinkan Anda untuk mengontrol peradaban yang ada pada periode 500 SM. e. - 1 tahun SM. e. Pada musim panas 2018, proyek ini dalam versi alpha. [ Keterangan diambil dari Wikipedia].

Kenapa tepatnya 0 AD?

Saya meminta seorang rekan dari Egor Bredikhin ( EgorBredikhin ) untuk memilih dan memeriksa saya beberapa proyek terbuka kecil yang dengannya saya dapat mengerjakan di antara tugas-tugas lain. Dia mengirimi saya log untuk proyek 0 M. Untuk pertanyaan: "Mengapa proyek ini?" - ada jawaban: "Ya, saya baru saja memainkan game ini, strategi real-time yang bagus." Ok, maka itu akan menjadi 0 AD :).

Kerapatan kesalahan


Saya ingin memuji penulis 0 AD untuk kualitas yang baik dari kode C ++. Bagus sekali, Anda jarang berhasil memenuhi kepadatan kesalahan yang begitu rendah. Tentu saja, ini bukan semua kesalahan, tetapi yang bisa dideteksi menggunakan PVS-Studio. Seperti yang saya katakan, meskipun PVS-Studio tidak menemukan semua kesalahan, namun demikian, kita dapat dengan aman berbicara tentang hubungan antara kepadatan kesalahan dan kualitas kode secara keseluruhan.

Beberapa angka. Jumlah total baris kode yang tidak kosong adalah 231270. Dari jumlah tersebut, 28,7% adalah komentar. Secara total, 165.000 baris kode C ++ murni.

Jumlah pesan yang dikeluarkan oleh penganalisa kecil, dan setelah melihat semuanya, saya menulis 19 kesalahan. Saya akan mempertimbangkan semua kesalahan ini nanti di artikel. Mungkin saya melewatkan sesuatu, menganggap kesalahan itu sebagai kode ceroboh yang tidak berbahaya. Namun, secara umum, ini tidak mengubah gambar.

Jadi, saya menemukan 19 kesalahan pada 165.000 baris kode. Kami menghitung kepadatan kesalahan: 19 * 1000/165000 = 0,115.

Untuk kesederhanaan, kami membulatkan dan menganggap bahwa penganalisa PVS-Studio mendeteksi 0,1 kesalahan per 1000 baris kode dalam kode permainan.

Hasil yang bagus! Sebagai perbandingan, dalam artikel saya yang terbaru tentang Android, saya menghitung bahwa saya mendeteksi setidaknya 0,25 kesalahan per 1000 baris kode. Bahkan, kepadatan kesalahan bahkan lebih besar, saya hanya tidak menemukan kekuatan untuk menganalisis seluruh laporan dengan cermat.

Atau ambil EFL Core Libraries sebagai contoh, yang saya teliti pelajari dan hitung jumlah cacatnya. Di dalamnya, PVS-Studio mendeteksi 0,71 kesalahan per 1000 baris kode.

Jadi, penulis 0 AD adalah orang yang baik, namun, demi keadilan, harus dicatat bahwa sejumlah kecil kode yang ditulis dalam C ++ membantu mereka. Sayangnya, semakin besar proyek, semakin cepat kompleksitasnya tumbuh, dan kepadatan kesalahan meningkat secara non-linear ( lebih ).

Kesalahan


Sekarang mari kita lihat 19 kesalahan yang saya temukan di game. Untuk menganalisis proyek, saya menggunakan PVS-Studio versi 6.24. Saya sarankan Anda mencoba mengunduh versi demo dan memeriksa proyek yang sedang Anda kerjakan.

Catatan Kami memposisikan PVS-Studio sebagai solusi B2B. Untuk proyek kecil dan pengembang individual, kami memiliki opsi lisensi gratis: " Cara menggunakan PVS-Studio gratis ."

Kesalahan N1

Mari kita mulai dengan melihat kesalahan kompleks. Sebenarnya, ini tidak rumit, tetapi Anda harus membiasakan diri dengan sepotong kode yang cukup besar.

void WaterManager::CreateWaveMeshes() { .... int nbNeighb = 0; .... bool found = false; nbNeighb = 0; for (int p = 0; p < 8; ++p) { if (CoastalPointsSet.count(xx+around[p][0] + (yy + around[p][1])*SideSize)) { if (nbNeighb >= 2) { CoastalPointsSet.erase(xx + yy*SideSize); continue; } ++nbNeighb; // We've found a new point around us. // Move there xx = xx + around[p][0]; yy = yy + around[p][1]; indexx = xx + yy*SideSize; if (i == 0) Chain.push_back(CoastalPoint(indexx,CVector2D(xx*2,yy*2))); else Chain.push_front(CoastalPoint(indexx,CVector2D(xx*2,yy*2))); CoastalPointsSet.erase(xx + yy*SideSize); found = true; break; } } if (!found) endedChain = true; .... } 

Peringatan PVS-Studio: V547 CWE-570 Ekspresi 'nbNeighb> = 2' selalu salah. WaterManager.cpp 581

Sekilas, pesan penganalisa tampak aneh. Mengapa kondisi nbNeighb> = 2 selalu salah? Memang, dalam tubuh loop ada peningkatan variabel nbNeighb !

Lihat di bawah dan Anda akan melihat pernyataan break yang mengganggu eksekusi loop. Oleh karena itu, jika variabel nbNeighb ditingkatkan, siklus akan dihentikan. Dengan demikian, nilai variabel nbNeighb tidak akan pernah mencapai nilai lebih dari 1.

Kode jelas mengandung beberapa jenis kesalahan logis.

Kesalahan N2

 void CmpRallyPointRenderer::MergeVisibilitySegments( std::deque<SVisibilitySegment>& segments) { .... segments.erase(segments.end()); .... } 

PVS-Studio Peringatan: V783 CWE-119 Dereferencing iterator yang tidak valid ' segment.end ()' mungkin terjadi. CCmpRallyPointRenderer.cpp 1290

Kode yang sangat, sangat aneh. Mungkin programmer ingin menghapus elemen terakhir dari wadah. Dalam hal ini, kode harus seperti ini:

 segments.erase(segments.end() - 1); 

Meskipun, maka akan lebih mudah untuk menulis:

 segments.pop_back(); 

Sejujurnya, tidak sepenuhnya jelas bagi saya apa yang seharusnya ditulis di sini.

Kesalahan N3, N4

Saya memutuskan untuk mempertimbangkan dua kesalahan bersama-sama, karena terkait dengan kebocoran sumber daya dan mengharuskan terlebih dahulu untuk menunjukkan apa itu makro WARN_RETURN .

 #define WARN_RETURN(status)\ do\ {\ DEBUG_WARN_ERR(status);\ return status;\ }\ while(0) 

Jadi, seperti yang Anda lihat, makro WARN_RETURN menyebabkan fungsi untuk keluar dari tubuh. Sekarang mari kita lihat cara yang tidak akurat untuk menggunakan makro ini.

Fragmen pertama.

 Status sys_generate_random_bytes(u8* buf, size_t count) { FILE* f = fopen("/dev/urandom", "rb"); if (!f) WARN_RETURN(ERR::FAIL); while (count) { size_t numread = fread(buf, 1, count, f); if (numread == 0) WARN_RETURN(ERR::FAIL); buf += numread; count -= numread; } fclose(f); return INFO::OK; } 

Peringatan PVS-Studio: V773 CWE-401 Fungsi ini keluar tanpa melepaskan pegangan 'f'. Kebocoran sumber daya mungkin terjadi. unix.cpp 332

Jika fungsi ketakutan tidak dapat membaca data, maka fungsi sys_generate_random_bytes akan keluar tanpa melepaskan deskriptor file. Dalam praktiknya, ini hampir tidak mungkin. Sangat diragukan bahwa Anda tidak akan dapat membaca data dari "/ dev / urandom". Namun, kode itu ceroboh.

Fragmen kedua.

 Status sys_cursor_create(....) { .... sys_cursor_impl* impl = new sys_cursor_impl; impl->image = image; impl->cursor = XcursorImageLoadCursor(wminfo.info.x11.display, image); if(impl->cursor == None) WARN_RETURN(ERR::FAIL); *cursor = static_cast<sys_cursor>(impl); return INFO::OK; } 

Peringatan PVS-Studio: V773 CWE-401 Fungsi ini keluar tanpa melepaskan pointer 'impl'. Kebocoran memori dimungkinkan. x.cpp 421

Jika kursor gagal dimuat, kebocoran memori terjadi.

Kesalahan N5

 Status LoadHeightmapImageOs(....) { .... shared_ptr<u8> fileData = shared_ptr<u8>(new u8[fileSize]); .... } 

Peringatan PVS-Studio: V554 CWE-762 Penggunaan shared_ptr salah. Memori yang dialokasikan dengan 'baru [] akan dibersihkan menggunakan' hapus '. MapIO.cpp 54

Opsi yang benar:

 shared_ptr<u8[]> fileData = shared_ptr<u8>(new u8[fileSize]); 

Kesalahan N6

 FUTrackedPtr(ObjectClass* _ptr = NULL) : ptr(_ptr) { if (ptr != NULL) FUTracker::TrackObject((FUTrackable*) ptr); ptr = ptr; } 

PVS-Studio Warning: V570 Variabel 'ptr' ditugaskan untuk dirinya sendiri. FUTracker.h 122

Kesalahan N7, N8
 std::wstring TraceEntry::EncodeAsText() const { const wchar_t action = (wchar_t)m_action; wchar_t buf[1000]; swprintf_s(buf, ARRAY_SIZE(buf), L"%#010f: %c \"%ls\" %lu\n", m_timestamp, action, m_pathname.string().c_str(), (unsigned long)m_size); return buf; } 

Peringatan PVS-Studio: V576 CWE-628 Format salah. Pertimbangkan untuk memeriksa argumen aktual kelima dari fungsi 'swprintf_s'. Argumen tipe char diharapkan. trace.cpp 93

Di sini kita menemukan sejarah yang bingung dan tidak jelas implementasi alternatif dari fungsi swprintf dalam Visual C ++. Saya tidak akan menceritakannya kembali , tetapi merujuk pada dokumentasi untuk diagnostik V576 (lihat bagian "Garis lebar").

Dalam hal ini, kemungkinan besar, kode ini akan berfungsi dengan benar ketika dikompilasi dalam Visual C ++ untuk Windows dan salah ketika membangun untuk Linux atau macOS.

Kesalahan serupa: V576 CWE-628 Format salah. Pertimbangkan untuk memeriksa argumen aktual keempat dari fungsi 'swprintf_s'. Argumen tipe char diharapkan. vfs_tree.cpp 211

Kesalahan N9, N10, N11

Klasik Pada awalnya, pointer digunakan, dan baru dicentang.

 static void TEST_CAT2(char* dst, ....) { strcpy(dst, dst_val); // <= int ret = strcat_s(dst, max_dst_chars, src); TS_ASSERT_EQUALS(ret, expected_ret); if(dst != 0) // <= TS_ASSERT(!strcmp(dst, expected_dst)); } 

Peringatan PVS-Studio: V595 CWE-476 Pointer 'dst' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 140, 143. test_secure_crt.h 140

Saya pikir kesalahan tidak memerlukan penjelasan. Peringatan serupa:

  • V595 CWE-476 Pointer 'dst' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 150, 153. test_secure_crt.h 150
  • V595 CWE-476 Pointer 'dst' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 314, 317. test_secure_crt.h 314

Kesalahan N12

 typedef int tbool; void MikkTSpace::setTSpace(...., const tbool bIsOrientationPreserving, ....) { .... m_NewVertices.push_back(bIsOrientationPreserving > 0.5 ? 1.0f : (-1.0f)); .... } 

V674 CWE-682 '0,5' literal dari tipe 'ganda' dibandingkan dengan nilai dari tipe 'int'. Pertimbangkan untuk memeriksa ekspresi 'bIsOrientationPreservasi> 0,5'. MikktspaceWrap.cpp 137

Tidak masuk akal untuk membandingkan variabel tipe int dengan konstanta 0,5. Selain itu, dari segi makna, ini umumnya merupakan variabel dari tipe boolean, yang berarti membandingkannya dengan 0,5 terlihat sangat aneh. Asumsikan bahwa alih-alih bIsOrientationPreserving variabel lain harus digunakan di sini.

Kesalahan N13

 virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr<u8>& fileContents, size_t size) { ScopedLock s; VfsDirectory* directory; VfsFile* file; Status st; st = vfs_Lookup(pathname, &m_rootDirectory, directory, &file, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE); // There is no such file, create it. if (st == ERR::VFS_FILE_NOT_FOUND) { s.~ScopedLock(); return CreateFile(pathname, fileContents, size); } .... } 

Peringatan PVS-Studio: V749 CWE-675 Destructor dari objek 's' akan dipanggil untuk kedua kalinya setelah meninggalkan ruang lingkup objek. vfs.cpp 165

Sebelum membuat file, diperlukan objek bertipe ScopedLock "membuka" objek. Untuk ini, sebuah destructor secara eksplisit disebut. Masalahnya adalah destruktor untuk objek s akan dipanggil secara otomatis lagi ketika fungsi keluar. Yaitu destructor akan dipanggil dua kali. Saya belum mempelajari perangkat kelas ScopedLock , tetapi dalam hal apa pun, Anda sebaiknya tidak melakukan ini. Seringkali, panggilan ganda kepada destruktor menyebabkan perilaku yang tidak terdefinisi atau kesalahan tidak menyenangkan lainnya. Bahkan jika kodenya berfungsi dengan baik sekarang, sangat mudah untuk memecahkan semuanya dengan mengubah implementasi kelas ScopedLock .

Kesalahan N14, N15, N16, N17

 CFsmEvent* CFsm::AddEvent( unsigned int eventType ) { .... pEvent = new CFsmEvent( eventType ); if ( !pEvent ) return NULL; .... } 

Peringatan PVS-Studio: V668 CWE-570 Tidak ada gunanya menguji pointer 'pEvent' terhadap null, karena memori dialokasikan menggunakan operator 'baru'. Pengecualian akan dihasilkan jika terjadi kesalahan alokasi memori. fsm.cpp 259

Memeriksa pointer tidak masuk akal, karena jika terjadi kesalahan alokasi memori, pengecualian std :: bad_alloc akan dibuang .

Jadi, pemeriksaannya berlebihan, tetapi kesalahannya tidak serius. Namun, semuanya jauh lebih buruk ketika beberapa logika dieksekusi di tubuh pernyataan if . Pertimbangkan kasus berikut:

 CFsmTransition* CFsm::AddTransition(....) { .... CFsmEvent* pEvent = AddEvent( eventType ); if ( !pEvent ) return NULL; // Create new transition CFsmTransition* pNewTransition = new CFsmTransition( state ); if ( !pNewTransition ) { delete pEvent; return NULL; } .... } 

Peringatan Analyzer: V668 CWE-570 Tidak ada gunanya menguji pointer 'pNewTransition' terhadap nol, karena memori dialokasikan menggunakan operator 'baru'. Pengecualian akan dihasilkan jika terjadi kesalahan alokasi memori. fsm.cpp 289

Suatu upaya dilakukan untuk membebaskan memori yang alamatnya disimpan dalam pointer pEvent . Secara alami, ini tidak akan terjadi dan kebocoran memori akan terjadi.

Bahkan, ketika saya mulai berurusan dengan kode ini, ternyata semuanya lebih rumit dan mungkin tidak ada satu kesalahan, tetapi dua. Sekarang saya akan menjelaskan apa yang salah dengan kode ini. Untuk melakukan ini, kita perlu membiasakan diri dengan perangkat fungsi AddEvent .

 CFsmEvent* CFsm::AddEvent( unsigned int eventType ) { CFsmEvent* pEvent = NULL; // Lookup event by type EventMap::iterator it = m_Events.find( eventType ); if ( it != m_Events.end() ) { pEvent = it->second; } else { pEvent = new CFsmEvent( eventType ); if ( !pEvent ) return NULL; // Store new event into internal map m_Events[ eventType ] = pEvent; } return pEvent; } 

Perhatikan bahwa fungsi tidak selalu mengembalikan pointer ke objek baru yang dibuat menggunakan operator baru . Terkadang mengambil objek yang sudah ada dari wadah m_Events . Dan penunjuk ke objek yang baru dibuat, omong-omong, juga akan ditempatkan di m_Events .

Dan di sini muncul pertanyaan: siapa yang memiliki dan harus menghancurkan benda-benda yang petunjuknya disimpan dalam wadah m_Events ? Saya tidak terbiasa dengan proyek, tetapi kemungkinan besar di suatu tempat ada kode yang menghancurkan semua objek ini. Kemudian menghapus objek di dalam fungsi CFsm :: AddTransition umumnya berlebihan.

Saya mendapat kesan bahwa Anda cukup menghapus fragmen kode berikut:

 if ( !pNewTransition ) { delete pEvent; return NULL; } 

Kesalahan lain:

  • V668 CWE-571 Tidak ada gunanya menguji pointer 'ret' terhadap null, karena memori dialokasikan menggunakan operator 'baru'. Pengecualian akan dihasilkan jika terjadi kesalahan alokasi memori. TerrainTextureEntry.cpp 120
  • V668 CWE-571 Tidak ada gunanya menguji pointer 'answer' terhadap null, karena memori dialokasikan menggunakan operator 'baru'. Pengecualian akan dihasilkan jika terjadi kesalahan alokasi memori. SoundManager.cpp 542

Kesalahan N18, N19

 static void dir_scan_callback(struct de *de, void *data) { struct dir_scan_data *dsd = (struct dir_scan_data *) data; if (dsd->entries == NULL || dsd->num_entries >= dsd->arr_size) { dsd->arr_size *= 2; dsd->entries = (struct de *) realloc(dsd->entries, dsd->arr_size * sizeof(dsd->entries[0])); } if (dsd->entries == NULL) { // TODO(lsm): propagate an error to the caller dsd->num_entries = 0; } else { dsd->entries[dsd->num_entries].file_name = mg_strdup(de->file_name); dsd->entries[dsd->num_entries].st = de->st; dsd->entries[dsd->num_entries].conn = de->conn; dsd->num_entries++; } } 

Peringatan PVS-Studio: V701 CWE-401 realloc () kemungkinan kebocoran: ketika realloc () gagal mengalokasikan memori, pointer asli 'dsd-> entri' hilang. Pertimbangkan untuk menetapkan realloc () ke pointer sementara. mongoose.cpp 2462

Jika ukuran array menjadi tidak mencukupi, maka memori dialokasikan menggunakan fungsi realloc . Kesalahannya adalah bahwa nilai pointer ke blok memori asli segera ditimpa dengan nilai baru yang dikembalikan oleh fungsi realokasi .

Jika alokasi memori gagal, fungsi realloc akan mengembalikan NULL, dan NULL ini akan ditulis ke variabel entri dsd-> . Setelah itu tidak mungkin untuk membebaskan blok memori yang alamatnya sebelumnya disimpan dalam entri dsd-> . Kebocoran memori akan terjadi.

Kesalahan lain: V701 CWE-401 realloc () kemungkinan kebocoran: ketika realloc () gagal mengalokasikan memori, penunjuk asli 'Buffer' hilang. Pertimbangkan untuk menetapkan realloc () ke pointer sementara. Preprocessor.cpp 84

Kesimpulan


Saya tidak bisa mengatakan bahwa kali ini artikelnya ternyata menarik, atau saya berhasil menunjukkan banyak kesalahan besar. Apa yang harus dilakukan, sekali waktu tidak perlu. Apa yang saya lihat, kemudian saya menulis .

Terima kasih atas perhatian anda Untuk perubahan, saya akan mengakhiri artikel dengan undangan untuk mengikuti saya di Instagram @pvs_studio_unicorn dan di Twitter @Code_Analysis .



Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Andrey Karpov. Kerja bagus, penulis game 0 AD!

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


All Articles