Philip Withnall philip.withnall@collabora.co.uk 2015 Usando a ferramenta certa para várias tarefas Rafael Fontenelle rafaelff@gnome.org 2017 Ferramentas

Ferramentas de desenvolvimento são muito mais do que um editor de texto e um compilador. O uso correto das ferramentas certas pode facilitar drasticamente a depuração e o rastreamento de problemas complexos com alocação de memória e chamadas de sistema; além de outras coisas. Algumas das ferramentas mais comumente usadas são descritas abaixo; outras ferramentas existem para casos de uso mais especializados e devem ser usados quando apropriado.

Um princípio geral a se usar quando se está desenvolvendo é sempre ter tantas opções de depuração habilitadas quanto possível, em vez de mantê-las desabilitadas até próximo da data de lançamento. Ao testar o código constantemente com todas as ferramentas de depuração disponível, erros podem ser encontrados desde cedo, antes que eles façam parte do código e, portanto, mais seja difícil de removê-los.

Na prática, isso significa ter todos os avisos, do compilador e de outras ferramentas, habilitados e definidos para falhar no processo de compilação com um erro se forem emitidos.

Resumo

Compile com frequência com um segundo compilador. ()

Habilite uma seleção grande de avisos de compilador e faça-os falhar. ()

Use GDB para depurar e passear pelo código. ()

Use Valgrind para analisar o uso de memória, erros de memória, cache e desempenho da CPU e erros em threads. ()

Use gcov e lcov para analisar a cobertura de teste de unidade. ()

Use sanitizadores de compilador para analisar problemas de memória, thread e comportamentos indefinidos. ()

Envie para Coverity como um cronjob e elimine erros de análise estática na medida em que eles aparecem. ()

Use o analisador estático do Clang e o Tartan regularmente para eliminar localmente erros analisáveis estaticamente

GCC e Clang

GCC é o compilador C padrão para Linux. Existe uma alternativa na forma de Clang, com funcionalidade comparável. Escolha um (provavelmente GCC) para usar como compilador principal, mas ocasionalmente use o outro para compilar o código, já que os dois detectam um conjunto um pouco diferente de erros e avisos no código. Clang também vem com uma ferramenta de análise estática que pode ser usada para detectar erros no código sem precisar compilá-lo e executá-lo; veja .

Ambos compiladores devem ser usados com tantos sinalizadores de avisos habilitados quanto possível. Apesar de avisos de compilador de vez em quando fornecerem falsos positivos, a maioria dos avisos legitimamente aponta para problemas no código e, portanto, devem ser corrigidos em vez de ignorados. Uma política de desenvolvimento de habilitar todos sinalizadores de avisos e também especificar o sinalizador -Werror (que torna todos os avisos em fatais para compilação) promove correção de avisos assim que forem introduzidos. Isso ajuda na qualidade do código. A alternativa de ignorar avisos leva a longas sessões de depuração para rastrear erros causados por problemas que teriam sido levantados pelos avisos. Similarmente, ignorar avisos até o fim do ciclo de desenvolvimento para, em um momento posterior, gastar um pedaço de tempo habilitando e corrigindo todos eles é uma perda de tempo.

Ambos GCC e Clang oferecem suporte a um amplo alcance de sinalizadores de compilador, estando apenas alguns relacionados a um código moderno e multipropósito (por exemplo, outros estão desatualizados e são para alguma arquitetura específica). Encontrar um conjunto razoável de sinalizadores para habilitar pode ser complicado e por isto que a macro AX_COMPILER_FLAGS existe.

AX_COMPILER_FLAGS permite um conjunto consistente de avisos de compilador e também testa se o computador oferece suporte a cada sinalizador antes de habilitá-lo. Isso conta para diferenças no conjunto de sinalizadores aos quais GCC e Clang oferecem suporte. Para usá-lo, adicione AX_COMPILER_FLAGS ao configure.ac. Se você está usando cópias de macros autoconf-archive na árvore do projeto, copie ax_compiler_flags.m4 para o diretório m4/ deste. Note que ele depende das macros de autoconf-archive a seguir, as quais são licenciadas sob GPL e, portanto, potencialmente não podem ser copiadas para a árvore. Elas podem permanecer no autoconf-archive, com este como uma dependência de tempo de compilação do projeto:

ax_append_compile_flags.m4

ax_append_flag.m4

ax_check_compile_flag.m4

ax_require_defined.m4

