Análise estática de código

- por Sergio Prado

Categorias: Linguagem C Tags: , , ,

Diz o ditado que, no processo de desenvolvimento de software, passamos 50% do tempo corrigindo bugs, e os outros 50% inserindo estes bugs – do inglês “In the software development process, we pass 50% debugging, and the other 50% bugging”.

A melhor forma de perder menos tempo na correção de problemas, é não criar problemas em primeiro lugar. Mas somos humanos, e errar é humano. Portanto, vamos errar. Invariavelmente, vamos acabar inserindo alguns bugs no código. Portanto, precisamos de ferramentas que minimizem a probabilidade destes erros acontecerem. Uma destas ferramentas é o próprio compilador.

Mas na prática, não é assim que a maioria dos desenvolvedores/engenheiros enxergam esta ferramenta. Segundo estes, o objetivo do compilador é gerar o arquivo executável/binário, e ponto final. E aquele monte de warnings que o compilador “cuspiu” na tela? É facil de resolver, basta desabilitar um parâmetro do compilador. Pronto, compilou sem nenhum warning.

Mas os warnings continuam lá. O compilador bem que tentou te avisar: olhe, é bem provável que aquele ponteiro não tenha sido inicializado antes de você tentar usá-lo para referenciar alguma região de memória, ou então, preste atenção, esta variável é char (1 byte) e você esta tentando atribuir um inteiro de 2 bytes a ela.

Porque então não investir 10 minutos do seu tempo para checar as mensagens de warning do compilador? É muito menos custoso do que depois perder 4 horas para achar um problema que uma ferramenta poderia ter encontrado automaticamente para você. É extremamente importante criar o hábito de checar os warnings de compilação.

Eu sei, você pode dizer que o compilador é muito chato, e alguns warnings, ou a maioria, não são críticos ou não irão causar nenhum tipo de problema para a aplicação. O que eu posso te dizer é o seguinte: ninguém entende melhor da linguagem, sua sintaxe e semântica, do que o compilador. Portanto, não tente ser mais esperto do que ele. O objetivo então, e principalmente para quem trabalha com sistemas embarcados, é desenvolver um código extremamente portável, limpo, e o mais próximo possível do padrão ANSI. Desta forma, o compilador vai ser muito mais “bonzinho” com você.

O que precisamos então é de uma ferramenta que possa gerar mais warnings, não menos. Uma ferramenta que analise o código e indique construções estranhas, potenciais problemas e usos não comuns da linguagem. Conforme já mencionei, alguns compiladores possuem uma boa parte desta funcionalidade, como o gcc através dos parâmetros “Wall”, “Wextra” e “pedantic” Mas existem ferramentas especificas para este trabalho. A estas ferramentas damos o nome genérico de LINT.

Um artigo muito bom sobre o uso de LINT em sistemas embarcados, suas vantagens e como usá-la dentro do processo de desenvolvimento de software pode ser encontrado aqui. Nosso objetivo aqui é exemplificar através do uso de ferramentas disponíveis no mercado.

Ferramentas

O que as pessoas normalmente reclamam deste tipo de ferramenta é a quantidade excessiva de warnings gerados, muitos deles falso-positivos.

Pode-se chegar a mais de 10.000 warnings para um codigo de 1.000 linhas. Por isso, a ferramenta precisa ser treinada – leia-se configurada – antes de utilizada. E este processo pode levar um tempo. Um bom tempo na verdade. Mas as recompensas são grandes no final.

Existem muitas ferramentas disponíveis, tanto livres  quanto comerciais. Uma lista bem completa pode ser encontrada aqui. Para efeitos demonstrativos vamos utilizar a ferramenta PC-LINT.

O PC-LINT é comercial, e uma das mais usadas ferramentas de análise estática de codigo para C/C++. Para efeito de testes, vamos utilizar um recurso disponivel online no site da ferramenta, onde é possivel testar trechos de codigo sem que seja necessário baixar e instalar a ferramenta.

Vamos testar aqui 2 trechos diferentes de código, um desenvolvido especificamente para os testes e um retirado de um pacote open-source usado em sistemas embarcados com Linux.

Teste 1: O jogo dos 4 erros

O programa abaixo tem o objetivo de varrer um vetor de números, somar apenas os números pares, e exibir o resultado. Você é bom naquele jogo dos 7 erros? Então tente encontrar aqui os 4 erros que inseri neste código.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
 
int soma(int a, int b) {
    return (a + b);
}
 
int subtrai(int a, int b) {
    return (a - b);
}
 
int main() {
 
    int ind, ret;
    char buf[10] = {15,24,37,42,53,67,79,81,94,6};
    char result;
 
    for (ind = 0; ind <= 10; ind++) {
        if ((buf[ind] % 2) = 0)
            result = soma(buf[ind], result);
    }
    printf("Soma = %d\n", result);
 
    return(0);
}

Você encontrou os erros? Quanto tempo você levou? A ferramenta encontrou os erros de forma instantânea:

  • Warning 661:  Possible access of out-of-bounds pointer (1 beyond end of data) by operator ‘[‘ [Reference: file diy.c: lines 17, 18]
  • Info 774:  Boolean within ‘if’ always evaluates to False [Reference: file diy.c: line 18]
  • Info 734:  Loss of precision (assignment) (31 bits to 7 bits)
  • Warning 530:  Symbol ‘result’ (line 15) not initialized

Seguem os erros, na ordem das mensagens acima:

  1. Estou ultrapassando os limites do buffer “buf” ao acessá-lo no loop com a variável “ind”. Na linha 17 deveria trocar “ind<=10” por “ind<10”.
  2. Na comparação usada na linha 18, “acidentalmente” digitei “=” ao invés de “==”.
  3. Na linha 19, estou atribuindo um inteiro a um byte. A variável “result” deveria ser, no minimo, um inteiro.
  4. A variavel “result” não foi inicializada com o valor 0 antes de iniciar a soma.

Além destes erros, a ferramenta ainda apontou alguns warnings de símbolos (variáveis e funções) não utilizados, e poderíamos utilizar estas dicas para deixar o código mais claro e limpo:

Warning 529:  Symbol ‘ret’ (line 13) not subsequently referenced Info 714:  Symbol ‘subtrai(int, int)’ (line 7, file diy.c) not referenced

Teste 2: Ferramenta Date do Busybox

Este teste foi feito com o pacote open-source Busybox. O Busybox é muito utilizado em sistemas embarcados com Linux, pois proporciona um conjunto de ferramentas comumente utilizadas em Linux, porém bem mais enxutas que suas versões originais. Os testes foram feitos com o módulo date.c.

A ferramenta não encontrou nenhum erro crítico, mas algumas mensagens de warning foram exibidas, e uma análise e revisão do código poderia deixá-lo bem mais limpo e portável. Algumas das mensagens informadas pela ferramenta:

  • Loss of sign (assignment) (int to unsigned int)
  • Type mismatch (assignment) (char * = int)
  • Use of goto is deprecated Symbol ‘argc’ (line 41) not referenced Loss of precision (arg. no. 1) (unsigned int to int)

Minha conclusão é a seguinte: se você tem 4 horas para cortar uma árvore, afie seu machado por 3 horas, e em menos de uma hora esta árvore estará cortada. Às vezes achamos que vamos perder muito tempo até aprender a usar ou configurar uma ferramenta, mas não percebemos que este investimento é valido, pois o tempo que perdemos depois para corrigir os problemas é muito maior. Sem contar que o aprendizado durante o processo vale, e muito, a pena.

Um abraço a todos,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.