Valgrind: uma ferramenta esquecida nas sombras ou uma ferramenta séria de depuração?

Last updated

Há alguns meses, falamos sobre os sanitizers do C++ — pequenas ferramentas úteis que nos ajudam a manter nosso código limpo e livre de erros de memória.

Hoje, vamos dar um passo atrás no tempo e falar sobre uma ferramenta mais antiga, mas ainda assim muito poderosa, chamada Valgrind. Ela vem auxiliando os desenvolvedores há bastante tempo, pois detecta uma ampla variedade de erros, como leituras de memória não inicializada, estouros de buffer na pilha, vazamentos de memória, impasses, etc.

Depois que o Clang lançou sua coleção de sanitizadores, o Valgrind ficou em segundo plano, mas, como você verá nesta postagem do blog, há alguns casos em que o Valgrind realmente deixa os sanitizadores comendo poeira. 

Uma fera sob o capô

Uma das maiores vantagens do Valgrind em relação aos sanitizadores é o fato de ele não exigir que o software seja instrumentado antes da verificação. Isso significa que ele pode ser usado para depurar qualquer tipo de software “direto da caixa”, sem a necessidade de acesso ao código-fonte.

Isso não seria possível sem a “fera” que se esconde por trás do Valgrind. Resumindo, assim que o Valgrind é inicializado, ele assume o controle do seu programa e o executa em uma CPU simulada fornecida pelo núcleo do Valgrind. Em seguida, ele insere seu próprio código de instrumentação, dependendo do tipo de erro que você deseja detectar. 

Ao contrário dos sanitizadores, o Valgrind utiliza a Instrumentação Binária Dinâmica (DPI) e a compilação Just-In-Time (JIT) para incorporar o código de instrumentação ao código do nosso software, ou seja, para interceptar chamadas à função de alocação a fim de armazenar algumas informações adicionais. 

Here’s how it works. Each allocated block maps to a shadow block where the call stack at the time of the malloc function call is stored. Once the free function is called, Valgrind tries to find the shadow block corresponding to the address passed to free. If the block is not found, Valgrind emits an error message. Otherwise, the block is added to the queue and gets marked as inaccessible. This way, it is possible to detect invalid access to freed memory. However, please note that blocks can be removed from the queue once the system runs out of free space.

Quer saber mais?
Entre em contato hoje mesmo para falar com um especialista em fraude e identidade da Microblink

Flexibilidade proporcionada por uma arquitetura em camadas

Conforme mostrado na figura abaixo, o Valgrind é composto por duas camadas: o núcleo do Valgrind e o plug-in da ferramenta, que pode ser qualquer uma das ferramentas do conjunto de ferramentas do Valgrind, incluindo: 

  • Memcheck — monitora alocações de memória e identifica vazamentos de memória
  • Helgrind — detecta problemas relacionados ao multithreading (por exemplo, deadlocks, disputas de dados, etc.)
  • Cachegrind — atua como um analisador de desempenho de cache e previsão de ramificação
  • Massif — analisa o uso da memória do heap

Lembre-se de que o Valgrind é um software de código aberto e você pode criar sua própria ferramenta, se quiser. 

Essas duas camadas têm, cada uma, sua própria função. A parte central carrega o software em teste no processo e desmonta seu código. Feito isso, os fragmentos de código são repassados ao plug-in da ferramenta, que adiciona a instrumentação ao código e, por fim, o remonta. 

Uma maneira mais fácil de depurar

Conforme mencionado em nossa postagem anterior no blog, os sanitizadores de C++ exigem que seu código seja recompilado. Isso pode ser inconveniente quando você deseja testar seu código tanto com o AddressSanitizer quanto com o MemorySanitizer. Como os dois não podem funcionar juntos, você precisaria realizar várias execuções de teste para detectar leituras de memória não inicializada e outros bugs relacionados a endereços. Por outro lado, o Valgrind pode executar praticamente qualquer software tal como está. A única coisa de que ele precisa é suporte para todas as instruções que nosso software está usando.

