Para aqueles que querem jogar detetive: encontre o erro na função do Midnight Commander

Encontre o erro!


Convidamos você a tentar encontrar um erro em uma função muito simples do projeto GNU Midnight Commander. Porque Assim mesmo. É engraçado e interessante. Embora não, nós mentimos. Mais uma vez, queremos demonstrar um erro que uma pessoa encontra com dificuldade no processo de revisão de código, mas encontra facilmente o analisador de código estático do PVS-Studio.

Recentemente, recebemos uma carta perguntando por que o analisador gera um aviso na função EatWhitespace , cujo código é fornecido abaixo. Na verdade, a questão não é tão simples. Tente descobrir o que há de errado com esse código.

static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

Como você pode ver, a função EatWhitespace é muito pequena. Mesmo um comentário em uma função ocupa mais espaço do que o corpo da própria função :). Agora alguns detalhes.

Descrição da função Getc :

 int getc ( FILE * stream ); 

A função retorna o caractere apontado pelo indicador interno da posição do arquivo do fluxo especificado. Então o indicador vai para o próximo caractere. Se o final do arquivo for atingido no momento da chamada para o fluxo, a função retornará EOF e definirá o indicador de fim do arquivo para esse fluxo. Se ocorrer um erro de leitura, a função retornará um valor EOF e definirá um indicador de erro para o fluxo especificado (ferror).

Descrição da função isspace :

 int isspace( int ch ); 

A função verifica se o caractere está em branco, de acordo com a classificação do código do idioma atual. No código do idioma padrão, os seguintes caracteres são espaços em branco:

  • espaço (0x20, ``);
  • mudança de página (0x0c, '\ f');
  • avanço de linha LF (0x0a, '\ n');
  • retorno de carro CR (0x0d, '\ r');
  • guia horizontal (0x09, '\ t');
  • guia vertical (0x0b, '\ v').

Valor de retorno Valor diferente de zero, se o caractere for espaço em branco, zero caso contrário.

A função EatWhitespace deve pular todos os caracteres considerados em branco, exceto o feed de linha '\ n'. Outro motivo para interromper a leitura de um arquivo pode estar chegando ao final do arquivo (EOF).

E agora, sabendo tudo isso, tente encontrar um erro!

Para impedir que o leitor acidentalmente não olhe imediatamente para a resposta, adicione alguns unicórnios em espera.

Figura 1. Hora de procurar um erro.  Unicórnios vão esperar.


Figura 1. Hora de procurar um erro. Unicórnios vão esperar.

Ainda não encontrou o erro?

O fato é que enganamos os leitores sobre o isspace . Haha Este não é um recurso padrão, mas uma macro caseira. Sim, somos inocentes e deixamos você confuso.

Figura 2. Um unicórnio dá aos leitores uma impressão falsa do espaço em questão.


Figura 2. Um unicórnio dá aos leitores uma falsa impressão do que é isspace .

Na verdade, é claro, nós e nosso unicórnio não temos culpa. Os autores do projeto GNU Midnight Commander contribuíram para a confusão ao decidir criar sua própria implementação de espaço de iss no arquivo charset.h :

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

Ao criar essa macro, alguns desenvolvedores confundiram outros. O código é escrito na suposição de que isspace é uma função padrão que considera retornos de carro (0x0d, '\ r') como um dos caracteres de espaço em branco.

A macro implementada considera apenas espaços e tabulações como caracteres de espaço em branco. Vamos substituir a macro e ver o que acontece.

 for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile)) 

A subexpressão ('\ n'! = C) é redundante (redundante), pois seu resultado sempre será verdadeiro. O analisador PVS-Studio alerta sobre isso, emitindo um aviso:

V560 Uma parte da expressão condicional é sempre verdadeira: ('\ n'! = C). params.c 136.

Para maior clareza, vamos analisar três opções para o desenvolvimento de eventos:

  • O final do arquivo é atingido. O fim do arquivo (EOF) não é um espaço ou tabulação. A subexpressão ('\ n'! = C) não é calculada devido à avaliação de curto-circuito . O ciclo para.
  • Qualquer caractere que não seja um espaço ou tabulação é lido. A subexpressão ('\ n'! = C) não é calculada devido à avaliação de curto-circuito. O ciclo para.
  • Leia um caractere de espaço ou uma guia horizontal. A subexpressão ('\ n'! = C) é calculada, mas seu resultado sempre será verdadeiro.

Em outras palavras, o código revisado é equivalente a isso:

 for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile)) 

Descobrimos que o código não funciona conforme o esperado. Vamos ver agora que consequências isso tem.

O programador que escreveu a chamada isspace no corpo da função EatWhitespace esperava que uma função padrão fosse chamada. Por isso, ele adicionou a condição de que o avanço de linha LF ('\ n') não deve ser considerado um caractere de espaço em branco.

Portanto, o programador planejou que, além das guias de espaço e horizontais, caracteres como mudança de página e guia vertical fossem ignorados.

Vale ressaltar que também foi planejado pular o caractere de retorno de carro CR (0x0d, '\ r'). Isso não acontece e o ciclo para quando encontra esse símbolo. Isso causará surpresas desagradáveis ​​se o separador de linhas no arquivo for a sequência CR + LF usada em alguns sistemas não UNIX, como o Microsoft Windows.

Para aqueles que desejam aprender mais sobre os motivos históricos para usar LF ou CR + LF como separadores de linhas, aqui está o artigo da Wikipedia " Feed de linha ".

A função EatWhitespace deve processar arquivos da mesma maneira, onde LF e CR + LF são usados ​​como um separador. Para o caso de CR + LF, não é assim. Em outras palavras, se seu arquivo veio do mundo do Windows, você está sem sorte :).

Talvez este não seja um erro sério, especialmente porque o GNU Midnight Commander é comum em sistemas operacionais do tipo UNIX, onde o caractere LF (0x0a, '\ n') é usado para converter uma linha. No entanto, devido a essas insignificâncias, surgem vários problemas irritantes de incompatibilidade de dados preparados nos sistemas Linux e Windows.

O erro descrito é interessante, pois é quase impossível detectar com uma revisão de código clássica. Nem todos os desenvolvedores de projetos podem conhecer os meandros da macro, e esquecê-los é muito fácil. Este é um bom exemplo de análise de código estática que complementa as análises de código e outras técnicas de detecção de erros.

Substituir funções padrão é uma má prática. A propósito, recentemente no artigo “ Análise de código estático do amor ”, um caso semelhante foi considerado com a macro #define sprintf std :: printf .

Uma solução melhor seria atribuir à macro um nome exclusivo, por exemplo, is_space_or_tab . Então a confusão seria impossível.

Talvez o motivo da criação da macro tenha sido a operação lenta da função isspace padrão , e o programador criou uma versão mais rápida, suficiente para resolver todas as tarefas necessárias. Mas ainda assim, esta decisão está errada. Seria mais confiável definir o isspace de maneira a obter código não compilado. E para implementar a funcionalidade necessária em uma macro com um nome exclusivo.

Obrigado pela atenção. Convidamos você a baixar e experimentar o analisador PVS-Studio para testar seus projetos. Além disso, lembramos que recentemente o analisador adicionou suporte à linguagem Java.



Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Andrey Karpov. Quer jogar um detetive? Encontre o bug em uma função do Midnight Commander .

Source: https://habr.com/ru/post/pt439374/


All Articles