AX_COMPILER_FLAGS oferece suporte a desabilitar -Werror para compilações de lançamento, de forma que lançamentos sempre seja possível compilar com novos compiladores que podem ter introduzido mais avisos. Defina seu terceiro parâmetro para “yes” para compilações de lançamento (e apenas compilações de lançamento) para habilitar essa funcionalidade. Compilações de desenvolvimento e de integração contínua (em inglês “continuous integration” ou, abreviado, “CI”) deve sempre ter -Werror habilitado.

Compilações de lançamento podem ser detectadas usando a macro AX_IS_RELEASE, podendo o seu resultado ser passado diretamente para AX_COMPILER_FLAGS:

AX_IS_RELEASE([git]) AX_COMPILER_FLAGS([WARN_CFLAGS],[WARN_LDFLAGS],[$ax_is_release])

A escolha de política de estabilidade de lançamento (o primeiro argumento para AX_IS_RELEASE) deve ser feita por projeto, levando em consideração a estabilidade de versionamento do projeto.

GDB

GDB é o depurador padrão para C no Linux. Seu uso mais comum é para depurar travamentos, e para analisar o código enquanto ele é executado. Um tutorial completo para usar GDB é fornecido aqui.

Para executar GDB em um programa dentro da árvore fonte, use libtool exec gdb --args ./nome-do-programa --alguns --argumentos --aqui

Isso é necessário porque o libtool interfaceia cada binário compilado na árvore fonte em um script shell que define algumas variáveis libtool. Não é necessário para depurar executáveis instalados.

GDB possui muitos recursos avançados que podem ser combinados para essencialmente criar pequenos scripts de depuração, ativados por diferentes pontos de interrupção no código. Algumas vezes, essa é uma abordagem útil (para depuração de contagem de referência), mas outras vezes simplesmente usar g_debug() para emitir uma mensagem de depuração é mais simples.

Valgrind

Valgrind é uma suíte de ferramentas para instrumentar e perfilar programas. Sua ferramenta mais famosa é o memcheck, mas há, no Valgrind, também várias outras ferramentas poderosas e úteis. Elas são cobertas separadamente nas seções a seguir.

Uma forma útil de executar Valgrind é executar uma suíte de teste de unidade do programa sob Valgrind, configurando-o para retornar um código de status indicando o número de erros que ele encontrou. Quando executado como parte de make check, isso fará com que as verificações tenham sucesso se Valgrind encontrar nenhum problema; do contrário, falhará. Porém, executar make check sob Valgrind não é algo trivial de se fazer na linha de comando. Uma macro, AX_VALGRIND_CHECK pode ser usada para adicionar um novo alvo make check-valgrind para automatizar isso. Para usá-lo:

Copie ax_valgrind_check.m4 para o diretório m4/ de seu projeto.

Adicione AX_VALGRIND_CHECK para configure.ac.

Adicione @VALGRIND_CHECK_RULES@ para o Makefile.am em cada diretório que contém testes de unidade.

Quando make check-valgrind é executado, ele salva seus resultados em test-suite-*.log, um arquivo de registro por ferramenta. Note que você precisará executá-lo a partir de um diretório contendo os testes de unidade.

Valgrind possui uma forma de suprimir falsos positivos, usando arquivos de supressão. Estes listam padrões que pode corresponder a rastros de pilhas de erros. Se um rastro de pilha de um erro corresponde a parte de uma entrada de supressão, ele não é relatado. Por vários motivos, GLib atualmente causa vários falsos positivos no memcheck e helgrind e drd, o que deve ser suprimido por padrão para Valgrind ser útil. Por este motivo, todo projeto deve usar um arquivo de supressão padrão de GLib, bem como um específico do projeto.

Há suporte a arquivos de supressão na macro AX_VALGRIND_CHECK:

@VALGRIND_CHECK_RULES@ VALGRIND_SUPPRESSIONS_FILES = meu-projeto.supp glib.supp EXTRA_DIST = $(VALGRIND_SUPPRESSIONS_FILES)
memcheck

O memcheck é um analisador de uso e alocação de memória. Ele detecta problemas com acessos e modificações de memória da heap (alocações e liberações). É uma ferramenta altamente robusta e madura, e sua saída pode ser totalmente confiada. Se ele diz que “definitely” é um vazamento de memória, definitivamente há um vazamento de memória que deve ser corrigido. Se ele diz que “potentially” é um vazamento de memória, pode haver um vazamento a ser corrigido, ou pode ser uma memória alocada em tempo de inicialização e usada ao longo da vida do programa sem precisar ser liberada.

Para executar memcheck manualmente em um programa instalado, use:

valgrind --tool=memcheck --leak-check=full nome-do-meu-programa

Ou, se está executando seu programa a partir do diretório fonte, use o seguinte para evitar a execução de verificação de vazamento em scripts auxiliares do libtool:

libtool exec valgrind --tool=memcheck --leak-check=full ./nome-do-meu-programa

Valgrind lista cada problema de memória que ele detecta, junto com um rastro curto (se você compilou seu programa com símbolos de depuração), permitindo que a causa do erro de memória seja apontada e corrigida.

Um tutorial completo sobre uso do memcheck está aqui.

cachegrind e KCacheGrind

cachegrind é um perfilador de desempenho do cache que também pode medir a execução de uma instrução e, portanto, é muito útil para perfilar desempenho em geral de um programa. KCacheGrind é uma interface gráfica útil para isso que permite visualização e exploração de dados de perfil, e as ferramentas raramente devem ser usados separadamente.

cachegrind funciona simulando a hierarquia de memória do processador, de forma que há situações em que não é perfeitamente acurado. Porém, seu resultado é sempre suficientemente representativo para ser muito útil em depurar pontos de desempenho.

Um tutorial completo sobre uso do cachegrind está aqui.

helgrind e drd

helgrind e drd são detectores de erro em threads, verificações de condições de corrida em acessos de memória e abusos do API pthredas do POSIX. Elas são ferramentas similares, mas são implementadas usando técnicas diferentes, de forma que ambas deveriam ser usadas.

Os tipos de erros detectados pelo helgrind e drd são: dados acessados por múltiplas threads sem travamento consistente, alterações na ordem de aquisição de trava, liberação um mutex enquanto ele está travado, travamento de um mutex travado, destravamento de um mutex destravado e vários outros erros. Cada erro, quando detectado, é emitido para o console em um pequeno relatório, com um relatório separado fornecendo os detalhes de alocação ou criação de mutexes ou threads envolvidos, de forma que suas definições podem ser localizadas.

helgrind e drd podem produzir mais falsos positivos que memcheck ou cachegrind, de forma que suas saídas podem ser estudadas com um pouco de cuidado. Porém, problemas em threads são notoriamente elusivos mesmo para programadores mais experientes, de forma que erros de helgrind e drd não devem ser desconsiderados.

Tutorias completos sobre usar helgrind e drd estão aqui e aqui.

sgcheck

sgcheck é um verificador de limites de vetor que detecta acessos a vetores que podem ter ultrapassados o tamanho do vetor. Porém, é uma ferramenta muito jovem, ainda marcada como experimental e, portanto, pode produzir muitos mais falsos positivos que outras ferramentas.

Por ser experimental, sgcheck deve ser executado passando --tool=exp-sgcheck para o Valgrind, em vez de --tool=sgcheck.

Um tutorial completo sobre uso do sgcheck está aqui.

gcov e lcov

gcov é uma ferramenta de perfilamento construída em GCC que instrumenta código adicionando instruções extras em tempo de compilação. Quando o programa é executado, esse código gera os arquivos de saída de perfilamento .gcda e .gcno. Esses arquivos podem ser analisados pela ferramenta lcov, que gera relatórios visuais de cobertura de código em tempo de execução, linhas destacadas de código no projeto que executa mais do que outros.

Um uso crítico para essa coleção de dados de cobertura de código é quando se está executando testes de unidade: se a quantidade de código coberto (por exemplo, quais linhas em particular foram executadas) pelos testes de unidade é conhecida, ela pode ser usado para guiar expansões posteriores de testes de unidade. Ao verificar regularmente a cobertura de código atingido pelos testes de unidade e expandindo-os para 100%, você pode se certificar de que todo o projeto está sendo testado. Frequentemente é o caso de que um teste de unidade exercita a maioria do código, mas não um caminho de fluxo de controle em particular, o qual acaba por abrigar erros residuais.

lcov oferece suporte a medida de cobertura de ramo, de forma que é seja adequado para demonstrar cobertura de código crítico de segurança. É perfeitamente adequado para código crítico que não seja de segurança.

Como a cobertura de código tem que estar habilitada em ambos tempo de compilação de execução, uma macro é fornecida para simplificar as coisas. A macro AX_CODE_COVERAGE adiciona um alvo de make check-code-coverage ao sistema de compilação, o qual executa os testes de unidade com cobertura de código habilitada e gera um relatório usando lcov.

Para adicionar suporte ao AX_CODE_COVERAGE a um projeto:

Copie ax_code_coverage.m4 para o diretório m4/ de seu projeto.

Adicione AX_CODE_COVERAGE para configure.ac.

Adicione @CODE_COVERAGE_RULES ao Makefile.am de topo de nível.

Adicione $(CODE_COVERAGE_CFLAGS) às variáveis *_CFLAGS do automake para cada alvo ao qual você deseja cobertura, por exemplo para todas bibliotecas com nenhum código de teste de unidade. Faça o mesmo para $(CODE_COVERAGE_LDFLAGS) e *_LDFLAGS.