Digamos que você queira depurar uma biblioteca cujo código-fonte está inacessível. O uso de sanitizers não traria resultados porque, ao contrário do Valgrind, eles atuam no nível do compilador. Com o Valgrind, você pode lidar com esses casos por padrão, mas lembre-se de que pode acabar recebendo mensagens de erro que não terão muito sentido para você, já que não tem controle sobre esse código. No entanto, você tem a liberdade de filtrar essas mensagens, registrando-as em um arquivo de supressão que é lido quando o Valgrind é iniciado. 

Além de um processo de depuração mais ágil, o Valgrind é uma excelente alternativa em plataformas nas quais os sanitizadores não são compatíveis. O Apple Clang, por exemplo, não vem com o LeakSanitizer, o que torna o Valgrind sua melhor alternativa, a menos que você esteja disposto a mudar para outro compilador. 

O desempenho como o maior ponto negativo

Vamos dar uma olhada em uma versão ligeiramente modificada do exemplo da nossa primeira postagem sobre sanitizadores em C++: 

char const * src{ "Hello world!" };
auto const dst{ std::make_unique< char[] >( std::strlen( src ) ) };

for ( auto i{ 0ul }; i < 1000000; ++i )
{
std::strcpy( dst.get(), src );
}

std::puts( dst.get() );

A for loop has been added to emphasize the performance difference between AddressSanitizer and Valgrind. In both cases, the code is compiled using the following command (running ASan also requires us to add -fsanitize=address):

clang++ example.cpp -g -o example.out

Uma rápida olhada no gráfico abaixo revela uma enorme diferença de desempenho entre o Valgrind e o ASan, e é por isso que muitos desenvolvedores podem se mostrar relutantes em usar a ferramenta desde o início.

Comparação lado a lado 

Para entender melhor quais erros o Valgrind e os sanitizadores são capazes de detectar, vamos dar uma olhada na tabela a seguir: 

* O MSan detecta
leituras de memória não inicializada** O UBSan detecta comportamento indefinido

As you can see, Valgrind won’t help you catch overflows in stack and global variables. This is because it only has access to the heap allocations performed by malloc function. Also, don’t count on it detecting any undefined behavior in your code — though you’ll get warned about trying to access memory that resulted from undefined behavior.  

Ao mesmo tempo, o ASan também não é tão perfeito assim. Ele não detecta leituras de memória não inicializada nem comportamentos indefinidos, ao passo que o MSan e o UBSan o fazem. 

Valgrind ou sanitizers: o que devemos usar?

A questão que permanece, então, é: devemos usar o Valgrind ou os sanitizers são uma alternativa melhor? Não há uma resposta correta para essa pergunta, pois essas duas ferramentas funcionam de maneiras muito diferentes. O ideal é usar ambas, dependendo do seu ambiente e dos erros que você deseja detectar.

Embora os sanitizers atuais imponham uma sobrecarga muito menor na CPU e ofereçam uma gama mais ampla de erros detectados, eles ainda apresentam desvantagens. Eles funcionam no nível do compilador, o que significa que é necessário ter o código-fonte. Além disso, exigem que você recompile seu código a cada execução de teste, levam mais tempo para serem integrados e não oferecem suporte em certas plataformas.  

Se você estiver trabalhando em um projeto grande e não quiser se preocupar em recompilar seu código, pode fazer mais sentido optar pelo Valgrind. Achamos ele especialmente útil na depuração de bibliotecas de código fechado e na detecção mais fácil de problemas como bugs do tipo “use-after-free”. No entanto, como os sanitizers estão ficando cada vez melhores, tendemos a preferi-los ao Valgrind.

No fim das contas, independentemente das ferramentas que você use, o mais importante é tornar seu código o mais seguro possível. Com isso em mente, fique ligado para mais posts interessantes sobre depuração em C++.

Descubra nossas soluções

Para explorar nossas soluções, você está a apenas um clique de distância. Experimente nossos produtos ou converse com um de nossos especialistas para se aprofundar no que oferecemos.

Relatório
Analisando o aumento das fraudes de identidade impulsionadas pela IA

A IA não se limitou a tornar a fraude mais rápida. Ela a transformou em um sistema. Analisamos milhões de interações relacionadas à identidade para mapear como os ataques de identidade estão evoluindo em diferentes regiões, tipos de ataque e níveis de sofisticação — e o que as organizações precisam repensar para acompanhar essa evolução.

Veja os dados