Para complementar a lista de linguagens de programação de código aberto que foram testadas usando o analisador de código estático PVS-Studio, foi selecionado o Perl 5. Este artigo trata de erros e dificuldades encontrados na visualização dos resultados da análise. O número de macros no código é tão grande que parece que o código não está escrito em C, mas em algum dialeto estranho dele. Apesar das dificuldades ao visualizar o código, consegui coletar problemas interessantes, que serão discutidos neste artigo.
1. Introdução
Perl é uma linguagem de programação dinâmica de propósito geral interpretada de alto nível (Perl é uma família de duas linguagens de programação dinâmica de alto nível, de propósito geral, interpretadas). Perl 5 foi lançado em 1994. Depois de algumas décadas, o código C com várias macros causa nervosismo aos programadores modernos.
O código fonte do Perl 5 foi retirado do
repositório oficial (
blead branch). Para verificar o projeto, usamos o analisador de código estático
PVS-Studio . A análise foi realizada no sistema operacional Linux, mas o analisador também está disponível para Windows e macOS.
Visualizar os resultados da análise não foi uma tarefa fácil. O fato é que o analisador verifica os arquivos
.i pré -
processados , nos quais todas as diretivas do pré-processador já estão abertas, e gera avisos nos arquivos com o código-fonte. Esse é o comportamento correto do analisador, você não precisa alterar nada, mas muitos avisos são emitidos para macros! E atrás das macros está um código ilegível.
O operador ternário não funciona da maneira que você pensa
V502 Talvez o operador '?:'
Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '-'. 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)) { .... } .... }
Vamos começar a revisão com um belo erro. A cada poucas revisões de código, é necessário repetir que o operador ternário tem quase a menor prioridade na computação.
Considere o fragmento com o erro:
s <= PL_bufend - (is_utf8) ? UTF8SKIP(s) : 1
A ordem das operações que o programador espera:
- ?:
- -
- <=
O que realmente acontece:
- -
- <=
- ?:
Mantenha um rótulo com as prioridades de operações: "
Prioridade de operações em C / C ++ ."
V502 Talvez o operador '?:'
Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '=='. 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); .... }
Código simples com um erro semelhante. Mas se você não souber as prioridades das operações, poderá cometer um erro em uma expressão de qualquer tamanho.
Outro local com afirmação:
- V502 Talvez o operador '?:' Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '=='. re_exec.c 9286
V502 Talvez o operador '?:'
Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '&&'. pp_hot.c 3036
PP(pp_match) { .... MgBYTEPOS_set(mg, TARG, truebase, RXp_OFFS(prog)[0].end); .... }
E aqui está um aviso para uma macro ... Para entender o que está acontecendo lá, mesmo a implementação da macro não ajudará, porque usa mais algumas macros!
Portanto, anexo um fragmento do arquivo pré-processado para esta linha de código:
(((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));
Em algum lugar aqui, o analisador duvidava do uso correto do operador ternário (existem três), mas não encontrei forças para entender o que está sendo feito neste código. Já vimos que os desenvolvedores cometem esses erros, então aqui também pode ser muito provável.
Mais três usos dessa macro:
- V502 Talvez o operador '?:' Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '&&'. pp_ctl.c 324
- V502 Talvez o operador '?:' Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '&&'. regexec.c 7335
- V502 Talvez o operador '?:' Funcione de maneira diferente do esperado. O operador '?:' Tem uma prioridade mais baixa que o operador '&&'. re_exec.c 7335
Nota colegas Andrei Karpov. Eu meditei nesse código por 10 minutos e estou inclinado a acreditar que não há erro. Mas, em qualquer caso, é extremamente doloroso ler esse código e é melhor não escrever assim.
Erros nas condições
V523 A
instrução 'then' é equivalente à instrução '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); }
Eu acho que você pode fazer isso sem examinar o conteúdo das macros para garantir que haja fragmentos de código duplicados suspeitos.
V564 O '|' O operador é aplicado ao valor do tipo bool. Você provavelmente esqueceu de incluir parênteses ou pretende usar o '||' operador. 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
Código muito estranho. A expressão "iscv | ! (kid-> op_private & OPpCONST_ENTERED) "não é usado de forma alguma. Há claramente um erro de digitação aqui. Por exemplo, talvez você deva escrever aqui:
: iscv = !(kid->op_private & OPpCONST_ENTERED), iscv
A expressão
V547 'RETVAL == 0' sempre é verdadeira. 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) {
A variável
RETVAL é verificada duas vezes seguidas. Além disso, pode ser visto pelo código que essa variável é sempre zero. Talvez em uma ou ambas as condições queiram verificar o ponteiro
RETVALSV , mas cometeram um erro de digitação.
Lançando avisos sobre o tamanho do operador
Existem vários tipos de regras de diagnóstico no analisador que procuram erros usando o operador
sizeof . No projeto Perl 5, dois desses diagnósticos geraram um total de cerca de mil avisos. Nesse caso, não é o analisador que deve culpar, mas as macros.
V568 É estranho que o argumento do operador sizeof () seja a expressão 'len + 1'. util.c 1084
char * Perl_savepvn(pTHX_ const char *pv, I32 len) { .... Newx(newaddr,len+1,char); .... }
O código possui muitas macros semelhantes. Eu escolhi um como exemplo, estamos interessados no argumento "len + 1".
Um pré-processador de macro se expande para o seguinte código:
(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))))));
Um aviso do analisador é emitido para a construção
sizeof (len +1) . O fato é que nenhum cálculo é feito nos argumentos do
tamanho do operador. Muitas macros são expandidas em código semelhante. Provavelmente, esse é um código antigo antigo, no qual ninguém deseja tocar em nada, mas os desenvolvedores atuais continuam usando macros antigas, sugerindo um comportamento diferente.
Desreferenciando ponteiros nulos
V522 A desreferenciação do ponteiro nulo 'sv' pode ocorrer. 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) ? .... .... } .... }
Esse fragmento de código é retirado completamente do arquivo pré-processado, porque é impossível verificar a presença do problema no arquivo de origem, novamente por causa das macros.
O ponteiro
sv é inicializado com zero na declaração. O analisador descobriu que, ao passar acima de
5 na
instrução switch , esse ponteiro é desreferenciado, o que nunca foi inicializado antes. Uma mudança no ponteiro
sv está presente na ramificação com um valor de
4 , mas no final desse bloco de código está a instrução
break . Muito provavelmente, é necessário código adicional neste local.
V595 O ponteiro 'k' foi utilizado antes de ser verificado no nullptr. Verifique as linhas: 15919, 15920. op.c 15919
void Perl_rpeep(pTHX_ OP *o) { .... OP *k = o->op_next; U8 want = (k->op_flags & OPf_WANT);
Nesse trecho de código, o analisador encontrou um ponteiro
k , que é desreferenciado uma linha antes da verificação de validade. Isso pode ser um erro ou um código extra.
O Diagnostics
V595 encontra muitos avisos em qualquer projeto, o Perl 5 não é uma exceção. É impossível incluir tudo isso no artigo, portanto, nos limitaremos a um exemplo, e os desenvolvedores verificarão o projeto por conta própria, se assim o desejarem.
Diversos
V779 Código inacessível detectado. É possível que haja um erro. 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; }
Na linha com
XSRETURN_EMPTY, o analisador detectou um código inacessível. Há duas
instruções de retorno nessa função e
croak_xs_usage - uma macro que é expandida para uma função noreturn:
void Perl_croak_xs_usage(const CV *const cv, const char *const params) __attribute__((noreturn));
Em locais semelhantes no código Perl 5, a macro
NOT_REACHED é usada para indicar uma ramificação inacessível.
V784 O tamanho da máscara de bit é menor que o tamanho do primeiro operando. Isso causará a perda de bits mais altos. inffast.c 296
void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) { .... unsigned long hold; unsigned bits; .... hold &= (1U << bits) - 1; .... }
O analisador detectou uma operação suspeita enquanto trabalhava com máscaras de bits. Como máscara de bit, uma variável de resolução mais baixa é usada do que a variável
hold . Isso resulta na perda de bits altos. Os desenvolvedores devem prestar atenção a este código.
Conclusão

Encontrar erros através de macros foi muito difícil. A visualização do relatório levou muito tempo e esforço. No entanto, o artigo inclui casos muito interessantes semelhantes a erros reais. O relatório do analisador é bastante grande, definitivamente há muito mais interessante lá. Mas não consigo mais olhar :). Aconselho o desenvolvedor a verificar o projeto por conta própria e eliminar os defeitos que eles serão capazes de detectar.
PS: Nós definitivamente queremos apoiar este projeto interessante e estamos prontos para fornecer aos desenvolvedores uma licença por vários meses.

Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Svyatoslav Razmyslov.
Perl 5: Como ocultar erros em macros