Perl 5:宏如何隐藏错误


为了补充使用PVS-Studio静态代码分析器测试过的开源编程语言的列表,选择了Perl 5,本文涉及发现的错误和查看分析结果的困难。 代码中的宏数量如此之多,以至于似乎该代码不是用C编写的,而是用某种奇怪的方言编写的。 尽管查看代码时遇到了困难,但我还是设法收集了一些有趣的问题,本文将对此进行讨论。

引言


Perl是一种高级解释型动态通用编程语言(Perl是两种高级,通用型,解释性动态编程语言的家族)。 Perl 5于1994年推出。 几十年后,带有大量宏的C代码使现代程序员感到不安。

Perl 5的源代码来自官方存储库blead分支)。 为了检查项目,我们使用了PVS-Studio静态代码分析器。 分析是在Linux操作系统上进行的,但分析仪也可用于Windows和macOS。

查看分析结果并非易事。 事实是分析器会检查已预处理的.i文件(已打开预处理器的所有指令),并在带有源代码的文件上生成警告。 这是分析仪的正确行为,您无需更改任何内容,但是会向宏发出许多警告! 宏后面是不可读的代码。

三元运算符不符合您的思维方式


V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'-'运算符。 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)) { .... } .... } 

让我们开始回顾一个美丽的错误。 每进行几次代码审查,都必须重复说三元运算符在计算中几乎具有最低的优先级。

考虑带有错误的片段:

 s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1 

程序员期望的操作顺序:
  1. ?:
  2. --
  3. <=

实际发生的情况:
  1. --
  2. <=
  3. ?:

保留操作优先级的标签:“ C / C ++中的操作优先级 ”。

V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'=='运算符。 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); .... } 

具有类似错误的简单代码。 但是,如果您不知道操作的优先级,则可以在任何大小的表达式中犯错误。

断言的另一个地方:

  • V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'=='运算符。 re_exec.c 9286

V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'&&'运算符。 pp_hot.c 3036

 PP(pp_match) { .... MgBYTEPOS_set(mg, TARG, truebase, RXp_OFFS(prog)[0].end); .... } 

这是对宏的警告...要了解其中发生的情况,即使宏的实现也无济于事,因为它使用了更多的宏!

因此,我为这行代码附​​加了预处理文件的一部分:

 (((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)); 

分析器在这里的某个地方怀疑三元运算符的正确使用(其中有3个),但是我没有发现理解此代码正在执行的功能。 我们已经看到开发人员犯了这样的错误,因此在这里也很有可能。

此宏的其他三种用法:

  • V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'&&'运算符。 pp_ctl.c 324
  • V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'&&'运算符。 regexec.c 7335
  • V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'&&'运算符。 re_exec.c 7335

注意同事安德烈·卡波夫(Andrei Karpov)。 我在此代码上思考了10分钟,并倾向于认为没有错误。 但是无论如何,阅读这样的代码是非常痛苦的,最好不要这样写。

条件错误


V523'then '语句等效于'else'语句。 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); } 

我认为您可以在不检查宏内容的情况下进行操作,以确保存在可疑的重复代码片段。

V564 '|' 运算符应用于布尔类型值。 您可能忘记了括号或打算使用'||' 操作员。 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 // <= ? SVt_PVCV : o->op_type == OP_RV2SV ? SVt_PV : o->op_type == OP_RV2AV ? SVt_PVAV : o->op_type == OP_RV2HV ? SVt_PVHV : SVt_PVGV); .... } 

非常奇怪的代码。 表达式“ iscv | !(kid-> op_private&OPpCONST_ENTERED)“未以任何方式使用。 显然这里有一个错字。 例如,也许您应该在这里写:

 : iscv = !(kid->op_private & OPpCONST_ENTERED), iscv // <= 

