Satu tahun lagi akan berakhir, jadi sekarang saatnya untuk membuat kopi dan membaca kembali laporan bug selama setahun terakhir. Tentu saja, ini akan memakan banyak waktu, jadi artikel ini ditulis. Saya sarankan untuk melihat tempat-tempat gelap paling menarik dari proyek yang kami temui pada 2019 dalam proyek yang ditulis dalam C dan C ++.
Tempat kesepuluh: "Apa OS kita?"
V1040 Kemungkinan salah ketik dalam ejaan nama makro yang telah ditentukan sebelumnya. Makro '__MINGW32_' mirip dengan '__MINGW32__'. winapi.h 4112
#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif
Di sini, kesalahan ketik dibuat dengan nama makro
__MINGW32 _ (MINGW32 menyatakan __MINGW32__). Di tempat lain proyek, verifikasi ditulis dengan benar:
Ini, kebetulan, bukan hanya kesalahan pertama dalam artikel "
CMake: kasus ketika proyek tidak dapat dimaafkan untuk kualitas kodenya ", tetapi secara umum kesalahan nyata pertama yang ditemukan oleh diagnostik V1040 dalam proyek terbuka nyata (19 Agustus 2019).
Tempat kesembilan: "Siapa yang pertama?"
V502 Mungkin
operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '=='. mir_parser.cpp 884
enum Opcode : uint8 { kOpUndef, .... OP_intrinsiccall, OP_intrinsiccallassigned, .... kOpLast, }; bool MIRParser::ParseStmtIntrinsiccall(StmtNodePtr &stmt, bool isAssigned) { Opcode o = !isAssigned ? (....) : (....); auto *intrnCallNode = mod.CurFuncCodeMemPool()->New<IntrinsiccallNode>(....); lexer.NextToken(); if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { intrnCallNode->SetIntrinsic(GetIntrinsicID(lexer.GetTokenKind())); } else { intrnCallNode->SetIntrinsic(static_cast<MIRIntrinsicID>(....)); } .... }
Kami tertarik pada bagian berikut dari kode ini:
if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... }
Operator '==' memiliki prioritas lebih tinggi daripada operator ternary (? :). Karena itu, ekspresi kondisional tidak dievaluasi dengan benar. Kode tertulis setara dengan yang berikut:
if ((o == !isAssigned) ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... }
Dan dengan mempertimbangkan fakta bahwa konstanta
OP_intrinsiccall dan
OP_intrinsiccallassignedignign memiliki nilai bukan nol, kondisi ini selalu mengembalikan nilai sebenarnya. Tubuh cabang
lain adalah kode yang tidak dapat dijangkau.
Kesalahan ini datang ke atas kami dari artikel "
Memeriksa kode Kompiler Ark baru-baru ini dibuka oleh Huawei ."
Tempat kedelapan: "Bahaya operasi bitwise"
V1046 Penggunaan bool 'dan' int 'yang tidak aman secara bersamaan dalam operasi' & = '. GSLMultiRootFinder.h 175
int AddFunction(const ROOT::Math::IMultiGenFunction & func) { ROOT::Math::IMultiGenFunction * f = func.Clone(); if (!f) return 0; fFunctions.push_back(f); return fFunctions.size(); } template<class FuncIterator> bool SetFunctionList( FuncIterator begin, FuncIterator end) { bool ret = true; for (FuncIterator itr = begin; itr != end; ++itr) { const ROOT::Math::IMultiGenFunction * f = *itr; ret &= AddFunction(*f); } return ret; }
Berdasarkan kode, fungsi
SetFunctionList diharapkan untuk mem-bypass daftar iterator. Dan jika setidaknya salah satu dari mereka tidak valid, maka nilai pengembalian akan
salah , jika tidak
benar .
Namun, pada kenyataannya, fungsi
SetFunctionList dapat mengembalikan
false bahkan untuk iterator yang valid. Mari kita
lihat situasinya. Fungsi
AddFunction mengembalikan jumlah iterator yang valid dalam daftar
fFunctions . Yaitu saat menambahkan iterator bukan nol, ukuran daftar ini akan meningkat secara berurutan: 1, 2, 3, 4, dll. Di sinilah kesalahan dalam kode mulai terwujud:
ret &= AddFunction(*f);
Karena Karena fungsi mengembalikan hasil dari tipe
int , bukan
bool , operasi '& =' dengan angka genap akan memberikan nilai
salah . Lagi pula, bit angka genap paling tidak signifikan akan selalu nol. Oleh karena itu, kesalahan yang tidak terlihat seperti itu akan merusak hasil fungsi
SetFunctionsList bahkan untuk argumen yang valid.
Jika Anda hati-hati membaca kode dari contoh (dan Anda membaca dengan cermat, kan?), Maka Anda mungkin memperhatikan bahwa ini adalah kode dari proyek ROOT. Tentu saja, kami mengujinya: "
Analisis kode ROOT - kerangka kerja untuk analisis data penelitian ilmiah ."
Tempat ketujuh: "Kebingungan dalam variabel"
V1001 [CWE-563] Variabel 'Mode' ditugaskan tetapi tidak digunakan pada akhir fungsi. SIModeRegister.cpp 48
struct Status { unsigned Mask; unsigned Mode; Status() : Mask(0), Mode(0){}; Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { Mode &= Mask; }; .... };
Sangat berbahaya untuk memberi argumen fungsi nama yang sama dengan anggota kelas. Sangat mudah bingung. Di hadapan kita adalah kasus seperti itu. Ungkapan ini tidak masuk akal:
Mode &= Mask;
Argumen fungsi berubah. Dan itu dia. Argumen ini tidak lagi digunakan. Kemungkinan besar, perlu menulis seperti ini:
Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { this->Mode &= Mask; };
Dan ini adalah kesalahan dari
LLVM . Kami memiliki tradisi dari waktu ke waktu untuk menganalisis proyek ini. Tahun ini kami juga memiliki
artikel tentang verifikasi.
Tempat keenam: "C ++ memiliki undang-undang sendiri"
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?
V709 Perbandingan mencurigakan ditemukan: 'f0 == f1 == m_fractureBodies.size ()'. Ingat bahwa 'a == b == c' tidak sama dengan 'a == b && b == c'. btFractureDynamicsWorld.cpp 483
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; .... } .... }
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 cek untuk melihat apakah
f0 dan
f1 sama, dan kemudian menjadi cek untuk melihat apakah
m_fractureBodies.size () sama dengan hasil
f0 == f1 . Akibatnya, operan ketiga di sini dibandingkan dengan 0 atau 1.
Kesalahan indah! Dan, untungnya, sangat jarang. Sejauh ini kami hanya
bertemu dengannya di tiga proyek terbuka, dan, yang menarik, semuanya hanyalah mesin game. Omong-omong, ini bukan satu-satunya kesalahan yang kami temukan di Bullet. Yang paling menarik masuk ke artikel kami "
PVS-Studio melihat ke dalam Red Dead Redemption - Bullet engine ".
Tempat kelima: "Apa akhir dari garis?"
Kesalahan berikut mudah terdeteksi jika Anda tahu tentang satu kehalusan.
V739 EOF tidak boleh dibandingkan dengan nilai tipe 'char'. 'Ch' harus dari tipe 'int'. json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
Ini adalah salah satu kesalahan yang sulit untuk diketahui jika Anda tidak tahu bahwa
EOF didefinisikan sebagai -1. Karenanya, jika Anda mencoba membandingkannya dengan variabel tipe
char yang ditandatangani , kondisinya hampir selalu
salah . Satu-satunya pengecualian adalah jika kode karakter adalah 0xFF (255). Ketika membandingkan simbol seperti itu akan berubah menjadi -1 dan kondisinya akan benar.
Ada banyak kesalahan terkait dengan permainan di atas ini: dari mesin ke permainan terbuka. Seperti yang Anda duga, tempat ini juga datang kepada kami dari daerah ini. Anda dapat melihat lebih banyak kesalahan dalam artikel "
Cataclysm Dark Days Menjelang, Analisis Statis, dan Bagel ."
Tempat keempat: "Keajaiban Angka Pi"
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.
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, misalnya, ada konstanta M_PI dari header math.h.
Kesalahan ini dari artikel "
PVS-Studio melihat ke dalam Red Dead Redemption - Bullet engine ", yang sudah tidak asing bagi kita di tempat keenam. Jika Anda belum menundanya untuk nanti, maka ini adalah kesempatan terakhir.
Sedikit penyimpangan
Jadi kita dekat dengan tiga kesalahan paling menarik teratas. Seperti yang mungkin Anda perhatikan, mereka tidak diurutkan berdasarkan konsekuensi bencana dari kehadiran mereka, tetapi oleh kompleksitas deteksi. Bagaimanapun, pada akhirnya, keuntungan paling penting dari analisis statis atas review kode adalah bahwa mesin tidak lelah dan tidak melupakan apa pun. :)
Dan sekarang saya perhatikan tiga yang pertama.
Tempat ketiga: "Pengecualian Sulit dipahami"
Kelas
V702 harus selalu diturunkan dari std :: exception (dan yang sama) sebagai 'publik' (tidak ada kata kunci yang ditentukan, jadi kompiler secara default ke 'private'). CalcManager CalcException.h 4
class CalcException : std::exception { public: CalcException(HRESULT hr) { m_hr = hr; } HRESULT GetException() { return m_hr; } private: HRESULT m_hr; };
Analyzer mendeteksi kelas yang diwarisi dari kelas
std :: exception melalui pengubah
pribadi (pengubah default jika tidak ada yang ditentukan). Masalah dengan kode ini adalah ketika Anda mencoba menangkap pengecualian umum
std :: exception, pengecualian tipe
CalcException akan dilewati. Perilaku ini terjadi karena warisan pribadi menghalangi konversi tipe implisit.
Ya, saya tidak ingin melihat program macet karena pengubah
publik yang hilang
. Ngomong-ngomong, saya yakin Anda pasti menggunakan aplikasi itu dalam hidup Anda setidaknya satu kali, kode sumber yang baru saja kita lihat. Ini adalah
kalkulator Windows standar lama yang baik yang juga kami
uji .
Tempat Kedua: Tag HTML Tidak Tertutup
V735 Mungkin HTML yang salah. Tag penutup "</body>" ditemukan, sedangkan tag "</div>" diharapkan. book.cpp 127
static QString makeAlgebraLogBaseConversionPage() { return BEGIN INDEX_LINK TITLE(Book::tr("Logarithmic Base Conversion")) FORMULA(y = log(x) / log(a), log<sub>a</sub>x = log(x) / log(a)) END; }
Seperti yang sering terjadi dengan kode C / C ++, tidak ada yang jelas dari sumbernya, jadi mari kita beralih ke kode yang sudah diproses untuk fragmen ini:
Penganalisis mendeteksi
tag <div> yang tidak tertutup. Ada banyak fragmen kode html dalam file ini dan sekarang harus diperiksa tambahan oleh pengembang.
Terkejut bahwa kita dapat memeriksa dan semacamnya? Ketika saya pertama kali melihat ini, saya terkesan. Jadi kami menganalisis sedikit kode html. Benar, hanya dalam kode C ++. :)
Ini bukan hanya tempat kedua, tetapi juga kalkulator kedua di atas kami. Anda dapat menemukan daftar semua kesalahan dalam artikel "
Mengikuti jejak kalkulator: SpeedCrunch ".
Tempat Pertama: "Fitur Standar yang Sulit dipahami"
Jadi kami sampai di tempat pertama. Masalah aneh yang mengesankan yang melalui tinjauan kode.
Cobalah untuk menemukannya sendiri:
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
Sekarang mari kita lihat apa yang disumpah oleh penganalisa:
V560 Bagian dari ekspresi kondisional selalu benar: ('\ n'! = C). params.c 136.
Aneh bukan? Mari kita lihat sesuatu yang menarik dalam proyek yang sama, tetapi dalam file yang berbeda (charset.h):
#ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t')
Jadi, dan ini sudah benar-benar aneh ... Ternyata jika variabel
c sama dengan
'\ n', maka fungsi
isspace (c) yang benar-benar tidak berbahaya akan mengembalikan false dan bagian kedua dari tes ini tidak akan dilakukan karena evaluasi hubung singkat. Jika
isspace (c) dijalankan, maka variabel
c adalah
'' atau
'\ t', dan ini jelas tidak sama dengan
'\ n' .
Tentu saja, Anda dapat mengatakan bahwa makro ini seperti
#define true false , dan kode seperti itu tidak akan pernah melewati tinjauan kode. Namun, kode ini lulus ulasan dan menunggu kami dengan aman di repositori proyek.
Jika Anda memerlukan analisis kesalahan yang lebih terperinci, maka ada di artikel "
Bagi mereka yang ingin bermain sebagai detektif: temukan kesalahan dalam fungsi dari Midnight Commander ".
Kesimpulan
Selama setahun terakhir, kami telah menemukan banyak kesalahan. Ini adalah kesalahan salin-tempel yang biasa, kesalahan dalam konstanta, tag tidak tertutup, dan banyak masalah lainnya. Tetapi penganalisa kami semakin membaik dan
belajar untuk mencari lebih banyak bug, jadi ini masih jauh dari akhir, dan artikel baru tentang proyek pengecekan akan diterbitkan sesering sebelumnya.
Jika seseorang membaca artikel kami untuk pertama kalinya, untuk berjaga-jaga, saya akan mengklarifikasi bahwa semua kesalahan ini ditemukan menggunakan penganalisa kode statis PVS-Studio, yang kami sarankan
unduh dan coba. Penganalisis dapat mendeteksi kesalahan dalam kode program yang ditulis dalam bahasa: C, C ++, C # dan Java.
Jadi kita telah sampai pada akhirnya! Jika Anda melewatkan dua level pertama, maka saya sarankan untuk tidak melewatkan kesempatan dan melewati mereka bersama kami:
C # dan
Java .

Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Maxim Zvyagintsev.
10 Bug Top Ditemukan di Proyek C ++ pada 2019 .