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

  • Tiago

    Bom artigo Sérgio.

    Você que já está mais familiarizado com este tipo de ferramenta, qual você indica? (que seja freeware)

    Abraços

  • Olá Tiago,

    Já usei o Splint (http://splint.org/), que é um fork do antigo lint desenvolvido para UNIX, e que traz alguns conceitos extras para o desenvolvimento de software seguro. è muito bom e bem fácil de configurar e utilizar.

    Já ouvi também falarem muito bem do Blast mas nunca cheguei a utilizá-lo (http://mtc.epfl.ch/software-tools/blast/index-epfl.php). Me parece ser um pouco mais complexo, porém mais flexível também.

    Um abraço!

  • energos

    Não acho que o uso de goto seja necessariamente uma vergonha. O problema é o uso abusivo dele, gerando código “spaghetti”. Como toda ferramente, se bem usada, pode ser útil.
    Tem uma discussão interessante sobre isso em http://kerneltrap.org/node/553/2131

  • Olá energos,

    Entendo perfeitamente seu ponto de vista. Toda ferramenta pode ser tanto eficiente quanto ineficiente, depende do uso que fazemos dela. No caso do goto em particular, é uma questão de estilo.

    Já vi casos elegantes do uso de goto, principalmente para o tratamento de erros no estilo try-except, como também já vi inúmeros casos de implementações bem ao estilo “linguiça” ou “espagueti”.

    Particularmente, eu prefiro um código com funções pequenas e bem definidas, e um controle de fluxo com if, for e do/while. Esses elementos são sufucientes para desenvolver um código em C elegante, simples, portável e de fácil manutenção.

    Legal você ter levantado esta questão, acho que esse assunto até vale um post separado…:)

    Um abraço!

  • Pingback: Netrino's Embedded C Coding Standard()

  • Paulo

    Sérgio,
    muito legal seu post, acompanho seu blog, sempre com ótimos artigos, uma duvida, na linha de código if ((buf[ind] % 2) = 0)

    era uma atribuição que você gostaria de fazer mesmo?
    abracos

  • Olá Paulo!

    Não, a intenção era fazer uma comparação. O que eu queria mostrar é um erro comum de programação em C, que pode ser identificado facilmente por uma ferramenta de análise estática de código.

    Um abraço!

  • Paulo

    ah, sim desculpe, eu vi o código e postei um comentário antes de terminar de ler o texto! =(
    desculpe

  • João Marcelo Dangremon

    Recomenda algum livro sobre o assunto?

    • Olá João marcelo,

      Infelizmente não conheço nenhum livro sobre este tema.

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