CMake adalah sistem lintas platform untuk mengotomatisasi pembuatan proyek. Sistem ini jauh lebih tua daripada penganalisis kode statis PVS-Studio, tetapi tidak ada yang mencoba menerapkan penganalisa pada kodenya dan meninjau kesalahannya. Ternyata, ada banyak dari mereka. Audiens CMake sangat besar. Proyek baru dimulai dan yang lama porting. Saya ngeri memikirkan berapa banyak pengembang yang bisa mendapatkan kesalahan.
Pendahuluan
CMake adalah sistem lintas platform untuk mengotomatisasi pembuatan perangkat lunak dari kode sumber. CMake tidak dimaksudkan secara langsung untuk membangun, itu hanya menghasilkan file untuk mengontrol membangun dari file CMakeLists.txt. Rilis pertama dari program ini terjadi pada tahun 2000. Sebagai perbandingan, analisa PVS-Studio hanya muncul pada tahun 2008. Pada saat itu, program ini bertujuan untuk mencari bug yang dihasilkan dari porting sistem 32-bit ke sistem 64-bit. Pada 2010, set pertama tujuan umum diagnostik muncul (V501-
V545 ). Omong-omong, kode CMake memiliki beberapa peringatan dari set pertama ini.
Kesalahan yang tidak bisa dimaafkan
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
Diagnostik
V1040 diimplementasikan belum lama ini. Kemungkinan besar, pada saat memposting artikel, artikel itu belum dirilis, namun, kami sudah menemukan kesalahan keren dengan bantuannya.
Ada kesalahan ketik yang dibuat atas nama
__MINGW32_ . Pada akhirnya, satu karakter garis bawah hilang. Jika Anda mencari kode dengan nama ini, Anda dapat melihat bahwa versi dengan dua karakter garis bawah di kedua sisi digunakan dalam proyek:
V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 558
bool IsVisualStudioMacrosFileRegistered(const std::string& macrosFile, const std::string& regKeyBase, std::string& nextAvailableSubKeyName) { .... if (ERROR_SUCCESS == result) { wchar_t subkeyname[256];
Untuk array yang dinyatakan secara statis,
sizeof operator akan menghitung ukuran dalam byte, dengan mempertimbangkan jumlah elemen dan ukurannya. Saat mengevaluasi nilai variabel
cch_subkeyname , pengembang tidak memperhitungkannya dan mendapat nilai 4 kali lebih besar dari yang dimaksudkan. Mari kita jelaskan dari mana "empat kali" berasal.
Array dan ukurannya yang salah diteruskan ke fungsi
RegEnumKeyExW :
LSTATUS RegEnumKeyExW( HKEY hKey, DWORD dwIndex, LPWSTR lpName,
Pointer
lpcchName harus mengarah ke variabel, berisi ukuran buffer dalam karakter: "Sebuah pointer ke variabel yang menentukan ukuran buffer yang ditentukan oleh parameter
lpClass , dalam karakter". Ukuran array
subkeyname adalah 512 byte dan dapat menyimpan 256 karakter dari tipe
wchar_t (di Windows, wchar_t adalah 2 byte). Ini adalah 256 yang harus diteruskan ke fungsi. Sebaliknya, 512 dikalikan dengan 2 dan kami mendapatkan 1024.
Saya pikir, sekarang sudah jelas bagaimana cara memperbaiki kesalahan ini. Anda perlu menggunakan pembagian alih-alih perkalian:
DWORD cch_subkeyname = sizeof(subkeyname) / sizeof(subkeyname[0]);
Ngomong-ngomong, kesalahan yang sama terjadi ketika mengevaluasi nilai variabel
cch_keyclass .
Kesalahan yang dijelaskan berpotensi menyebabkan buffer overflow. Semua fragmen seperti itu pasti harus diperbaiki:
- V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 556
- V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 572
- V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 621
- V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 622
- V531 Aneh bahwa operator sizeof () dikalikan dengan sizeof (). cmGlobalVisualStudioGenerator.cxx 649
V595 Pointer 'this-> BuildFileStream' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 133, 134. cmMakefileTargetGenerator.cxx 133
void cmMakefileTargetGenerator::CreateRuleFile() { .... this->BuildFileStream->SetCopyIfDifferent(true); if (!this->BuildFileStream) { return; } .... }
Pointer
ini-> BuildFileStream dereferenced tepat sebelum memeriksa validitasnya. Bukankah itu menimbulkan masalah bagi siapa pun? Di bawah ini ada contoh lain dari cuplikan tersebut. Itu dibuat seperti salinan karbon. Tetapi pada kenyataannya, ada banyak peringatan
V595 dan kebanyakan dari mereka tidak begitu jelas. Dari pengalaman saya, saya dapat mengatakan bahwa mengoreksi peringatan diagnostik ini memakan waktu paling lama.
- V595 Pointer 'this-> FlagFileStream' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 303, 304. cmMakefileTargetGenerator.cxx 303
V614 Uninitialized pointer 'str' digunakan. cmVSSetupHelper.h 80
class SmartBSTR { public: SmartBSTR() { str = NULL; } SmartBSTR(const SmartBSTR& src) { if (src.str != NULL) { str = ::SysAllocStringByteLen((char*)str, ::SysStringByteLen(str)); } else { str = ::SysAllocStringByteLen(NULL, 0); } } .... private: BSTR str; };
Analyzer mendeteksi penggunaan penunjuk
str yang tidak diinisialisasi. Itu muncul karena kesalahan ketik biasa. Saat memanggil fungsi
SysAllocStringByteLen , seseorang harus menggunakan pointer
src.str .
V557 Array overrun dimungkinkan. Nilai indeks 'lensymbol' bisa mencapai 28. archive_read_support_format_rar.c 2749
static int64_t expand(struct archive_read *a, int64_t end) { .... if ((lensymbol = read_next_symbol(a, &rar->lengthcode)) < 0) goto bad_data; if (lensymbol > (int)(sizeof(lengthbases)/sizeof(lengthbases[0]))) goto bad_data; if (lensymbol > (int)(sizeof(lengthbits)/sizeof(lengthbits[0]))) goto bad_data; len = lengthbases[lensymbol] + 2; if (lengthbits[lensymbol] > 0) { if (!rar_br_read_ahead(a, br, lengthbits[lensymbol])) goto truncated_data; len += rar_br_bits(br, lengthbits[lensymbol]); rar_br_consume(br, lengthbits[lensymbol]); } .... }
Bagian kode ini menyembunyikan beberapa masalah sekaligus. Saat mengakses array
basis panjang dan
panjang , indeks array mungkin keluar dari batas, karena pengembang menulis operator '>' alih-alih '> =' di atas. Pemeriksaan ini mulai kehilangan satu nilai yang tidak dapat diterima. Di sini kita tidak memiliki apa pun kecuali pola kesalahan klasik yang disebut Kesalahan
Off-by-one .
Inilah seluruh daftar operasi akses array oleh indeks yang tidak valid:
- V557 Array overrun dimungkinkan. Nilai indeks 'lensymbol' bisa mencapai 28. archive_read_support_format_rar.c 2750
- V557 Array overrun dimungkinkan. Nilai indeks 'lensymbol' bisa mencapai 28. archive_read_support_format_rar.c 2751
- V557 Array overrun dimungkinkan. Nilai indeks 'lensymbol' bisa mencapai 28. archive_read_support_format_rar.c 2753
- V557 Array overrun dimungkinkan. Nilai indeks 'lensymbol' bisa mencapai 28. archive_read_support_format_rar.c 2754
- V557 Array overrun dimungkinkan. Nilai indeks 'offssymbol' bisa mencapai 60. archive_read_support_format_rar.c 2797
Kebocoran memori
V773 Fungsi itu keluar tanpa melepaskan pointer 'testRun'. Kebocoran memori dimungkinkan. cmCTestMultiProcessHandler.cxx 193
void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner, bool started) { .... delete runner; if (started) { this->StartNextTests(); } } bool cmCTestMultiProcessHandler::StartTestProcess(int test) { .... cmCTestRunTest* testRun = new cmCTestRunTest(*this);
Analyzer mendeteksi kebocoran memori. Memori oleh pointer
testRun tidak dirilis, jika fungsi
testRun-> StartTest mengembalikan
true . Ketika menjalankan cabang kode lain, memori ini akan dirilis dalam fungsi
ini-> FinishTestProcess.Kebocoran sumber daya
V773 Fungsi ini keluar tanpa menutup file yang direferensikan oleh pegangan 'fd'. Kebocoran sumber daya mungkin terjadi. rhash.c 450
RHASH_API int rhash_file(....) { FILE* fd; rhash ctx; int res; hash_id &= RHASH_ALL_HASHES; if (hash_id == 0) { errno = EINVAL; return -1; } if ((fd = fopen(filepath, "rb")) == NULL) return -1; if ((ctx = rhash_init(hash_id)) == NULL) return -1;
Logika Aneh dalam Kondisi
V590 Pertimbangkan untuk memeriksa
ekspresi '* s! =' \ 0 '&& * s ==' ''. Ekspresi berlebihan atau mengandung kesalahan cetak. archive_cmdline.c 76
static ssize_t get_argument(struct archive_string *as, const char *p) { const char *s = p; archive_string_empty(as); while (*s != '\0' && *s == ' ') s++; .... }
* Perbandingan karakter dengan null adalah mubazir. Kondisi loop
sementara hanya bergantung pada apakah karakter sama dengan spasi atau tidak. Ini bukan kesalahan, tetapi komplikasi kode yang tidak perlu.
V592 Ekspresi tertutup oleh tanda kurung dua kali: ((ekspresi)). Sepasang tanda kurung tidak perlu atau salah cetak ada. cmCTestTestHandler.cxx 899
void cmCTestTestHandler::ComputeTestListForRerunFailed() { this->ExpandTestsToRunInformationForRerunFailed(); ListOfTests finalList; int cnt = 0; for (cmCTestTestProperties& tp : this->TestList) { cnt++;
Penganalisa memperingatkan bahwa operasi negasi mungkin harus dikeluarkan dari kurung. Tampaknya tidak ada bug di sini - hanya kurung ganda yang tidak perlu. Tetapi kemungkinan besar, ada kesalahan logika dalam kode.
Operator
lanjutan dieksekusi hanya dalam kasus jika daftar tes
ini-> TestsToRun tidak kosong dan
cnt tidak ada di dalamnya. Masuk akal untuk mengasumsikan bahwa jika daftar tes kosong, tindakan yang sama perlu dilakukan. Kemungkinan besar, kondisinya adalah sebagai berikut:
if (this->TestsToRun.empty() || std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) == this->TestsToRun.end()) { continue; }
V592 Ekspresi tertutup oleh tanda kurung dua kali: ((ekspresi)). Sepasang tanda kurung tidak perlu atau salah cetak ada. cmMessageCommand.cxx 73
bool cmMessageCommand::InitialPass(std::vector<std::string> const& args, cmExecutionStatus&) { .... } else if (*i == "DEPRECATION") { if (this->Makefile->IsOn("CMAKE_ERROR_DEPRECATED")) { fatal = true; type = MessageType::DEPRECATION_ERROR; level = cmake::LogLevel::LOG_ERROR; } else if ((!this->Makefile->IsSet("CMAKE_WARN_DEPRECATED") || this->Makefile->IsOn("CMAKE_WARN_DEPRECATED"))) { type = MessageType::DEPRECATION_WARNING; level = cmake::LogLevel::LOG_WARNING; } else { return true; } ++i; } .... }
Ini adalah contoh yang serupa, tetapi kali ini saya lebih yakin bahwa kesalahan terjadi. Fungsi
IsSet ("CMAKE_WARN_DEPRECATED") memeriksa bahwa nilai
CMAKE_WARN_DEPRECATED diatur secara global, dan fungsi
IsOn ("CMAKE_WARN_DEPRECATED) memeriksa bahwa nilai tersebut diatur dalam konfigurasi proyek. Kemungkinan besar, operator komplementer itu berlebihan, karena dalam kedua kasus, benar untuk menetapkan nilai
tipe dan
level yang sama.V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. cmCTestRunTest.cxx 151
bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started) { .... } else if ((success && !this->TestProperties->WillFail) || (!success && this->TestProperties->WillFail)) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; } .... }
Kode ini bisa lebih sederhana. Seseorang dapat menulis ulang ekspresi kondisional dengan cara berikut:
} else if (success != this->TestProperties->WillFail) { this->TestResult.Status = cmCTestTestHandler::COMPLETED; outputStream << " Passed "; }
Beberapa tempat lagi untuk disederhanakan:
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A && B) || Ekspresi (! A &&! B) 'sama dengan ekspresi' bool (A) == bool (B) '. cmCTestTestHandler.cxx 702
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. digest_sspi.c 443
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. tcp.c 1295
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. testDynamicLoader.cxx 58
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. testDynamicLoader.cxx 65
- V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. testDynamicLoader.cxx 72
Berbagai peringatan
V523 Pernyataan 'maka' setara dengan fragmen kode berikutnya. archive_read_support_format_ar.c 415
static int _ar_read_header(struct archive_read *a, struct archive_entry *entry, struct ar *ar, const char *h, size_t *unconsumed) { .... if (strcmp(filename, "__.SYMDEF") == 0) { archive_entry_copy_pathname(entry, filename); return (ar_parse_common_header(ar, entry, h)); } archive_entry_copy_pathname(entry, filename); return (ar_parse_common_header(ar, entry, h)); }
Ekspresi dalam kondisi terakhir mirip dengan dua baris terakhir dari fungsi. Pengembang dapat menyederhanakan kode ini dengan menghapus ketentuan, atau ada kesalahan dalam kode dan harus diperbaiki.
V535 Variabel 'i' digunakan untuk loop ini dan untuk loop luar. Periksa baris: 2220, 2241. multi.c 2241
static CURLMcode singlesocket(struct Curl_multi *multi, struct Curl_easy *data) { .... for(i = 0; (i< MAX_SOCKSPEREASYHANDLE) &&
Variabel
i digunakan sebagai penghitung lingkaran di loop luar dan dalam. Pada saat yang sama, nilai penghitung kembali dimulai dari nol di loop dalam. Mungkin ini bukan bug di sini, tetapi kodenya mencurigakan.
V519 Variabel 'tagString' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 84, 86. cmCPackLog.cxx 86
void cmCPackLog::Log(int tag, const char* file, int line, const char* msg, size_t length) { .... if (tag & LOG_OUTPUT) { output = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "VERBOSE"; } } if (tag & LOG_WARNING) { warning = true; display = true; if (needTagString) { if (!tagString.empty()) { tagString += ","; } tagString = "WARNING"; } } .... }
Variabel
tagString ditimpa dengan nilai baru di semua tempat. Sulit mengatakan apa masalahnya atau mengapa mereka melakukannya. Mungkin, operator '=' dan '+ =' kacau.
Seluruh daftar tempat-tempat tersebut:
- V519 Variabel 'tagString' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 94, 96. cmCPackLog.cxx 96
- V519 Variabel 'tagString' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 104, 106. cmCPackLog.cxx 106
- V519 Variabel 'tagString' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 114, 116. cmCPackLog.cxx 116
- V519 Variabel 'tagString' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 125, 127. cmCPackLog.cxx 127
V519 Variabel 'aes-> aes_set' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 4052, 4054. archive_string.c 4054
int archive_mstring_copy_utf8(struct archive_mstring *aes, const char *utf8) { if (utf8 == NULL) { aes->aes_set = 0;
Pengaturan paksa nilai
AES_SET_UTF8 terlihat mencurigakan. Saya pikir kode seperti itu akan membingungkan pengembang mana pun, yang datang untuk memperbaiki fragmen ini.
Kode ini disalin ke tempat lain:
- V519 Variabel 'aes-> aes_set' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 4066, 4068. archive_string.c 4068
Cara Menemukan Bug dalam Proyek di CMake
Pada bagian ini, saya akan secara singkat memberi tahu Anda cara memeriksa proyek-proyek CMake dengan PVS-Studio semudah satu-dua-tiga.
Windows / Visual StudioUntuk Visual Studio, Anda dapat membuat file proyek menggunakan CMake GUI atau perintah berikut:
cmake -G "Visual Studio 15 2017 Win64" ..
Selanjutnya, Anda dapat membuka file .sln dan memeriksa proyek menggunakan
plugin untuk Visual Studio.
Linux / macOSFile compile_commands.json digunakan untuk memeriksa sistem ini. Omong-omong, itu dapat dihasilkan dalam sistem pembangunan yang berbeda. Ini adalah bagaimana Anda melakukannya di CMake:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..
Hal terakhir yang harus dilakukan adalah menjalankan alat analisa dalam direktori dengan file .json:
pvs-studio-analyzer analyze -l /path/to/PVS-Studio.lic -o /path/to/project.log -e /path/to/exclude-path -j<N>
Kami juga telah mengembangkan modul untuk proyek-proyek CMake. Beberapa orang suka menggunakannya. Modul CMake dan contoh penggunaannya dapat ditemukan di repositori kami di GitHub:
pvs-studio-cmake-example .
Kesimpulan
Sejumlah besar pengguna CMake hebat untuk menguji proyek, tetapi banyak masalah dapat dicegah sebelum rilis dengan menggunakan alat analisis kode statis, seperti
PVS-Studio .
Jika Anda menyukai hasil analisis, tetapi proyek Anda tidak ditulis dalam C dan C ++, saya ingin mengingatkan bahwa analis juga mendukung analisis proyek di C # dan Java. Anda dapat menguji penganalisa pada proyek Anda dengan membuka
halaman ini.