Untuk melengkapi daftar bahasa pemrograman open source yang diuji menggunakan analisa kode statis PVS-Studio, Perl 5. dipilih. Artikel ini adalah tentang kesalahan yang ditemukan dan kesulitan dalam melihat hasil analisis. Jumlah makro dalam kode begitu besar sehingga tampaknya kode itu tidak ditulis dalam C, tetapi dalam beberapa dialek aneh itu. Meskipun kesulitan saat melihat kode, saya berhasil mengumpulkan masalah menarik, yang akan dibahas dalam artikel ini.
Pendahuluan
Perl adalah bahasa pemrograman tujuan umum dinamis tingkat tinggi yang diartikan (Perl adalah keluarga dari dua bahasa pemrograman tingkat tinggi, tujuan umum, ditafsirkan, dinamis). Perl 5 diluncurkan pada tahun 1994. Setelah beberapa dekade, kode C dengan banyak makro menyebabkan kegugupan bagi programmer modern.
Kode sumber untuk Perl 5 diambil dari
repositori resmi (cabang
blead ). Untuk memeriksa proyek, kami menggunakan penganalisa kode statis
PVS-Studio . Analisis dilakukan pada sistem operasi Linux, tetapi penganalisa juga tersedia untuk Windows dan macOS.
Melihat hasil analisis bukanlah tugas yang mudah. Faktanya adalah bahwa penganalisa memeriksa file
.i preprocessed , di mana semua arahan preprocessor sudah dibuka, dan menghasilkan peringatan pada file dengan kode sumber. Ini adalah perilaku penganalisa yang benar, Anda tidak perlu mengubah apa pun, tetapi banyak peringatan dikeluarkan untuk makro! Dan di belakang makro adalah kode yang tidak dapat dibaca.
Operator ternary tidak berfungsi seperti yang Anda pikirkan
V502 Mungkin
operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '-'. toke.c 9494
STATIC char * S_scan_ident(pTHX_ char *s, char *dest, STRLEN destlen, I32 ck_uni) { .... if ((s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1) && VALID_LEN_ONE_IDENT(s, PL_bufend, is_utf8)) { .... } .... }
Mari kita mulai ulasan dengan kesalahan yang indah. Setiap beberapa ulasan kode harus mengulangi bahwa operator ternary hampir memiliki prioritas terendah dalam komputasi.
Pertimbangkan fragmen dengan kesalahan:
s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1
Urutan operasi yang diharapkan oleh programmer:
- ?:
- -
- <=
Apa yang sebenarnya terjadi:
- -
- <=
- ?:
Simpan label dengan prioritas operasi: "
Prioritas operasi di C / C ++ ."
V502 Mungkin
operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '=='. re_exec.c 9193
STATIC I32 S_regrepeat(pTHX_ regexp *prog, char **startposp, const regnode *p, regmatch_info *const reginfo, I32 max _pDEPTH) { .... assert(STR_LEN(p) == reginfo->is_utf8_pat ? UTF8SKIP(STRING(p)) : 1); .... }
Kode sederhana dengan kesalahan serupa. Tetapi jika Anda tidak tahu prioritas operasi, maka Anda dapat membuat kesalahan dalam ekspresi ukuran berapa pun.
Tempat lain dengan menegaskan:
- V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '=='. re_exec.c 9286
V502 Mungkin
operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. pp_hot.c 3036
PP(pp_match) { .... MgBYTEPOS_set(mg, TARG, truebase, RXp_OFFS(prog)[0].end); .... }
Dan inilah peringatan untuk makro ... Untuk memahami apa yang terjadi di sana, bahkan penerapan makro tidak akan membantu, karena menggunakan beberapa makro lagi!
Oleh karena itu, saya lampirkan sebuah fragmen dari file yang telah diproses untuk baris kode ini:
(((targ)->sv_flags & 0x00000400) && (!((targ)->sv_flags & 0x00200000) || S_sv_only_taint_gmagic(targ)) ? (mg)->mg_len = ((prog->offs)[0].end), (mg)->mg_flags |= 0x40 : ((mg)->mg_len = (((targ)->sv_flags & 0x20000000) && !__builtin_expect(((((PL_curcop)->cop_hints + 0) & 0x00000008) ? (_Bool)1 :(_Bool)0),(0))) ? (ssize_t)Perl_utf8_length( (U8 *)(truebase), (U8 *)(truebase)+((prog->offs)[0].end)) : (ssize_t)((prog->offs)[0].end), (mg)->mg_flags &= ~0x40));
Di suatu tempat di sini, penganalisa meragukan penggunaan yang benar dari operator ternary (ada 3 dari mereka), tetapi saya tidak menemukan kekuatan untuk memahami apa yang sedang dilakukan dalam kode ini. Kami telah melihat bahwa pengembang membuat kesalahan seperti itu, jadi di sini juga sangat mungkin terjadi.
Tiga kegunaan lagi dari makro ini:
- V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. pp_ctl.c 324
- V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. regexec.c 7335
- V502 Mungkin operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Operator '?:' Memiliki prioritas lebih rendah daripada operator '&&'. re_exec.c 7335
Catatan rekan Andrei Karpov. Saya merenungkan kode ini selama 10 menit dan saya cenderung percaya bahwa tidak ada kesalahan. Tetapi bagaimanapun juga, sangat menyakitkan untuk membaca kode seperti itu, dan lebih baik tidak menulis seperti itu.
Kesalahan dalam kondisi
V523 Pernyataan 'lalu' sama dengan pernyataan 'lain'. toke.c 12056
static U8 * S_add_utf16_textfilter(pTHX_ U8 *const s, bool reversed) { .... SvCUR_set(PL_linestr, 0); if (FILTER_READ(0, PL_linestr, 0)) { SvUTF8_on(PL_linestr); } else { SvUTF8_on(PL_linestr); } PL_bufend = SvEND(PL_linestr); return (U8*)SvPVX(PL_linestr); }
Saya pikir Anda bisa melakukannya tanpa memeriksa isi makro untuk memastikan bahwa ada duplikat kode yang mencurigakan.
V564 The '|' operator diterapkan pada nilai tipe bool. Anda mungkin lupa menyertakan tanda kurung atau bermaksud menggunakan '||' operator. op.c 11494
OP * Perl_ck_rvconst(pTHX_ OP *o) { .... gv = gv_fetchsv(kidsv, o->op_type == OP_RV2CV && o->op_private & OPpMAY_RETURN_CONSTANT ? GV_NOEXPAND : iscv | !(kid->op_private & OPpCONST_ENTERED), iscv
Kode yang sangat aneh Ekspresi "iscv | ! (kid-> op_private & OPpCONST_ENTERED) "tidak digunakan dengan cara apa pun. Jelas ada kesalahan ketik di sini. Sebagai contoh, mungkin Anda harus menulis di sini:
: iscv = !(kid->op_private & OPpCONST_ENTERED), iscv
Ekspresi
V547 'RETVAL == 0' selalu benar. Typemap.c 710
XS_EUPXS(XS_XS__Typemap_T_SYSRET_pass); XS_EUPXS(XS_XS__Typemap_T_SYSRET_pass) { dVAR; dXSARGS; if (items != 0) croak_xs_usage(cv, ""); { SysRet RETVAL; #line 370 "Typemap.xs" RETVAL = 0; #line 706 "Typemap.c" { SV * RETVALSV; RETVALSV = sv_newmortal(); if (RETVAL != -1) {
Variabel
RETVAL diperiksa dua kali berturut-turut. Selain itu, dapat dilihat dari kode bahwa variabel ini selalu nol. Mungkin dalam satu atau kedua kondisi mereka ingin memeriksa pointer
RETVALSV , tetapi mereka membuat kesalahan ketik.
Melempar peringatan tentang ukuran operator
Ada beberapa jenis aturan diagnostik dalam penganalisa yang mencari kesalahan menggunakan operator
sizeof . Di proyek Perl 5, dua diagnostik ini menghasilkan total sekitar seribu peringatan. Dalam hal ini, bukan penganalisa yang harus disalahkan, tetapi makro.
V568 Aneh bahwa argumen operator sizeof () adalah ekspresi 'len + 1'. util.c 1084
char * Perl_savepvn(pTHX_ const char *pv, I32 len) { .... Newx(newaddr,len+1,char); .... }
Kode ini memiliki banyak makro yang mirip. Saya memilih satu sebagai contoh, kami tertarik pada argumen "len + 1".
Preprocessor makro berkembang menjadi kode berikut:
(newaddr = ((void)(__builtin_expect(((((( sizeof(size_t) < sizeof(len+1) || sizeof(char) > ((size_t)1 << 8*(sizeof(size_t) - sizeof(len+1)))) ? (size_t)(len+1) : ((size_t)-1)/sizeof(char)) > ((size_t)-1)/sizeof(char))) ? (_Bool)1 : (_Bool)0),(0)) && (S_croak_memory_wrap(),0)), (char*)(Perl_safesysmalloc((size_t)((len+1)*sizeof(char))))));
Peringatan analisa dikeluarkan untuk
ukuran (len +1) konstruksi. Faktanya adalah bahwa tidak ada perhitungan yang dibuat dalam argumen
ukuran operator. Banyak makro diperluas ke kode serupa. Ini mungkin kode warisan lama di mana tidak ada yang mau menyentuh apa pun, tetapi pengembang saat ini terus menggunakan makro lama, menunjukkan perilaku mereka yang berbeda.
Dereferencing null pointer
V522 Dereferencing dari pointer nol 'sv' mungkin terjadi. pp_ctl.c 577
OP * Perl_pp_formline(void) { .... SV *sv = ((void *)0); .... switch (*fpc++) { .... case 4: arg = *fpc++; f += arg; fieldsize = arg; if (mark < sp) sv = *++mark; else { sv = &(PL_sv_immortals[2]); Perl_ck_warner( (28 ), "...."); } .... break; case 5: { const char *s = item = ((((sv)->sv_flags & (....)) == 0x00000400) ? .... .... } .... }
Fragmen kode ini sepenuhnya diambil dari file pracroses, karena tidak mungkin untuk memverifikasi keberadaan masalah dari file sumber, sekali lagi karena makro.
Pointer
sv diinisialisasi ke nol setelah deklarasi. Penganalisa menemukan bahwa ketika melewati
5 dalam
pernyataan switch , pointer ini dereferenced, yang tidak pernah diinisialisasi sebelumnya. Perubahan dalam pointer
sv hadir di cabang dengan nilai
4 , tetapi pada akhir blok kode ini adalah pernyataan
break . Kemungkinan besar, kode tambahan diperlukan di tempat ini.
V595 Pointer 'k' digunakan sebelum diverifikasi terhadap nullptr. Periksa baris: 15919, 15920. op.c 15919
void Perl_rpeep(pTHX_ OP *o) { .... OP *k = o->op_next; U8 want = (k->op_flags & OPf_WANT);
Dalam cuplikan kode ini, penganalisa menemukan pointer
k , yang mendereferensi satu baris lebih awal dari pemeriksaan validitasnya. Ini bisa berupa kesalahan atau kode tambahan.
Diagnostik
V595 menemukan banyak peringatan dalam proyek apa pun, Perl 5 tidak terkecuali. Tidak mungkin untuk memasukkan semua ini ke dalam artikel, oleh karena itu kami akan membatasi diri pada satu contoh, dan pengembang akan memeriksa proyek sendiri jika mereka mau.
Lain-lain
V779 Kode tidak terjangkau terdeteksi. Mungkin saja ada kesalahan. universal.c 457
XS(XS_utf8_valid); XS(XS_utf8_valid) { dXSARGS; if (items != 1) croak_xs_usage(cv, "sv"); else { SV * const sv = ST(0); STRLEN len; const char * const s = SvPV_const(sv,len); if (!SvUTF8(sv) || is_utf8_string((const U8*)s,len)) XSRETURN_YES; else XSRETURN_NO; } XSRETURN_EMPTY; }
Pada baris dengan
XSRETURN_EMPTY, alat analisa mendeteksi kode yang tidak dapat dijangkau. Ada dua
pernyataan pengembalian dalam fungsi ini dan
croak_xs_usage - makro yang diperluas ke fungsi noreturn:
void Perl_croak_xs_usage(const CV *const cv, const char *const params) __attribute__((noreturn));
Di tempat-tempat serupa dalam kode Perl 5, makro
NOT_REACHED digunakan untuk menunjukkan cabang yang tidak terjangkau.
V784 Ukuran bit mask kurang dari ukuran operan pertama. Ini akan menyebabkan hilangnya bit yang lebih tinggi. inffast.c 296
void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { .... unsigned long hold; unsigned bits; .... hold &= (1U << bits) - 1; .... }
Penganalisis mendeteksi operasi yang mencurigakan saat bekerja dengan topeng bit. Sebagai bitmask, variabel resolusi lebih rendah digunakan daripada variabel
holding . Ini menghasilkan hilangnya bit tinggi. Pengembang harus memperhatikan kode ini.
Kesimpulan

Menemukan kesalahan melalui makro sangat sulit. Melihat laporan itu membutuhkan banyak waktu dan upaya. Namun demikian, artikel tersebut memuat kasus-kasus yang sangat menarik yang mirip dengan kesalahan nyata. Laporan analisa cukup besar, pasti ada jauh lebih menarik di sana. Tapi saya tidak bisa melihatnya lebih jauh :). Saya menyarankan pengembang untuk memeriksa proyek sendiri dan menghilangkan cacat yang akan dapat mereka deteksi.
PS Kami pasti ingin mendukung proyek yang menarik ini, dan kami siap memberikan pengembang dengan lisensi selama beberapa bulan.

Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Svyatoslav Razmyslov.
Perl 5: Cara Menyembunyikan Kesalahan di Macro