Seja prudente, faça a limpeza: mantenha seu código em C++ livre de bugs
Apesar de todas as perdas que causou, essa pandemia pelo menos nos deixou mais conscientes em relação à nossa higiene pessoal.
Estamos desinfetando ambientes, superfícies e nossas mãos com muito mais frequência; então, por que não higienizar nosso código já que estamos nisso? Afinal, o software comanda o mundo, e os bugs que causam o mau funcionamento dos programas podem causar sérios danos — assim como seus equivalentes virais.
Se você desenvolve em C e C++, sabe disso muito bem. É fácil alocar um pedaço de memória e esquecer de liberá-lo depois, ou acidentalmente gravar além do buffer de memória. Esses problemas são extremamente difíceis de detectar sem as ferramentas adequadas e costumam causar travamentos esporádicos e repentinos.
O uso de sanitizers durante a criação e o teste do seu programa pode ajudar a detectar antecipadamente uma grande quantidade de problemas no código-fonte, incluindo vazamentos de memória, estouros de buffer e comportamentos indefinidos.
Hoje, vamos dar uma olhada em três tipos de sanitizadores do Clang, como eles são usados e quais erros eles podem nos ajudar a eliminar logo no início.
Limpeza do espaço de endereços com o AddressSanitizer (ASan)
O AddressSanitizer (abreviado como ASan) é utilizado para detectar erros do tipo “use-after-free”, “double-free”, estouros e subfluxos de buffer (pilha, heap e buffer global), além de outros erros de memória.
It consists of both a compiler instrumentation module and a run-time library that inserts red zones around each set of bytes allocated with the malloc function. It also poisons the freed bytes and keeps track of the call stack for each malloc/free pair.
É assim que nosso código fica sem o ASan:
E eis o que o ASan faz para detectar erros relacionados a endereços:
Using ASan is as simple as adding the -fsanitize=address as both compiler and linker flag. You can also set a large number of run-time options via the ASAN_OPTIONS environment variable (here is a full list of ASan flags you can use).
Para entender de verdade o quanto essa ferramenta é útil, aqui vai um pequeno teste para você. Tente encontrar um bug no trecho de código abaixo:
char const * src{ "Hello world!" };
auto const dst{ std::make_unique< char[] >( std::strlen( src ) ) };
std::strcpy( dst.get(), src );
std::puts( dst.get() );
É difícil, não é? Agora, imagine que esse bug fizesse parte de uma base de código muito maior. Levaríamos uma eternidade para depurá-lo manualmente, enquanto que, com o ASan ativado, podemos identificar o estouro de buffer no heap oculto em segundos (veja a demonstração):
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c at pc 0x00000044b754 bp 0x7ffc9ea586d0 sp 0x7ffc9ea57e80
WRITE of size 13 at 0x60200000001c thread T0
#0 0x44b753 (/app/output.s+0x44b753)
O ASan também emitirá um aviso caso seu programa continue a usar um ponteiro após ele ter sido liberado (uma vulnerabilidade de segurança comum conhecida como erro de uso após liberação). Aqui está um exemplo de programa que contém esse bug:
constexpr std::size_t bodyOffset{ 96u };
char * message{ new char[ 1024 ] };
// fill in the message
char * bodyBegin{ message + bodyOffset };
delete [] message;
// many lines of code ...
std::puts( bodyBegin ); // heap-use-after-free bug
E aqui está um relatório que você recebe do ASan após testar o código:
==1==ERROR: AddressSanitizer: heap-use-after-free on address 0x6190000000e0 at pc 0x000000470a69 bp 0x7ffe047e6b90 sp 0x7ffe047e6340
READ of size 929 at 0x6190000000e0 thread T0
Muito legal, né?
Agora, você deve estar se perguntando: quanto isso custa? Bem, nada é de graça no mundo de hoje e, infelizmente, o mesmo vale para o ASan.
Ainda assim, se você levar em conta o tempo que essa ferramenta pode economizar, ela não é nada cara. A sobrecarga média é de aproximadamente o dobro, tanto em termos de desempenho quanto de uso de memória.
Falando em desempenho, o principal concorrente do ASan, o Valgrind, causará uma redução de velocidade de 10 a 100 vezes maior no seu programa. Na nossa opinião, essa é uma melhoria significativa.
Agora que limpamos nosso espaço de endereços, vamos fazer mais algumas medidas de higienização da nossa memória.
Detecção de leituras de memória não inicializada com o MemorySanitizer (MSan)
Você pode pensar que o AddressSanitizer cobre todos os bugs relacionados à memória, mas não é bem assim. Ele não lida com leituras de memória não inicializada, e é aí que entra outro sanitizador chamado MemorySanitizer (MSan, para abreviar).
O MSan permite que você copie memória não inicializada e realize operações lógicas e aritméticas simples nela, mas, ao tentar usá-la em uma instrução de tomada de decisão, você receberá um aviso. Para entender melhor isso, dê uma olhada no trecho de código a seguir (demonstração):
int *src{ new int[ 5 ] };
int dst[ 5 ];
std::memcpy( dst, src, 5 ); // copying uninitialized memory, OK
if ( src[ 0 ] ) // MSan warning: use-of-uninitialized-value
{
// more code ...
}
Como você pode ver, o MSan nos alertará sobre qualquer valor não inicializado em uso. Aquele vazamento de memória que você talvez tenha notado não será sinalizado, pois isso faz parte do que o ASan lida — é importante não nos confundirmos com isso.
To get to the origin of this value, use -fsanitize-memory-track-origins flag along with the -fsanitize=memory.
Now, let’s talk a bit about how MSan actually works. It implements a bit to bit shadow mapping, as shown in the figure below, where 1 means “poisoned” or uninitialized bit. This allows for very efficient computation of the shadow memory address. Given the application memory address ProductAddr, computed ShaddowAddr is ProductAddr & ShadowMask, where ShadowMask is a platform-specific constant.
Sempre que o acesso a um dos bits contaminados causar algum efeito colateral (por exemplo, em ramificações), será exibido um aviso. Esse bit adicional gera uma sobrecarga de 2,5 vezes na CPU e de 2 vezes na memória. A sobrecarga será um pouco maior se as origens de memória também forem rastreadas — 5 vezes na CPU e 3 vezes na memória, para ser mais específico.
Detectando comportamentos indefinidos com o UndefinedBehaviorSanitizer (UBSan)
Por último, mas não menos importante, o UndefinedBehaviorSanitizer (ou UBSan, para abreviar).
UBSan will catch signed integer overflow, use of null pointers, division by zero and other undefined behavior as you’re executing your program. Apart from -fsanitize=undefined compiler flag, which checks for all kinds of bugs, there are many additional flags that can be helpful in finding more specific bugs, including:
-fsanitize=bounds
-fsanitize=vptr
-fsanitize=enum
-fsanitize=signed-integer-overflow
-fsanitize=null
-fsanitize=unsigned-integer-overflow
-fsanitize=return
-fsanitize=integer-divide-by-zero
-fsanitize=unreachable
-fsanitize=alignment
Como você pode ver, o UBSan lida muito bem com bugs simples, como o mostrado no exemplo a seguir (demonstração):
int main()
{
int m = std::numeric_limits< int >::max();
return m + 1;
}
E eis o relatório que recebemos:
runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
O UBSan demonstrará melhor seu potencial tanto no desenvolvimento cotidiano quanto em grandes bases de código. Em relação ao desempenho, há uma sobrecarga de CPU de aproximadamente 1,25x e nenhum impacto no uso de memória (o UBSan não afeta o layout do espaço de endereços).
Pontos negativos a serem considerados
Agora que vimos como os sanitizers podem nos ajudar a escrever um código melhor, ainda precisamos mencionar algumas desvantagens do uso deles.
Em primeiro lugar, ao contrário do Valgrind, que realiza todas as verificações sem a necessidade de recompilar o código-fonte, os sanitizers exigem que o código seja recompilado. Isso pode levar algum tempo, especialmente se você estiver trabalhando com uma base de código extensa.
A segunda desvantagem (e, possivelmente, a mais importante) é o fato de que o ASan e o MSan não podem funcionar juntos. Isso significa que você precisará realizar várias execuções para testar seu software, o que, mais uma vez, pode levar um bom tempo. Talvez essa seja a razão pela qual os sanitizadores ainda não sejam muito apreciados pela comunidade C++.
Apesar de suas desvantagens, recomendamos fortemente o uso de sanitizers. Eles detectam problemas que podem parecer inofensivos para ferramentas concorrentes e não interrompem seu fluxo de trabalho com lentidões significativas.
Faz todo o sentido, portanto, integrá-los aos seus processos de desenvolvimento “rápidos”, como integração contínua ou pipelines de pull requests.
No entanto, não vamos descartar o Valgrind tão cedo, pois ele ainda pode ser muito útil para nós. Falaremos sobre suas vantagens em um dos próximos posts.
Por enquanto é só — fiquem ligados nas próximas postagens interessantes e lembrem-se de que a criação de software confiável começa com um código limpo.