V547表达式“ RETVAL == 0”始终为true。 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) { // <= if (RETVAL == 0) // <= sv_setpvn(RETVALSV, "0 but true", 10); else sv_setiv(RETVALSV, (IV)RETVAL); } ST(0) = RETVALSV; } } XSRETURN(1); } 

RETVAL变量连续检查两次。 此外,从代码中可以看出该变量始终为零。 也许在一种或两种情况下,他们想检查RETVALSV指针,但他们打错了字。

抛出有关sizeof运算符的警告


分析器中有几种类型的诊断规则,这些规则使用sizeof运算符查找错误。 在Perl 5项目中,其中两个诊断程序总共产生了大约一千条警告。 在这种情况下,应该责怪的不是分析器,而是宏。

V568奇怪的是sizeof()运算符的参数是'len + 1'表达式。 实用程序1084

 char * Perl_savepvn(pTHX_ const char *pv, I32 len) { .... Newx(newaddr,len+1,char); .... } 

该代码有很多类似的宏。 我选择一个作为示例,我们对参数“ len +1”感兴趣。

宏预处理器扩展为以下代码:

 (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)))))); 

sizeof(len +1)构造发出分析器警告。 事实是,没有在sizeof运算符的参数中进行任何计算。 许多宏被扩展为类似的代码。 这可能是一种旧的旧代码,其中没有人想触摸任何东西,但是当前的开发人员继续使用旧的宏,暗示了它们的不同行为。

解引用空指针


V522可能会取消引用空指针“ sv”。 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) ? .... .... } .... } 

此代码段完全取自预处理文件,因为同样由于宏的原因,无法从源文件验证问题的存在。

声明后, sv指针将初始化为零。 分析器发现,在switch语句中传递5时,该指针已取消引用,之前从未进行过初始化。 sv指针的更改出现在分支中,值为4 ,但该代码块的末尾是break语句。 很可能在此位置需要其他代码。

V595在针对nullptr进行验证之前,已使用了'k'指针。 检查行:15919,15920。op.c 15919

 void Perl_rpeep(pTHX_ OP *o) { .... OP *k = o->op_next; U8 want = (k->op_flags & OPf_WANT); // <= if ( k // <= && k->op_type == OP_KEYS && ( want == OPf_WANT_VOID || want == OPf_WANT_SCALAR) && !(k->op_private & OPpMAYBE_LVSUB) && !(k->op_flags & OPf_MOD) ) { .... } 

在此代码段中,分析器找到了指针k ,该指针早于其有效性检查被取消引用了一行。 这可能是错误,也可能是多余的代码。

Diagnostics V595在任何项目中都会发现很多警告,Perl 5也不例外。 不可能将所有这些内容都纳入本文,因此我们将仅举一个例子,开发人员将根据需要自行检查项目。

杂项


V779检测到无法访问的代码。 可能存在错误。 通用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; } 

在带有XSRETURN_EMPTY的行上分析器检测到无法访问的代码。 此函数和croak_xs_usage中有两个return语句 -宏被扩展为noreturn函数:

 void Perl_croak_xs_usage(const CV *const cv, const char *const params) __attribute__((noreturn)); 

在Perl 5代码中的类似位置, NOT_REACHED宏用于指示不可达分支。

V784位掩码的大小小于第一个操作数的大小。 这将导致丢失更高的位。 inffast.c 296

 void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { .... unsigned long hold; /* local strm->hold */ unsigned bits; /* local strm->bits */ .... hold &= (1U << bits) - 1; .... } 

分析器在使用位掩码时检测到可疑的操作。 作为位掩码,使用比分辨率hold更低分辨率的变量。 这导致高位的损失。 开发人员应注意此代码。

结论


图片6



通过宏查找错误非常困难。 查看报告需要大量时间和精力。 尽管如此,本文还是包含一些非常有趣的案例,这些案例与真实错误相似。 分析器报告相当大,肯定还有很多有趣的地方。 但是,我无法进一步研究它了:)。 我建议开发人员自己检查项目,并消除他们将能够检测到的缺陷。

PS我们绝对希望支持这个有趣的项目,并且准备为开发人员提供几个月的许可。



如果您想与说英语的读者分享这篇文章,请使用以下链接:Svyatoslav Razmyslov。 Perl 5:如何隐藏宏中的错误

Source: https://habr.com/ru/post/zh-CN425415/


All Articles