A documentação sobre o uso de gcov e lcov está aqui.

Sanitizadores de endereço, thread e comportamentos indefinidos

GCC e Clang oferecem suporte a vários sanitizadores: conjuntos de código extra e verificações que opcionalmente podem ser compilados nele para um aplicativo e usado para sinalizar vários comportamentos incorretos em tempo de execução. Elas são ferramentas poderosas, mas em especial têm que estar habilitadas, recompilando seu aplicativo para habilitá-los e desabilitá-los. Eles não podem estar habilitados ao mesmo tempo que um ao outro, ou usado ao mesmo tempo que Valgrind. Eles ainda estão jovens, então possuem pouca integração com outras ferramentarias.

Todos os sanitizadores estão disponíveis para GCC e Clang, aceitando o mesmo conjunto de opções de compilador.

Sanitizador de endereço

Também conhecido como “address sanitizer” (“asan”), ele detecta erros de uso-após-liberação e estouro de buffer em programas C e C++. Um tutorial completo sobre o uso de asan está disponível para Clang — as mesmas instruções devem funcionar para GCC.

Sanitizador de thread

Também chamado de “thread sanitizer” (“tsan”), ele detecta corridas de dados em localizações de memória, além de uma variedade de usos inválidos de APIs de thread do POSIX. Um tutorial completo sobre o uso de tsan está disponível para Clang — as mesmas instruções devem funcionar para GCC.

Sanitizador de comportamento indefinido

Também chamado de “undefined behavior sanitizer” (“ubsan”), ele é uma coleção de instrumentações menores que detectam vários comportamentos indefinidos em potencial em programas C. Um conjunto de instruções para habilitar ubsan está disponível para Clang — as mesmas instruções devem funcionar para o GCC.

Coverity

Coverity é uma das maiores e mais populares ferramentas comerciais de análise estática disponível. Porém, está disponível para uso para projetos de código aberto e qualquer projeto é encorajado a se inscrever. A análise é realizada executando algumas ferramentas de análise localmente e, então, enviando o código-fonte e resultados como um tarball para o site do Coverity. Os resultados ficam, então, visíveis online para todos os membros do projeto, como anotações do código-fonte do projeto (similarmente a como lconv apresenta seus resultados).

Como o Coverity não pode ser executado inteiramente localmente, ele não pode ser integrado apropriadamente no sistema de compilação. Porém, scripts existem para varrer automaticamente um projeto e enviar o tarball periodicamente para Coverity. A abordagem recomendada é executar esses scripts periodicamente em um servidor (geralmente como um cronjob), usando um checkout limpo do repositório git do projeto. Coverity automaticamente envia e-mail para membros do projeto sobre novos problemas na análise estática que forem encontrados, de forma que a mesma abordagem que os avisos de compilador possam ser levados: elmitar todos os avisos de análise estática

Coverity é bom, mas não é perfeito e produz um número de falsos positivos. Esses devem ser marcados como ignorados na interface online.

Analisador estático do Clang

Uma ferramenta que pode ser usada para realizar análise estática localmente é o analisador estático do Clang, que é uma ferramenta co-desenvolvida com o compilador Clang. Ele detecta uma variedade de problemas no código C que compiladores não conseguem, e que, do contrário, seria detectável apenas em tempo de execução (usando testes de unidade).

Clang produz alguns falsos positivos e não há uma forma fácil de ignorá-los. O recomendado é preencher um relatório de erro para o analisador estático, de forma que o falso positivo possa ser corrigido no futuro.

Um tutorial completo sobre o uso Clang está aqui.

Tartan

Porém, apesar de todos os poderes do analisador estático do Clang, ele não pode detectar problemas com bibliotecas específicas, como o GLib. Isso é um problema, se um projeto usa exclusivamente o GLib, e raramente usa APIs do POSIX (o qual Clang entende). Há um plug-in disponível para o analisador estático do Clang, chamado Tartan, que estende o Clang de forma a oferecer suporte a verificações de algumas das APIs comuns do GLib.

Tartan ainda é um software jovem, então ele produzirá falsos positivos e pode travar quando executado em algum código. Porém, ele pode localizar erros legítimos bem rapidamente, e compensa executar frequentemente sobre um código base para detectar novos erros no uso do GLib no código. Por favor, relate quaisquer problemas com Tartan.

Um tutorial completo sobre habilitar Tartan para uso com o analisador estático do Clang está aqui. Se configurado corretamente, a saída do Tartan será misturada com a saída normal do analisador estático.