- Aprender
- Computação e TI
- Programação
- Programação em C
- Depuração em C
Depuração, comportamento indefinido e C seguro
BásicoAula 8 de 8 do curso Curso de Programação em C
O que você vai aprender
- Ative os avisos do compilador, leia seus diagnósticos e escolha configurações de compilação que tornem os defeitos visíveis cedo, antes mesmo da execução do programa.
- Ative os avisos e leia as mensagens do compiladorCompile com um conjunto rigoroso de opções de aviso, leia cada diagnóstico linha por linha e corrija a causa em vez de silenciar a mensagem.
- Escolha configurações de compilação para depuração, testes e distribuiçãoUse versões separadas para depuração e distribuição, saiba o que -g e os níveis de otimização alteram e adicione análise estática e opções de reforço de segurança onde forem necessárias.
- Ative os avisos e leia as mensagens do compilador
- Investigue defeitos com método: reproduza e isole a falha, depois use gdb ou lldb para pausar o programa, percorrê-lo passo a passo, inspecionar valores e encontrar a verdadeira origem de um encerramento inesperado ou valor incorreto.
- Depure com método: reproduza, isole e teste uma hipóteseTransforme uma falha vaga em uma falha precisa, leia o que a mensagem de encerramento informa e restrinja as possíveis causas com pequenos experimentos controlados.
- Execute, pause e percorra um programa passo a passo no gdb ou lldbInicie um programa no gdb ou lldb, defina pontos de parada, avance linha por linha e inspecione variáveis e arranjos enquanto o programa estiver pausado.
- Diagnostique encerramentos inesperados e valores imprevistosUse a pilha de chamadas após um encerramento inesperado, mova-se entre registros de ativação e use pontos de observação, pontos de parada condicionais e despejos de memória do processo para encontrar a origem de um valor incorreto.
- Depure com método: reproduza, isole e teste uma hipótese
- Explique o que é comportamento indefinido e por que ele torna programas em C imprevisíveis, e reconheça suas fontes mais comuns em operações aritméticas, expressões, ponteiros e memória.
- Compreenda o significado de comportamento indefinidoDiferencie comportamento indefinido, definido pela implementação e não especificado e explique por que código com comportamento indefinido pode parecer funcionar e depois falhar.
- Identifique comportamento indefinido em operações aritméticas e expressõesReconheça estouro de inteiro com sinal, divisões e deslocamentos de bits inválidos, conversões fora da faixa representável, efeitos colaterais não sequenciados e leituras de valores não inicializados, e escreva as verificações que os evitam.
- Identifique comportamento indefinido no uso de ponteiros, arranjos e memóriaReconheça acessos fora dos limites, cadeias de caracteres inválidas, ponteiros nulos e pendentes, uso após a liberação e tamanhos de alocação incorretos, e conheça o padrão seguro para cada caso.
- Compreenda o significado de comportamento indefinido
- Faça um programa verificar suas próprias suposições durante a execução: expresse-as com asserções e compile versões de teste com sanitizadores que param em erros de memória e comportamento indefinido e informam exatamente onde ocorreram.
- Expresse suposições com asserçõesUse assert para condições que apenas um defeito pode tornar falsas, entenda o que NDEBUG faz e mantenha as asserções separadas do tratamento de erros esperados.
- Encontre erros de memória com AddressSanitizerCompile uma versão de teste com AddressSanitizer, leia seus relatórios de erro e saiba o que ele encontra, o que deixa passar e qual é seu custo.
- Detecte comportamento indefinido com UBSan e estabeleça uma rotina de verificaçãoUse UndefinedBehaviorSanitizer e conheça os outros sanitizadores; depois, combine avisos, sanitizadores e testes em uma rotina que detecte erros cedo.
- Expresse suposições com asserções
- Aplique regras de codificação segura, como as do SEI CERT C, a cadeias de caracteres, buffers, entrada e erros, e avalie os custos da falta de segurança de memória em C em relação ao que a linguagem oferece.
- Use um padrão de codificação seguraExplique como o Padrão de codificação segura em C do SEI CERT é organizado, leia uma regra com seus exemplos e conheça as regras centrais que evitam os defeitos mais comuns em C.
- Manipule cadeias de caracteres e buffers com segurançaSaiba sempre qual é o tamanho de um buffer, leia e copie cadeias de caracteres com funções que respeitem esse tamanho e detecte truncamentos.
- Valide a entrada e trate errosTrate todos os dados externos como não confiáveis, converta texto em números com verificações completas de erro e verifique o resultado de toda chamada de biblioteca que possa falhar.
- Avalie os compromissos envolvidos na segurança de memóriaExplique o que é segurança de memória, por que C não a oferece, quais são os custos disso e o que C oferece em contrapartida, e como camadas de defesa e linguagens com segurança de memória reduzem o risco.
- Use um padrão de codificação segura
Guia de estudo de Depuração em C
1Use o compilador para encontrar problemas
O compilador pode apontar muitos problemas antes de você executar um programa. Aprenda a ler suas mensagens como evidências sobre seu código e depois escolha opções que revelem mais do que importa. Informações de depuração, otimização, análise estática e reforço de segurança para distribuição cumprem funções diferentes. Manter essas funções claras ajuda você a compilar um programa que pode inspecionar durante o desenvolvimento e testar novamente na forma que seus usuários executarão.
1.1 Ative os avisos e leia as mensagens do compilador
Os avisos dão a você a chance de corrigir um defeito antes que uma entrada o desencadeie. Um erro impede que a compilação seja concluída. Um aviso permite que a compilação continue, mas informa algo que você precisa entender: código válido suspeito, possível comportamento indefinido ou uma violação das regras da linguagem que o compilador aceita.
Escolha verificações úteis
Inicie GCC ou Clang com uma versão explícita da linguagem e um conjunto amplo de avisos:
clang -std=c17 -Wall -Wextra -Wpedantic -g program.c -o program
-Wall ativa muitas verificações comuns, não todos os avisos. -Wextra adiciona outras, incluindo comparações entre valores com e sem sinal em C. -Wpedantic diagnostica muitas divergências em relação ao padrão ISO escolhido; não é um teste completo de conformidade. /W4 do MSVC cumpre uma função semelhante no Windows.
Adicione verificações adequadas aos riscos do seu código:
| Opção | O que ajuda a revelar |
|---|---|
-Wconversion | Conversões implícitas que podem alterar um valor |
-Wshadow | Uma declaração local que oculta outra variável |
-Wformat=2 | Verificações adicionais de cadeias de formatação |
-Werror | Faz avisos ativados causarem falha na compilação |
Por exemplo, double price = 3.7; int whole = price; descarta a parte fracionária. -Wconversion torna essa conversão visível. Se o truncamento for intencional e o valor couber no tipo, (int)price explicita a escolha. Se a parte fracionária importar, mantenha um tipo de ponto flutuante. Para arredondar ao inteiro mais próximo, lround de <math.h> tem uma função diferente de uma conversão explícita.
Leia a primeira mensagem útil
Um diagnóstico normalmente identifica um local no código-fonte, uma mensagem e a opção de aviso. Um acento circunflexo marca a expressão relevante; notas seguintes podem identificar uma declaração ou sugerir uma correção. Leia o grupo inteiro. Quando um erro de sintaxe causar muitas mensagens posteriores, corrija o primeiro erro e compile novamente antes de investigar as demais.
Suponha que um programa contenha estas três linhas, com average declarado como double:
if (count = 0) { /* report no data */ }
printf("%d\n", average);
int unused;
A atribuição na condição define count como zero, tornando a condição falsa. Se a intenção era fazer uma comparação, use count == 0. Adicionar parênteses ao redor da atribuição pode silenciar um aviso e manter o defeito intacto.
O formato %d espera um int, mas o argumento é um double. Use %f; uma incompatibilidade de formato é comportamento indefinido, não uma solicitação para converter o valor. Exclua a declaração não usada se ela não tiver nenhuma finalidade. São três causas diferentes, então cada uma precisa de sua própria correção.
Siga as evidências
Preste atenção especial a valores não inicializados, retornos ausentes, endereços de variáveis locais retornados e sizeof em parâmetros de arranjo. A falta de um cabeçalho pode deixar uma função não declarada; versões recentes de GCC e Clang rejeitam isso por padrão. Não adicione uma conversão explícita apenas para fazer um diagnóstico desaparecer. Primeiro explique os tipos e valores pretendidos, depois faça o código expressar essa intenção.
Um programa sem avisos ainda pode ter uma condição de laço incorreta ou um índice inválido fornecido pela entrada. Alguns avisos também dependem de otimização e análise de fluxo de dados, e compiladores diferentes detectam casos diferentes. Mantenha o conjunto de avisos ativado nas compilações comuns e depois investigue o comportamento em tempo de execução com o depurador e ferramentas de teste.
Principais conclusões
- Escolha explicitamente a versão de C.
-Wallnão ativa todos os avisos.- Corrija a causa de um diagnóstico antes de suprimi-lo.
- Uma conversão de formato deve corresponder ao tipo do argumento.
- Uma compilação sem avisos ainda precisa de testes em tempo de execução.
1.2 Escolha configurações de compilação para depuração, testes e distribuição
O mesmo código-fonte pode ser compilado para facilitar a inspeção, para testes intensivos ou para uma distribuição eficiente. As configurações de compilação determinam quais informações você pode ver e quais verificações adicionais são executadas. Dê uma finalidade clara a cada versão compilada.
Mantenha informações do código-fonte
A opção -g adiciona nomes, tipos e mapeamentos entre instruções de máquina e linhas do código-fonte. Ela permite que um depurador mostre seu código e ajuda relatórios de sanitizadores a identificar locais. Ela não ativa verificações em tempo de execução nem desativa a otimização. Sem informações de depuração, alguns nomes de funções ainda podem estar disponíveis, mas é muito mais difícil recuperar linhas do código-fonte e variáveis locais.
A otimização altera o quanto a execução segue a ordem do código-fonte:
| Configuração | Finalidade usual | O que você pode ver |
|---|---|---|
-O0 | Depuração direta | A ordem do código-fonte geralmente é mais fácil de acompanhar |
-Og | Depuração com otimização leve | Algumas transformações permanecem |
-O2 | Versão eficiente para distribuição | Operações reordenadas e variáveis removidas |
Uma sessão de depuração otimizada pode pular linhas ou exibir uma variável como “”. O compilador preservou o comportamento do código definido, mas pode não manter mais todos os valores intermediários do código-fonte. -O0 não traduz literalmente cada linha de C em uma instrução de máquina.
Compile para a finalidade
Um comando simples de depuração é:
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 program.c -o program-debug
Uma versão de teste adiciona sanitizadores. Uma versão para distribuição costuma usar -O2 e pode definir NDEBUG para remover asserções. A validação comum de entrada e o tratamento de erros devem permanecer ativos quando as asserções desaparecerem. Teste também a configuração de distribuição: um problema visível apenas com otimização é um forte motivo para investigar comportamento indefinido, incluindo leituras de valores não inicializados e estouro de inteiro com sinal.
Mantenha opções compartilhadas em um Makefile ou em outra configuração de compilação. Nomes de saída separados evitam que você execute acidentalmente um binário antigo de distribuição enquanto inspeciona código-fonte recém-editado. Após cada alteração no código-fonte, recompile o executável que pretende executar.
Adicione análise e reforço de segurança
A análise estática acompanha possíveis caminhos pelo código-fonte sem executar o programa. Experimente gcc -fanalyzer ou clang --analyze; scan-build, clang-tidy e cppcheck oferecem outros fluxos de análise. Um analisador pode perceber que um ramo de erro libera um ponteiro duas vezes mesmo quando seus testes nunca entram nesse ramo. Examine seu raciocínio: a análise de caminhos possíveis pode produzir alarmes falsos.
A análise dinâmica verifica a execução real. Sanitizadores e Valgrind veem apenas os caminhos que você executa, portanto sua cobertura depende das entradas. Combine os dois tipos de análise.
O reforço de segurança para distribuição limita os danos de defeitos restantes. -fstack-protector-strong insere verificações em registros de ativação selecionados para detectar muitas sobrescritas. Em sistemas compatíveis, _FORTIFY_SOURCE adiciona verificações a chamadas de biblioteca quando os tamanhos dos objetos podem ser determinados. Ela exige otimização; o nível 2 é comum, enquanto o nível 3 exige suporte adequado do compilador e da biblioteca C. Esses são recursos da plataforma, não garantias do ISO C.
Um programa com reforço de segurança pode parar quando detecta corrupção. A operação inválida ainda precisa ser corrigida no código-fonte; alterar opções de distribuição não pode corrigir um laço que ultrapassa os limites.
Principais conclusões
- Use
-gpara obter informações de depuração relacionadas ao código-fonte. - Depure com
-O0ou-Oge teste também a versão para distribuição. - Mantenha as verificações de entrada ativas quando
NDEBUGremover as asserções. - A análise estática e a análise em tempo de execução encontram caminhos e defeitos diferentes.
- Mantenha as opções de compilação em uma configuração reproduzível.
- O reforço de segurança reduz os danos causados por defeitos que permanecem.
2Encontre e corrija defeitos com um depurador
Um encerramento inesperado mostra onde a execução parou; o erro pode ter ocorrido muito antes. Comece com uma falha reproduzível e uma entrada pequena e depois use um depurador para inspecionar o programa em momentos escolhidos. Pontos de parada e execução passo a passo mostram como os valores evoluem. Registros de ativação, pontos de observação e pontos de parada condicionais ajudam a rastrear um valor incorreto até o código que o criou, para que cada experimento responda a uma pergunta específica.
2.1 Depure com método: reproduza, isole e teste uma hipótese
Uma falha é o comportamento que você vê: um encerramento inesperado, um total incorreto ou uma saída que nunca aparece. Um defeito é o erro que a causou. Manter os dois conceitos separados evita que você altere a linha do encerramento antes de descobrir como o programa chegou até ela.
Torne a falha reproduzível
Registre o comando, a entrada, as opções de compilação, o resultado esperado e o resultado real. Mantenha a entrada que causa a falha inalterada enquanto experimenta. Se um programa falhar apenas com um arquivo, salve esse arquivo antes de editá-lo. A reprodução oferece uma forma de avaliar se uma correção proposta funciona.
Em seguida, reduza o caso preservando a falha. Remova registros ou código sem relação com ela e execute novamente. Suponha que um registro entre 1.000 desencadeie um encerramento inesperado por si só. Testar metades pode localizá-lo em cerca de 10 rodadas. Se nenhuma das metades falhar sozinha, a falha pode exigir uma sequência de registros ou um estado acumulado antes. Preserve essa sequência e remova partes menores ao redor dela. Dividir ao meio só é útil quando a entrada mantida ainda exercita a causa.
Faça uma pergunta testável por vez
Um campo vazio em um arquivo reduzido sugere uma hipótese: “O analisador usa o campo antes de verificar se ele existe”. Teste-a com uma entrada pequena que contenha um campo vazio. Inspecione o ponteiro retornado ou adicione uma verificação específica. Altere uma coisa por vez; se você alterar o analisador, o tamanho do buffer e a entrada ao mesmo tempo, o sucesso não revelará qual alteração teve efeito.
Corrija a regra violada, execute novamente o caso reduzido e depois execute novamente a entrada original e os testes existentes. Guarde a entrada que desencadeia a falha como um teste de regressão para que uma edição posterior não possa restaurar a mesma falha silenciosamente.
Interprete a parada
“Segmentation fault” geralmente informa SIGSEGV: o sistema interrompeu um acesso inválido à memória. Um ponteiro nulo, um ponteiro liberado ou um endereço fora dos limites é uma causa comum. Um acesso inválido também pode não causar um encerramento imediato, portanto a ausência de mensagem, por si só, revela pouco.
“Aborted” geralmente informa SIGABRT, gerado por abort(). Uma asserção que falhou ou uma verificação da biblioteca que detectou corrupção do heap pode causá-lo. No bash, códigos de saída comuns para sinais são 139 para SIGSEGV e 134 para SIGABRT; esses números são convenções do shell, não valores de retorno de C que seu programa precise usar.
Observe sem perder a saída
Um diagnóstico curto pode revelar o primeiro valor incorreto:
fprintf(stderr, "index=%zu total=%d\n", index, total);
Rótulos tornam a saída legível quando a mesma linha é executada muitas vezes. stderr é útil para diagnósticos porque não usa armazenamento completo em buffer; stdout redirecionado para um arquivo ou canal entre processos normalmente usa armazenamento completo em buffer, então um encerramento inesperado pode fazer a saída pendente se perder. Remova as impressões temporárias após a correção.
Explique uma função suspeita linha por linha em voz alta. Esse método do “pato de borracha” obriga você a relacionar cada operação ao comportamento pretendido. Se um defeito mudar quando você adicionar impressões ou alterar a otimização, investigue comportamento indefinido com sanitizadores. Aumentar um arranjo até um encerramento inesperado desaparecer pode ocultar um índice inválido sem corrigi-lo.
Principais conclusões
- Salve uma reprodução exata antes de alterar o código.
- Reduza a entrada preservando a falha.
- Teste uma hipótese com uma alteração ou observação controlada.
- Rastreie um encerramento inesperado até sua causa, que pode estar antes no código.
- Execute novamente o caso original e mantenha um teste de regressão.
2.2 Execute, pause e percorra um programa passo a passo no gdb ou lldb
Um depurador permite pausar um programa em execução e inspecionar os valores que explicam seu comportamento. Use GDB em muitos sistemas Linux ou LLDB no macOS. Os botões de depuração das IDEs usam as mesmas ideias: parar, executar, inspecionar e avançar passo a passo.
Comece pelo executável correto
Compile com -g -O0 e depois abra o resultado com gdb ./program ou lldb ./program. No prompt, run records.txt 3 inicia o programa com dois argumentos. No GDB, run < input.txt redireciona a entrada padrão; no LLDB, use process launch -i input.txt. Use quit para sair.
Defina um ponto de parada antes de executar. break main no GDB e b main no LLDB param no início de main. Ambos aceitam uma localização por arquivo e linha, como b program.c:12. Os comandos completos do LLDB incluem breakpoint set --name main e breakpoint set --file program.c --line 12.
Um ponto de parada normalmente interrompe a execução antes que a instrução indicada no código-fonte seja executada. Se a linha selecionada atribuir um valor, imprima-o depois de avançar sobre ela para inspecionar o novo valor. Recompile após editar o código-fonte: o depurador executa código de máquina, não o texto aberto no editor.
Escolha como avançar
| Comando | Finalidade no GDB e no LLDB |
|---|---|
next ou n | Executar a linha atual, normalmente sem entrar nas chamadas |
step ou s | Entrar em uma função chamada quando as informações do código-fonte permitirem |
finish | Executar até a função selecionada retornar |
continue ou c | Retomar até outra parada ou o encerramento do programa |
Outros pontos de parada podem interromper next ou finish. Use next para uma chamada de biblioteca que você não precisa inspecionar. Se entrar em código que não deseja examinar, finish retorna o controle à função chamadora.
Inspecione um laço uma passagem por vez
Considere um total inicializado com zero e este arranjo:
int a[5] = {4, 8, 15, 16, 23};
for (int i = 0; i <= 5; ++i) {
sum += a[i];
}
Pare no corpo do laço e imprima i e sum antes da adição. Após as primeiras cinco adições, os totais acumulados são 4, 12, 27, 43 e 66. Na próxima parada, i == 5, mas os índices válidos terminam em 4. Você encontrou a iteração extra sem precisar ler o elemento inválido. Altere a condição para i < 5, recompile e execute novamente.
print expr ou p expr avalia uma expressão em qualquer um dos depuradores. Comece com observações seguras, como p i e p sum; uma expressão do depurador que desreferencia um ponteiro inválido pode tentar uma leitura inválida. display i do GDB repete a impressão a cada parada. info locals no GDB e frame variable no LLDB mostram as variáveis locais. Para cinco elementos válidos acessados por ptr, use p *ptr@5 no GDB ou parray 5 ptr no LLDB.
Você pode testar uma hipótese alterando um valor enquanto o programa está pausado: o GDB aceita set var i = 4, enquanto o LLDB aceita expr i = 4. Isso altera uma execução, não o código-fonte. Para gerenciar as paradas, use info breakpoints e delete 1 no GDB, ou breakpoint list e breakpoint delete 1 no LLDB.
Principais conclusões
- Compile e recompile o executável com informações de depuração.
- A instrução do código-fonte na qual o programa parou normalmente ainda não foi executada.
- Use
next,step,finishecontinuepara avançar de maneiras diferentes. - Inspecione índices e limites antes de avaliar expressões que acessam a memória.
2.3 Diagnostique encerramentos inesperados e valores imprevistos
Um encerramento inesperado dentro de uma biblioteca muitas vezes começa com um argumento inválido vindo do seu código. A pilha de chamadas mostra as chamadas de função ativas. Use-a para sair do local da parada e chegar à função chamadora cujos valores você pode explicar.
Leia os registros de ativação a partir da chamada atual
Digite bt em qualquer um dos depuradores. O registro #0 é a chamada ativa mais interna; os registros com números maiores são suas funções chamadoras. Um registro fornece o contexto de execução da função, incluindo os argumentos e as variáveis locais disponíveis. A otimização pode remover parte dessas informações.
Suponha que find_name(2) não encontre uma correspondência e que a função chamadora faça isto:
const char *name = find_name(2);
printf("%zu\n", strlen(name));
A parada pode ocorrer dentro de strlen. Em bt, encontre o primeiro registro que pertence ao seu código. Se ele for o registro 1, selecione-o com frame 1 no GDB ou frame select 1 no LLDB. Imprima name. Um valor nulo explica por que a operação com a cadeia de caracteres falhou. A função chamadora deve tratar a ausência do nome antes de chamar strlen; editar a biblioteca não pode corrigir o argumento.
Use up e down para se mover entre as funções chamadoras e as chamadas. Selecionar um registro altera o que o depurador exibe e a quais variáveis locais uma expressão se refere. Isso não retoma a execução nem desfaz uma chamada.
Pare onde um valor muda
Um ponto de parada pergunta: “O que acontece neste local?”. Um ponto de observação pergunta: “Qual operação altera este valor?”. Se uma contagem armazenada se tornar incorreta muito antes de um encerramento inesperado, pause enquanto a contagem estiver no escopo e use:
GDB: watch count
LLDB: watchpoint set variable count
Continue até o depurador parar em uma alteração. Inspecione os valores antigo e novo e o local no código-fonte da instrução que escreveu o valor. Uma alteração pode ser legítima, portanto compare-a com a regra da estrutura de dados. O hardware oferece apenas um pequeno número de pontos de observação; quatro é comum em x86-64. O armazenamento de uma variável local só tem significado enquanto sua função está ativa.
Para uma iteração tardia conhecida, use um ponto de parada condicional. break 20 if i == 999 do GDB pausa na linha 20 somente quando a condição é verdadeira. O equivalente no LLDB é breakpoint set --line 20 --condition 'i == 999'. Isso economiza centenas de passos manuais.
Inspecione um encerramento salvo
Um despejo de memória do processo registra o estado de um processo encerrado inesperadamente. Onde despejos estão ativados, ulimit -c unlimited os permite para programas iniciados nesse shell. Abra um despejo salvo com gdb ./program core ou lldb ./program -c core, usando o executável que o produziu. As políticas do sistema controlam se um despejo é salvo e onde ele fica; sistemas Linux baseados em systemd podem gerenciá-lo por meio de coredumpctl.
Execute bt, selecione seu registro de ativação e inspecione os valores como faria em um encerramento observado ao vivo. O despejo não permite continuar o programa. Se malloc ou free detectar corrupção, a escrita inválida original pode não estar nessa pilha. Reproduza o problema com AddressSanitizer para parar mais perto dessa escrita. Se um valor local parecer arbitrário, verifique se a execução chegou à sua inicialização antes de culpar o depurador.
Principais conclusões
- Use
btpara rastrear as chamadas ativas. - Selecione um registro de ativação no seu código e inspecione os argumentos passados à biblioteca.
- Pontos de observação param em alterações de valores; pontos de parada condicionais param em estados escolhidos.
- Um despejo de memória do processo permite a inspeção após um encerramento inesperado.
- A corrupção pode ter ocorrido antes das chamadas visíveis na pilha no encerramento final.
3Reconheça e evite comportamento indefinido
Algumas operações em C não têm resultado garantido pela linguagem. Elas podem parecer funcionar em uma versão compilada e falhar em outra, tornando a tentativa e erro comum enganosa. Aprenda a diferenciar comportamento indefinido de escolhas documentadas da plataforma e variações permitidas. Depois, examine operações aritméticas, ordem de avaliação de expressões, limites de arranjos e tempos de vida da memória, colocando verificações antes de uma operação ultrapassar um desses limites.
3.1 Compreenda o significado de comportamento indefinido
Comportamento indefinido, ou UB, significa que o padrão C não impõe requisitos sobre o que acontece após uma operação inválida. Ele não promete uma exceção, uma mensagem de erro útil ou um valor previsível após retornar ao início da faixa. Um programa pode ser encerrado inesperadamente, corromper dados, imprimir uma resposta plausível ou parecer funcionar.
Diferencie três tipos de comportamento
| Tipo | O que a linguagem permite | Exemplo em C17 |
|---|---|---|
| Indefinido | Nenhum comportamento exigido | Estouro de inteiro com sinal |
| Definido pela implementação | Uma escolha documentada da implementação | Se char simples tem sinal |
| Não especificado | Uma escolha permitida que não precisa ser documentada | Ordem de avaliação de argumentos independentes de função |
O comportamento definido pela implementação pode ser usado deliberadamente quando a plataforma documentada faz parte do projeto de um programa. Para código portável, evite presumir que todas as implementações fazem a mesma escolha. O comportamento não especificado também é válido: f(a(), b()) pode avaliar qualquer uma das chamadas independentes primeiro. Escreva código cuja correção resista a essa variação permitida. A largura de int e, em C17, o deslocamento para a direita de um inteiro negativo com sinal são outras escolhas que dependem da plataforma.
Por que a otimização revela defeitos
C deixa as verificações de limites de arranjos e de soma com sinal para o programador. O compilador pode otimizar supondo que o programa obedece às regras da linguagem. Uma tentativa de detectar estouro pode violar justamente a regra que procura testar:
int will_overflow(int x) {
return x + 1 < x;
}
Para todo x em que a soma com sinal é definida, x + 1 é maior que x. Portanto, o compilador pode substituir o resultado por zero. Em INT_MAX, fazer a soma é UB, então o compilador não deve à função chamadora um resultado que retorne ao início da faixa. Ver uma resposta diferente em uma versão compilada sem otimização não torna esse teste válido.
O teste seguro é executado antes da operação:
if (x == INT_MAX) {
/* handle overflow */
} else {
++x;
}
Use <limits.h> para obter os limites da implementação. Um máximo fixado no código pressupõe uma determinada largura de tipo; a macro identifica o limite real.
Coloque as verificações antes do primeiro uso
Essa ordem também é essencial para ponteiros:
if (p == NULL) {
return -1;
}
int v = *p;
Inverter essas instruções permite que uma desreferenciação nula ocorra antes da proteção. Depois que o código desreferencia p, um otimizador pode concluir que o caminho definido já exige que p não seja nulo e remover uma verificação de nulidade posterior.
Alterar opções de compilação, adicionar uma impressão ou mover uma variável pode mudar o sintoma visível de UB. Uma execução bem-sucedida estabelece apenas o que aquela versão compilada fez para aquela entrada. Para corrigir o código, identifique qual operação não tem significado definido e qual condição deve ser verdadeira antes dela. Avisos podem identificar alguns casos; a análise estática pode explorar caminhos possíveis; ASan e UBSan verificam muitas operações executadas. Regras de codificação fornecem os padrões correspondentes no código-fonte, como verificar um índice ou um limite aritmético antes de usá-lo.
Principais conclusões
- UB não garante um encerramento inesperado nem um resultado estável.
- Escolhas definidas pela implementação devem ser documentadas.
- Escolhas não especificadas permanecem dentro das regras da linguagem.
- Otimizadores podem presumir que estouro de inteiro com sinal nunca ocorre em código definido.
- Valide um ponteiro ou limite aritmético antes da operação arriscada.
3.2 Identifique comportamento indefinido em operações aritméticas e expressões
Uma operação aritmética pode falhar antes que você tenha a chance de inspecionar seu resultado. Verifique os operandos enquanto seus valores ainda são válidos e saiba qual tipo executa cada operação.
Limites de tipos com e sem sinal
O estouro de inteiro com sinal é indefinido. Com um int de 32 bits, INT_MAX é 2.147.483.647; somar um não é uma forma portável de obter um número negativo. INT_MIN / -1, -INT_MIN e uma multiplicação excessiva também falham quando o resultado matemático não cabe no tipo.
A aritmética sem sinal sofre redução modular dentro da faixa de seu tipo. Para um unsigned int de 32 bits, UINT_MAX + 1u é zero, e 0u - 1u é 4.294.967.295. Isso é definido, mas ainda pode prejudicar um cálculo de tamanho. Um laço decrescente sem sinal que usa i >= 0 nunca alcança um valor negativo de parada.
Para uma soma geral com sinal, teste antes de somar:
if ((b > 0 && a > INT_MAX - b) ||
(b < 0 && a < INT_MIN - b)) {
/* report overflow */
} else {
result = a + b;
}
As subtrações usadas nesses testes permanecem na faixa porque cada uma é protegida pela verificação do sinal de b. Calcular primeiro a soma e depois compará-la é tarde demais. Um tipo de maior faixa só ajuda quando consegue armazenar todos os resultados intermediários: converta um operando antes da operação. C23 fornece ckd_add em <stdckdint.h>; GCC e Clang oferecem __builtin_add_overflow.
Divisão, deslocamentos de bits e conversões
Rejeite zero antes de uma divisão inteira ou operação de resto. Rejeite também a divisão do mínimo de um tipo com sinal por menos um quando o resultado não puder ser representado. Sistemas de ponto flutuante IEEE 754 costumam produzir infinito ou NaN para divisão por zero, permitindo que resultados numéricos inválidos se propaguem sem uma interrupção como a de operações inteiras.
Para operações com bits, use operandos sem sinal. A quantidade de deslocamento deve ser não negativa e menor que a largura do operando esquerdo após a promoção. Com operandos de 32 bits, deslocar por 32 é indefinido. Em C17, deslocar um valor negativo com sinal para a esquerda é indefinido; 1 << 31 também é indefinido para um int de 32 bits, enquanto 1u << 31 é válido para um unsigned int de 32 bits.
Uma conversão de ponto flutuante para inteiro exige que o valor truncado caiba no tipo, mesmo quando o destino é sem sinal. (unsigned)-1.0 é indefinido. A conversão de inteiro para um tipo sem sinal, por sua vez, reduz o valor à faixa do destino: (unsigned)-1 se torna UINT_MAX. A conversão de um inteiro fora da faixa para um tipo com sinal é definida pela implementação em C17. Verifique as faixas antes de converter para um tipo de menor faixa; -Wconversion ajuda a revelar alterações implícitas.
Torne a ordem de avaliação inofensiva
printf("%d %d\n", i, ++i); lê e altera i sem a ordenação necessária. i = i++ + 1; e a[i] = i++; também contêm operações não sequenciadas. Divida o trabalho em instruções separadas com uma ordem pretendida explícita.
Inicialize variáveis automáticas antes de lê-las: int sum = 0; torna sum += x; significativo. Variáveis globais e estáticas começam em zero. A exigência de representação em complemento de dois do C23 não define o estouro de inteiro com sinal; código portável ainda precisa das verificações.
Principais conclusões
- Verifique a aritmética com sinal antes de calcular o resultado.
- A redução modular sem sinal é definida, mas pode tornar tamanhos e laços incorretos.
- Verifique os operandos da divisão e as quantidades de deslocamento de bits.
- A conversão de ponto flutuante para tipo sem sinal difere da conversão de inteiro para tipo sem sinal.
- Sequencie as alterações explicitamente e inicialize os valores antes do uso.
- C23 ainda deixa o estouro de inteiro com sinal indefinido.
3.3 Identifique comportamento indefinido no uso de ponteiros, arranjos e memória
Um ponteiro precisa de mais do que um endereço plausível. O objeto deve existir, o acesso deve permanecer dentro de seus limites e a operação deve ser permitida para esse objeto. Essas verificações explicam tanto encerramentos inesperados imediatos quanto corrupção descoberta muito depois.
Permaneça dentro do objeto
Um arranjo de n elementos tem índices de zero a n - 1. Um ponteiro para a posição imediatamente após o último elemento pode marcar o fim de um laço, mas você não deve desreferenciá-lo. i <= n visita um elemento a mais. A regra CERT ARR30-C trata desses erros de limites.
Uma escrita fora dos limites pode atingir memória acessível que pertence a outro objeto. O programa pode continuar até que uma contagem, um ponteiro ou um registro do alocador danificado seja usado. Um encerramento posterior em free não identifica a escrita original. O ASan pode parar na escrita quando suas verificações detectam a violação de limites.
Forme uma cadeia de caracteres válida
Uma cadeia de caracteres em C termina com '\0'. Seu armazenamento precisa de um byte além da contagem de caracteres. C aceita char s[5] = "hello";, mas todos os cinco bytes contêm letras, sem deixar espaço para o terminador. Passá-la para strlen ou %s pode causar uma leitura além do arranjo. Use espaço suficiente e garanta que um terminador esteja realmente presente. STR31-C trata da capacidade; STR32-C trata da passagem de uma cadeia terminada em nulo.
Um literal de cadeia de caracteres não pode ser modificado. Após char *p = "hi";, p[0] = 'H'; é indefinido. Use char p[] = "hi"; para obter uma cópia modificável, ou const char *p para tornar visíveis ao compilador as tentativas de alterar o literal. Esse é o tema de STR30-C.
Respeite o tempo de vida e a responsabilidade pela liberação
Verifique se há NULL antes de desreferenciar um resultado de alocação, abertura de arquivo ou busca. Quando um bloco é liberado, todos os ponteiros para seu interior perdem o acesso a um objeto ativo. Não leia nem escreva por meio deles e não libere o bloco novamente. Libere apenas o ponteiro apropriado retornado por uma alocação dinâmica, nunca um arranjo local ou um ponteiro para o interior de um objeto. free(NULL) é inofensivo, mas definir um ponteiro como nulo após a liberação não corrige outras referências ao mesmo objeto.
Uma função que retorna o endereço de um arranjo local retorna um ponteiro pendente:
char *greeting(void) {
char text[16] = "Welcome";
return text;
}
O tempo de vida do arranjo termina quando a função retorna. Forneça à função chamada um buffer e sua capacidade para que ela o preencha, ou aloque um resultado cuja responsabilidade pela liberação passe à função chamadora. Uma impressão bem-sucedida imediatamente após o retorno não pode prolongar o tempo de vida do arranjo antigo.
Aloque bytes para cada elemento
malloc(n) solicita bytes, não n objetos do tipo apontado. A expressão usual baseada no elemento é n * sizeof *a. Primeiro impeça que esse produto sofra redução modular:
if (n > SIZE_MAX / sizeof *a) {
/* reject the requested size */
}
Inclua <stdint.h> para SIZE_MAX e <stdlib.h> para funções de alocação. Quando o tamanho for válido, aloque, verifique o ponteiro retornado e escreva cada valor antes de lê-lo. malloc deixa a memória não inicializada. calloc(n, sizeof *a) preenche seu armazenamento com zeros, e as principais bibliotecas rejeitam produtos que causam estouro. Uma verificação de tamanho, uma verificação do resultado da alocação e um índice válido resolvem problemas diferentes; os três são importantes.
Principais conclusões
- Desreferencie ponteiros somente dentro dos limites de um objeto cujo tempo de vida esteja ativo.
- Reserve espaço e armazene o terminador da cadeia de caracteres.
- Não modifique literais de cadeia de caracteres.
- Liberar uma alocação invalida o acesso por todas as referências ao mesmo objeto.
- Retorne armazenamento com um tempo de vida que a função chamadora possa usar.
- Verifique a multiplicação pela quantidade de elementos e o resultado da alocação.
4Detecte erros em tempo de execução com asserções e sanitizadores
Você pode fazer uma versão de teste verificar mais do que sua saída final. Asserções verificam as suposições que seu próprio código promete manter, enquanto sanitizadores verificam operações como acesso à memória e aritmética no momento em que acontecem. Aprenda a ler cada relatório, escolher uma ferramenta para o erro de que você suspeita e executar entradas que alcancem os caminhos arriscados. Uma rotina de verificação útil conecta um erro detectado a uma correção e a um teste permanente.
4.1 Expresse suposições com asserções
Uma asserção expressa algo que deve ser verdadeiro porque seu código está correto. Ela transforma uma suposição violada e oculta em uma parada perto do ponto em que essa suposição importa.
Verifique um contrato de programação
Inclua <assert.h> e escreva assert(condition);. Uma condição verdadeira permite que a execução continue. Uma condição falsa imprime informações de diagnóstico em stderr e chama abort(). A mensagem identifica a expressão, o local no código-fonte e a função; seu texto exato depende da plataforma.
Para uma função auxiliar interna de média cuja função chamadora promete fornecer argumentos válidos:
double average(const int *a, size_t n) {
assert(a != NULL);
assert(n > 0);
/* compute the average */
}
Uma chamada acidental com zero elementos falha no ponto da promessa violada. Em um depurador, bt mostra os mecanismos da asserção e de abortamento, seguidos das chamadas no seu código. Selecione o registro de ativação da função chamadora para inspecionar como ela forneceu zero.
Contratos aparecem em vários pontos:
| Tipo | O que estabelece | Exemplo |
|---|---|---|
| Pré-condição | Exigido na entrada | Um arranjo de entrada não vazio |
| Pós-condição | Garantido após a operação | Um arranjo está ordenado |
| Invariante | Mantido ao longo das etapas | Uma contagem armazenada corresponde à estrutura |
Uma asserção é documentação executável: ela identifica a propriedade e também a verifica durante uma execução de teste. Escolha expressões que observem o estado sem alterá-lo.
Mantenha operações necessárias fora das asserções
Definir NDEBUG antes de incluir <assert.h>, normalmente com -DNDEBUG, desativa assert. Sua expressão não é avaliada. Isso cria um defeito grave se a expressão executar uma operação necessária:
assert(++count <= MAX);
O incremento desaparece em uma versão compilada com NDEBUG. Escreva a ação separadamente:
++count;
assert(count <= MAX);
A mesma regra exclui a abertura de arquivos, a alocação de memória ou o fechamento de arquivos dentro de uma asserção. Essas operações devem ocorrer em todas as configurações. Suas falhas precisam de tratamento de erros comum.
Valide falhas esperadas normalmente
Um arquivo de entrada ausente, texto inválido do usuário e memória esgotada podem ocorrer durante o uso correto do programa. Verifique esses casos com if, informe o erro e retorne ou libere os recursos. Uma função auxiliar privada pode usar uma asserção para verificar uma faixa validada, mas o ponto público de entrada deve impor essa faixa com verificações que permaneçam nas versões para distribuição.
Por exemplo, sempre teste fclose(f) == EOF no fluxo de controle comum ao fechar um arquivo de saída. A saída armazenada em buffer pode falhar durante o fechamento; remover a chamada ou sua verificação pode ocultar a perda de dados.
Verifique fatos fixos em tempo de compilação
Uma asserção estática testa uma expressão constante durante a compilação. Em C17, <assert.h> fornece static_assert como macro para _Static_assert do C11:
static_assert(sizeof(int) == 4, "requires a 4-byte int");
Uma condição falsa faz a compilação falhar. Ela não pode verificar uma contagem informada pelo usuário que só é conhecida em tempo de execução. C23 torna static_assert uma palavra-chave e permite omitir a mensagem. Use essa distinção para colocar cada verificação onde as informações necessárias estiverem disponíveis.
Principais conclusões
- Asserções verificam compromissos internos do programa.
- Uma asserção que falha em tempo de execução informa sua localização e aborta o programa.
- Mantenha efeitos colaterais fora das expressões de asserção.
- Use tratamento de erros comum para entrada inválida e falhas de recursos.
NDEBUGdesativa asserções em tempo de execução.- Asserções estáticas verificam expressões constantes durante a compilação.
4.2 Encontre erros de memória com AddressSanitizer
O AddressSanitizer, geralmente chamado de ASan, adiciona verificações às operações de memória compiladas. Ele pode relatar um acesso inválido no momento em que acontece, antes que dados corrompidos causem um encerramento inesperado em outro ponto.
Compile com as verificações ligadas ao programa
Use GCC ou Clang para compilar e ligar com ASan:
clang -std=c17 -g -fsanitize=address -fno-omit-frame-pointer program.c -o program-asan
-g fornece localizações no código-fonte. Manter ponteiros de registro de ativação ajuda nos rastreamentos da pilha de chamadas. Para um projeto com vários arquivos, instrumente os arquivos-fonte relevantes e use a opção do sanitizador também na etapa final de ligação. O MSVC fornece /fsanitize=address em versões compiladas compatíveis do Windows.
O ASan detecta muitos acessos fora dos limites de blocos do heap e de arranjos na pilha e globais. Ele também detecta uso após a liberação, liberação dupla, liberação inválida e os erros de uso após o fim do escopo para os quais há suporte. No Linux, o ambiente de execução usual do ASan inclui verificação de vazamentos no encerramento. O Clang fornecido pela Apple no macOS não oferece essa detecção de vazamentos; conjuntos de ferramentas LLVM e suporte de plataformas variam.
Siga um relatório até a causa
Considere um arranjo alocado com sucesso, com int de 4 bytes:
int *a = malloc(10 * sizeof *a);
for (int i = 0; i <= 10; ++i) {
a[i] = i;
}
A alocação contém 40 bytes. Os índices de zero a nove usam esses bytes. O índice dez começa imediatamente após eles. O ASan pode informar “heap-buffer-overflow”, “WRITE of size 4” e uma localização a zero bytes de uma região de 40 bytes. O rastreamento da escrita identifica a atribuição; o rastreamento da alocação identifica o malloc que estabeleceu os limites do bloco.
Leia os relatórios nesta ordem:
- Identifique a categoria do erro e se o acesso foi uma leitura ou escrita.
- Encontre o primeiro registro de ativação no seu código no rastreamento do acesso.
- Compare o acesso com a região alocada e seu tamanho de elemento.
- Para um erro de tempo de vida, encontre também os rastreamentos da alocação e da liberação.
Aqui a correção é i < 10. Alocar outro elemento mantendo a regra de parada incorreta oculta a diferença entre a quantidade pretendida e o laço. O ASan para no primeiro erro detectado por padrão. Corrija-o, recompile e execute novamente.
Entenda os limites da ferramenta
O ASan acompanha a memória permitida usando memória sombra. Zonas de proteção proibidas cercam as alocações e os arranjos para os quais há suporte. Blocos liberados passam um tempo em quarentena antes da reutilização, ajudando a revelar acessos a memória que deixou de ser válida. Uma escrita além de um membro de arranjo para outro membro da mesma estrutura pode permanecer dentro de uma região válida maior e escapar à detecção.
O ASan não verifica leituras comuns de valores não inicializados. O Memcheck do Valgrind pode encontrar esses usos e muitos defeitos no heap sem recompilação, mas costuma executar cerca de 20 a 30 vezes mais devagar. A redução típica de velocidade do ASan é de cerca de duas vezes, com uso adicional de memória. Não combine uma versão compilada com ASan com Valgrind, MemorySanitizer ou ThreadSanitizer. Execute deliberadamente entradas de casos-limite: um erro em um caminho não executado não pode aparecer no relatório.
Principais conclusões
- Compile e ligue com
-fsanitize=addresse informações de depuração. - Leia o rastreamento do acesso junto com os rastreamentos da alocação ou liberação.
- Corrija a primeira causa relatada, recompile e execute novamente.
- O ASan não detecta leituras comuns de valores não inicializados.
- Use entradas de casos-limite para alcançar acessos arriscados à memória.
4.3 Detecte comportamento indefinido com UBSan e estabeleça uma rotina de verificação
Uma ferramenta de verificação de memória não cobre todas as operações inválidas em C. O UndefinedBehaviorSanitizer, ou UBSan, instrumenta muitas operações aritméticas e operações com ponteiros e arranjos. Combine-o com ASan para que uma execução de teste possa detectar mais de uma classe de defeito.
Torne falhas aritméticas visíveis
Com int de 32 bits, somar dois valores de 2.000.000.000 exige um total de 4.000.000.000. Esse resultado excede INT_MAX. Um relatório do UBSan identifica o estouro de inteiro com sinal na adição e informa os operandos. Imprimir uma resposta negativa que parece ter retornado ao início da faixa sem a ferramenta não tornaria a operação válida.
Use um tipo para o total que possa representar a faixa aceita ou rejeite uma soma antes que ela cause estouro. Converter o resultado já estourado para long long é tarde demais; a própria operação deve usar um tipo adequado.
Um comando de teste comum é:
clang -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined program.c -o program-check
O UBSan normalmente permite a recuperação após muitos relatórios, então o código posterior pode continuar. -fno-sanitize-recover=undefined faz essas falhas interromperem o teste com um código de erro. Isso ajuda verificações automatizadas a detectar um defeito mesmo quando a saída final impressa parece correta.
Escolha a ferramenta para o defeito
| Ferramenta | Finalidade principal | Limite importante |
|---|---|---|
| UBSan | Muitas operações indefinidas de aritmética, deslocamento de bits, ponteiros nulos, alinhamento e arranjos | Não detecta todos os tipos de UB |
| ASan | Muitos erros de limites e de tempo de vida | Não acompanha valores comuns não inicializados |
| MemorySanitizer | Usos de memória não inicializada | Precisa de dependências instrumentadas para resultados confiáveis |
| ThreadSanitizer | Corridas de dados | Exige uma versão compilada separada do ASan |
A redução modular sem sinal é definida, portanto o grupo undefined não a relata. Clang tem uma verificação separada, -fsanitize=unsigned-integer-overflow, para código em que a redução modular indica um defeito. Não conclua que todo resultado inteiro indesejado é UB.
MemorySanitizer é uma ferramenta do Clang usada principalmente no Linux; bibliotecas não instrumentadas podem confundir seu rastreamento. ThreadSanitizer detecta acessos conflitantes por threads sem sincronização, com pelo menos uma escrita. Os acessos não precisam ocorrer exatamente no mesmo instante. Nenhuma das ferramentas pode ser combinada com ASan em uma única versão compilada.
Torne a verificação uma rotina
Mantenha os avisos ativados e resolva-os. Compile com ASan e UBSan e depois execute entradas vazias, entradas com um elemento, buffers cheios, texto inválido e valores próximos dos limites aceitos. Execute análise estática para inspecionar caminhos que os testes podem não alcançar e repita as verificações automaticamente quando o código mudar. Execute também a configuração de distribuição, em que as configurações de otimização e asserção são diferentes.
O teste por fuzzing fornece muitas entradas geradas ou modificadas. libFuzzer e AFL++ podem revelar caminhos incomuns em analisadores; sanitizadores transformam uma operação inválida desencadeada em um relatório útil. Salve uma entrada que encontre um defeito, reduza-a sem perder a falha, corrija a causa e mantenha-a como teste de regressão. Uma execução sem erros é evidência sobre as entradas executadas, não uma garantia para todas as execuções possíveis.
Principais conclusões
- Use UBSan para muitos erros aritméticos e de expressão.
- Faça falhas do UBSan interromperem testes automatizados quando a recuperação puder ocultá-las.
- A redução modular sem sinal precisa de uma verificação separada quando for indesejada.
- Use versões compiladas separadas para MemorySanitizer ou ThreadSanitizer.
- Combine testes de casos-limite, fuzzing, análise estática e entradas de regressão.
- A cobertura dos sanitizadores depende da execução.
5Escreva código C mais seguro
Um C mais seguro começa onde os dados entram em um programa e continua por cada cálculo de tamanho, cópia, alocação e caminho de erro. Um padrão de codificação transforma defeitos recorrentes em verificações concretas que você pode aplicar ao escrever código. Aprenda a preservar os tamanhos dos buffers, validar entradas numéricas completas e manter clara a responsabilidade pela liberação de recursos. Depois, avalie essas responsabilidades em relação ao controle de memória oferecido por C e à proteção oferecida por linguagens com segurança de memória.
5.1 Use um padrão de codificação segura
Um padrão de codificação segura reúne regras concretas que evitam defeitos recorrentes. Ele oferece a quem revisa o código perguntas mais úteis do que “Isto parece seguro?”. Cada regra conecta uma operação arriscada à condição ou ao projeto necessário para torná-la segura.
Leia uma regra para entender seu motivo
O Padrão de codificação segura em C do SEI CERT vem do Software Engineering Institute da Carnegie Mellon University. A edição em livro de 2016 é acompanhada de uma versão on-line em evolução. Ele distingue regras, cuja violação provavelmente causará defeitos, de recomendações que melhoram a confiabilidade e a segurança.
Os identificadores combinam uma categoria, um número e -C. As regras usam números de 30 a 99; as recomendações usam de 00 a 29. Por exemplo, INT32-C trata de estouro de inteiro com sinal, enquanto DCL00-C é uma recomendação sobre declarações. As categorias incluem declarações (DCL), expressões (EXP), inteiros (INT), ponto flutuante (FLP), arranjos (ARR), cadeias de caracteres (STR), memória (MEM), entrada e saída (FIO), ambiente (ENV), erros (ERR) e assuntos diversos (MSC).
A página de uma regra normalmente explica o defeito, mostra código não conforme, apresenta soluções conformes e classifica o risco. Leia as suposições de cada solução. Uma função substituta só é útil quando seu comportamento realmente atende ao requisito.
Aplique uma regra a um caminho dos dados
FIO30-C trata de cadeias de formatação. Este código trata dados externos como um programa de formatação:
printf(user_name);
Um nome de usuário que contenha conversões de formato faz printf procurar argumentos que não foram fornecidos; %n pode causar uma escrita na memória. Mantenha o formato sob controle do programa:
printf("%s", user_name);
Como alternativa, fputs(user_name, stdout) imprime o texto sem processamento de formato. -Wformat=2 inclui verificações que ajudam a encontrar formatos inseguros. A correção altera a forma como os dados são interpretados, não apenas o comprimento possível da cadeia de caracteres.
Crie uma lista de verificação útil
Para memória, pergunte se os índices permanecem dentro dos objetos (ARR30-C), se o armazenamento da cadeia inclui o terminador (STR31-C), se o armazenamento liberado nunca é acessado (MEM30-C), se as alocações sob sua responsabilidade são liberadas (MEM31-C), se apenas alocações válidas são liberadas (MEM34-C) e se bytes suficientes são alocados (MEM35-C).
Para expressões, verifique redução modular sem sinal indesejada (INT30-C), estouro de inteiro com sinal (INT32-C), divisão inválida (INT33-C), leituras de valores não inicializados (EXP33-C) e desreferenciações nulas (EXP34-C). Nos pontos de interação com bibliotecas, trate falhas relatadas (ERR33-C) e erros de conversão (ERR34-C). Evite system() conforme orienta ENV33-C, pois um shell interpreta sua entrada.
Tabelas de risco classificam gravidade, probabilidade e custo de correção de 1 a 3; uma pontuação maior de custo significa correção mais barata. O produto desses valores é a prioridade, de 1 a 27. Os níveis agrupam prioridades em L1 (12–27), L2 (6–9) e L3 (1–4). Isso ajuda a ordenar as correções sem declarar inofensivos os defeitos de menor prioridade.
Use as regras durante a revisão de código e use resultados de analisadores associados a seus identificadores. MISRA C é outro padrão usado em software embarcado crítico para a segurança. As regras ajudam a evitar defeitos, enquanto avisos, testes e sanitizadores verificam se a implementação realmente as segue. Memorizar apenas os identificadores não detecta um terminador ausente nem um ramo de erro ignorado.
Principais conclusões
- Leia o mecanismo da falha e a solução conforme em conjunto.
- Identificadores CERT distinguem categorias, regras e recomendações.
- Mantenha dados externos fora das cadeias de formatação.
- Aplique verificações concretas a limites, tempo de vida, operações aritméticas e erros.
- Use classificações de risco para ordenar o trabalho e depois verifique o código com ferramentas.
5.2 Manipule cadeias de caracteres e buffers com segurança
A manipulação segura de cadeias de caracteres exige uma origem válida, uma capacidade conhecida do destino e uma decisão sobre o texto que não couber. Uma função com limite de tamanho evita algumas sobrescritas, mas você ainda deve verificar se toda a operação foi bem-sucedida.
Leve a capacidade junto com o ponteiro
sizeof mede o tipo de seu operando. Para um char buf[20] local, ele fornece 20 bytes. Em void fill(char buf[]), o parâmetro é ajustado para um ponteiro; sizeof buf mede esse ponteiro, não o arranjo da função chamadora. Passe um parâmetro de capacidade, como em fill(buf, sizeof buf), enquanto a função chamadora ainda tem o tipo de arranjo.
Uma cadeia com cinco caracteres precisa de seis bytes, incluindo '\0'. A capacidade conta cada byte em que se pode escrever; o comprimento da cadeia exclui o terminador. É por isso que malloc(strlen(src)) aloca um byte a menos para uma cópia.
Leia uma linha com limite
Use fgets(buf, sizeof buf, stdin) e verifique seu valor de retorno antes de usar o conteúdo. Ela armazena no máximo a capacidade menos um caracteres e depois um terminador. Ela mantém uma quebra de linha quando esta couber. Após uma leitura bem-sucedida, buf[strcspn(buf, "\n")] = '\0'; remove uma quebra de linha armazenada.
Se uma linha longa preencher o buffer, os caracteres não lidos permanecem para a próxima chamada. Decida se vai rejeitar a linha e descartar o restante, ler mais ou aceitar o truncamento. Remover uma quebra de linha não comprova que você leu a linha completa. gets não aceita uma capacidade e foi removida em C11; nunca a use.
Para uma única palavra em char name[20], %19s em scanf limita os caracteres armazenados e deixa espaço para o terminador. Verifique se o número esperado de conversões foi bem-sucedido. Para texto numérico não confiável, prefira uma linha completa seguida de strtol com verificações; %d tem comportamento indefinido quando o valor convertido não pode ser representado, mesmo que você inspecione a contagem de retorno de scanf.
Detecte uma cópia truncada
strcpy e strcat não conhecem a capacidade do destino. strncpy pode deixar o resultado sem terminador. Com uma capacidade positiva, snprintf termina o texto armazenado e informa quantos caracteres a saída completa exigiria:
char dst[8];
int r = snprintf(dst, sizeof dst, "%s", "debugging");
if (r < 0) {
/* report encoding failure */
} else if ((size_t)r >= sizeof dst) {
/* report truncation */
}
A cadeia armazenada é “debuggi”, e r é 9. A saída completa precisa de dez bytes, incluindo seu terminador. Verifique se o resultado é negativo antes de convertê-lo para size_t. Para que o resultado caiba, r deve ser menor que a capacidade.
Adeque a operação ao armazenamento
Use memmove quando a origem e o destino se sobrepuserem; memcpy com regiões sobrepostas é indefinido. strlcpy está disponível em sistemas BSD, macOS e glibc 2.38 em diante, mas não faz parte do ISO C. As funções do Anexo K são opcionais e não estão disponíveis em muitas bibliotecas.
Mantenha dados do usuário fora do próprio formato. Faça cada conversão corresponder a seu argumento: %d para int, %ld para long, %zu para size_t, %f para double e %s para uma cadeia terminada em nulo. %p exige um void *, portanto passe uma conversão apropriada do ponteiro. Um limite de largura não pode corrigir um tipo de argumento incompatível.
Principais conclusões
- Passe a capacidade do destino junto com um ponteiro.
- Reserve um byte para o terminador da cadeia de caracteres.
- Trate explicitamente linhas de entrada incompletas.
- Verifique se
snprintfapresentou falha ou truncamento. - Use
memmovepara regiões de memória sobrepostas. - Faça cada conversão de formato corresponder ao tipo de seu argumento.
5.3 Valide a entrada e trate erros
Dados externos podem ser sintaticamente válidos e ainda assim inseguros para a operação pretendida. Valide seu comprimento, formato completo e faixa útil antes que se tornem um índice, tamanho de alocação ou contagem de laço.
Analise o número inteiro
atoi não oferece uma forma confiável de distinguir um zero válido de texto inválido, e um resultado que não pode ser representado causa comportamento indefinido. strtol fornece um ponteiro de fim da conversão e um erro de faixa. Após ler uma linha completa e remover sua quebra de linha, um analisador decimal rigoroso pode usar:
errno = 0;
char *end;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
/* reject the numeric text */
} else if (v < 0 || v > 150) {
/* reject this application's range */
} else {
int age = (int)v;
/* use age */
}
Inclua <errno.h> e <stdlib.h>. O primeiro teste distingue três falhas: ausência de algarismos, texto ao final e um valor fora da faixa de long. O teste da faixa permitida pela aplicação vem antes da conversão para int. strtol aceita espaços iniciais e um sinal opcional; decida se a gramática da aplicação os permite.
Para “42abc”, a conversão pode retornar 42, mas end aponta para as letras restantes. Para “abc”, nenhum algarismo é consumido. Para um inteiro decimal grande demais, errno se torna ERANGE. Um número retornado diferente de zero, por si só, não comprova uma conversão completa nem uma faixa segura.
Preserve a responsabilidade pela liberação em caso de falha
Toda operação que pode falhar precisa de uma resposta. Verifique resultados de alocação, fopen, fgets, contagens de conversão e conclusão da saída. O retorno NULL de fgets pode significar fim da entrada ou erro de leitura. fclose pode informar uma falha de escrita de dados armazenados em buffer. Use informações de erro adequadas à função; muitas chamadas de biblioteca definem errno, que perror ou strerror pode transformar em um diagnóstico legível.
Para redimensionar para um tamanho positivo, proteja o cálculo do tamanho e a alocação original:
if (new_n == 0) {
free(a);
a = NULL;
} else if (new_n > SIZE_MAX / sizeof *a) {
/* reject the size */
} else {
int *tmp = realloc(a, new_n * sizeof *a);
if (tmp == NULL) {
/* handle failure; a still owns its old block */
} else {
a = tmp;
}
}
Atribuir realloc diretamente ao único ponteiro responsável pela alocação faz perder o endereço do bloco antigo em caso de falha. Trate zero separadamente: C17 torna realloc com tamanho zero definido pela implementação, enquanto C23 o torna indefinido. Após um redimensionamento bem-sucedido, use o ponteiro retornado e descarte referências antigas ao mesmo objeto.
Deixe um caminho de erro claro
Uma única seção de liberação pode liberar os recursos adquiridos na ordem inversa. Inicialize com NULL os ponteiros pelos quais você é responsável, feche apenas arquivos abertos e retorne um código de falha após informar a causa. Um goto cleanup pode facilitar a análise desses caminhos.
Evite system() em tarefas que envolvam texto externo: o shell pode interpretar esse texto como comandos. Prefira uma operação de biblioteca ou uma interface adequada para iniciar programas com argumentos passados separadamente. Trate argumentos de linha de comando, valores de ambiente, conteúdo de arquivos e dados de rede como entradas que exigem o mesmo cuidado que texto digitado.
Principais conclusões
- Valide a conversão completa e a faixa permitida pela aplicação antes de usar um número.
- Defina
errnocomo zero antes destrtole inspecione seu ponteiro de fim da conversão. - Verifique falhas de biblioteca e preserve os recursos em todos os caminhos.
- Use um ponteiro temporário para
realloccom tamanho positivo. - Trate tamanhos zero e estouro de multiplicação antes de redimensionar.
- Libere apenas os recursos realmente adquiridos.
- Mantenha texto externo fora de cadeias de comandos do shell.
5.4 Avalie os compromissos envolvidos na segurança de memória
Segurança de memória significa que os acessos permanecem dentro do armazenamento que um programa tem direito de usar. C oferece controle direto da memória e deixa as verificações de limites e de tempo de vida para o programador.
Diferencie limites de tempo de vida
A segurança espacial mantém um acesso dentro dos limites de seu objeto. Escrever no elemento dez de um arranjo de dez elementos a viola. A segurança temporal mantém o acesso dentro do tempo de vida do objeto. Ler uma alocação após free, liberá-la duas vezes ou usar o endereço retornado de uma variável local viola as regras de tempo de vida.
As consequências podem ir além de um encerramento inesperado. Uma leitura pode expor dados confidenciais. Uma escrita pode corromper um arquivo, alterar dados de controle ou criar uma vulnerabilidade de segurança quando uma entrada externa controla a operação. Um programa que continua após uma corrupção pode produzir resultados convincentes, mas falsos.
Essa responsabilidade também afeta equipes experientes. A Microsoft informou em 2019 que cerca de 70% das vulnerabilidades às quais havia atribuído identificadores CVE, ano após ano por mais de uma década, diziam respeito à segurança de memória. Em dezembro de 2023, a CISA e agências parceiras recomendaram que fabricantes de software planejassem a transição para linguagens com segurança de memória. Essas são observações históricas específicas, não um percentual medido para todos os programas em C.
Entenda os motivos para escolher C
C oferece controle sobre a disposição e a alocação da memória e o acesso ao hardware, sem exigir um coletor de lixo. Seu pequeno ambiente de execução é adequado a firmware com recursos limitados. A linguagem tem amplo suporte de compiladores, grandes bases de código existentes e interfaces que muitas outras linguagens podem chamar. Esses benefícios explicam seu uso em núcleos de sistemas operacionais, software embarcado, ambientes de execução e bibliotecas.
Eles não tornam todo programa em C previsivelmente rápido: algoritmos, alocação, caches e chamadas de sistema continuam importantes. O benefício relevante é o controle sobre os custos e os requisitos do ambiente de execução.
| Abordagem | Proteção de memória | Custo ou responsabilidade |
|---|---|---|
| C | Limites e tempos de vida garantidos pelo programador | Raciocínio, verificações e testes manuais |
| Rust seguro | Responsabilidade, empréstimo e verificações de limites | Aprender suas regras de responsabilidade |
| Go, Java, C#, Python | Tempos de vida gerenciados e verificações de limites | Custos de execução e memória variam |
| Swift | Contagem de referências e acesso com verificação | Custos de responsabilidade e execução variam |
Operações inseguras e interfaces com C exigem cuidado especial em sistemas que, de resto, oferecem segurança de memória. A segurança de memória também deixa erros de lógica e injeção por entrada para serem resolvidos.
Escolha camadas para o risco restante
Evite defeitos com regras de codificação, avisos e revisão. Detecte defeitos com análise estática, testes de casos-limite, sanitizadores e fuzzing. Limite os danos restantes com protetores de pilha, verificações compatíveis de _FORTIFY_SOURCE, randomização do leiaute do espaço de endereçamento (ASLR) e memória não executável. Uma medida de mitigação pode impedir um ataque ou transformar uma corrupção em encerramento inesperado, mantendo o defeito no código-fonte.
Os custos orientam o uso. O ASan normalmente dobra o tempo de execução e aumenta o uso de memória; o Valgrind costuma tornar a execução de 20 a 30 vezes mais lenta. Eles são mais adequados a testes do que ao uso comum em distribuição. O reforço de segurança de menor custo costuma permanecer nas versões para distribuição.
Para um novo analisador de arquivos da internet sem restrição que exija C, uma linguagem com segurança de memória reduz um risco importante. Para firmware existente em C em um microcontrolador com 32 KB de memória, o suporte à linguagem e os limites de recursos podem favorecer C com verificações rigorosas. Baseie a escolha nas restrições, na exposição a entradas e na facilidade de manutenção; a substituição gradual de um analisador arriscado também pode ser viável.
Principais conclusões
- A segurança espacial diz respeito aos limites; a segurança temporal diz respeito aos tempos de vida.
- Defeitos de memória podem expor dados e corromper resultados.
- C oferece controle direto e ampla compatibilidade, com a responsabilidade de garantir a segurança manualmente.
- Linguagens com segurança de memória eliminam muitos defeitos de memória, mas deixam outras classes de defeitos.
- Combine prevenção, detecção e medidas de mitigação para distribuição.
- Escolha uma linguagem com base nas restrições reais de recursos e integração.
Exercícios resolvidos de Depuração em C
1Compile um programa que você possa inspecionar
9 passos
- 1IntroduçãoComece antes da primeira execução
Um programa pode compilar e ainda conter um defeito que o compilador sabe sinalizar. Sua primeira tarefa é solicitar essas mensagens, como avisos, e interpretá-las.
Você inspecionará uma atribuição acidental e uma incompatibilidade de formato e depois escolherá configurações para uma sessão de depuração (uma versão de depuração) e uma versão para distribuição. Essas decisões preparam as ferramentas seguintes para mostrar evidências úteis em vez de endereços sem explicação ou valores locais ausentes.
- 2ExplicaçãoEscolha o conjunto de avisos
Comece com
-std=c17 -Wall -Wextra -Wpedantic. A primeira opção define a versão da linguagem; as demais ativam avisos comuns, avisos adicionais e diagnósticos para muitas divergências em relação a esse padrão.-Wallnão significa todos os avisos. Adicione-Wconversionpara conversões implícitas que alteram valores e-Wformat=2para verificações de formato mais rigorosas.-Werrortransforma os avisos ativados em erros de compilação. - 3ExemploLeia a condição
O programador quer informar que uma coleção está vazia:
if (items = 0) { puts("No items"); }Um aviso do compilador aponta uma atribuição em uma condição. Primeiro pergunte o que a expressão faz: ela armazena zero em
itemse depois a condição testa esse zero. O corpo nunca é executado. Substitua=por==para testar a contagem existente; adicionar parênteses ao redor da atribuição preservaria a operação incorreta. - 4DesafioCorrija o tipo da saída
double ratio = 2.5;é impresso comprintf("%d\n", ratio);. O que deve mudar e por quê?---reveal---
Use
printf("%f\n", ratio);.%despera umint, enquanto%fespera umdoubleemprintf. Um formato incompatível tem comportamento indefinido. Converter paraintalteraria o número impresso ao descartar sua parte fracionária, portanto não atenderia ao objetivo de imprimir a razão. - 5DicaLeia as mensagens em ordem
Um diagnóstico geralmente fornece a localização no código-fonte, o problema e a opção responsável. Leia também as notas relacionadas; elas podem identificar a declaração conflitante.
Corrija o primeiro erro do compilador e recompile. A falta de um ponto e vírgula pode confundir a análise posterior e criar uma cascata de mensagens. Depois que os erros desaparecerem, inspecione a causa de cada aviso. Uma compilação sem avisos ainda precisa de testes para erros de lógica e dependentes da entrada.
- 6ExplicaçãoPreserve estados úteis
Para uma versão simples para depuração, adicione
-g -O0. As informações de depuração mapeiam a execução para nomes e linhas do código-fonte; a baixa otimização mantém mais estados intermediários disponíveis para inspeção.-gnão desativa a otimização. Com-O2, o código ainda pode ser depurado, mas as linhas podem parecer saltar e variáveis podem ser eliminadas pela otimização.-Ogé outra opção de desenvolvimento que mantém otimização leve. - 7ConexãoEscolha verificações para finalidades diferentes
A análise estática acompanha possíveis caminhos do código-fonte sem executar o programa. Um sanitizador verifica operações nos caminhos que os testes realmente executam. Use ambos para ampliar a cobertura.
O reforço de segurança para distribuição tem outra finalidade: protetores de pilha e verificações de fortificação compatíveis podem detectar ou limitar parte da corrupção restante. A fortificação exige otimização e suporte adequado da biblioteca. Uma sobrescrita detectada ainda precisa de uma correção no código-fonte.
- 8DesafioMantenha a validação na versão para distribuição
Uma versão para depuração usa avisos rigorosos e asserções. Uma versão para distribuição adiciona
-O2 -DNDEBUG. O programador propõe usarassert(valid_input)como única verificação de entrada. O que deve mudar antes da distribuição?---reveal---
Valide a entrada com o fluxo de controle comum e trate a falha.
NDEBUGremove a avaliação das asserções, portanto a verificação proposta desaparece. Mantenha os avisos ativados e teste a configuração para distribuição, assim como a versão para depuração; configurações diferentes podem revelar defeitos ocultos. - 9RecapitulaçãoRelacione as configurações de compilação
Use as mensagens do compilador para identificar operações que precisam de correção. Faça os formatos corresponderem aos tipos, investigue conversões e corrija os primeiros erros antes de acompanhar as mensagens decorrentes deles.
Para a próxima sessão de depuração, compile o executável correto com informações do código-fonte e um nível de otimização adequado. Mantenha as configurações reproduzíveis, adicione análise e testes com sanitizadores e preserve as verificações de entrada quando as configurações de distribuição removerem as asserções.
2Rastreie um total incorreto e um encerramento inesperado
12 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoTransforme um sintoma em uma pergunta
- 2ExplicaçãoPreserve o caso que falha
- 3DesafioEscolha um experimento controlado
- 4ExemploPause no corpo do laço
- 5ExemploAcompanhe as passagens válidas
- 6DesafioCorrija e verifique a passagem extra
- 7DicaEntre apenas em código útil
- 8ExemploVá para a função chamadora
- 9ExplicaçãoDetecte uma alteração quando ela acontecer
- 10DesafioInvestigue um encerramento no alocador
- 11ConexãoUse um estado salvo do encerramento
- 12RecapitulaçãoEscolha a visão que responde à pergunta
3Verifique a operação antes que ela se torne inválida
12 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoPergunte o que C garante
- 2ExplicaçãoDiferencie variação de defeito
- 3ExemploUm teste de estouro que chega tarde demais
- 4DesafioPosicione a verificação do ponteiro
- 5ExemploProteja uma soma com sinal
- 6Ideia-chaveUma redução modular definida ainda pode prejudicar o programa
- 7DesafioConverta para um tipo de maior faixa antes de multiplicar
- 8DicaVerifique vários limites das expressões
- 9ExemploDiferencie um marcador de fim de um elemento
- 10ExemploAcompanhe cada referência ao mesmo objeto após a liberação
- 11DesafioCorrija um total e seu tempo de vida
- 12RecapitulaçãoVerifique tipo, faixa, limites e tempo de vida
4Leia as verificações como evidências de um defeito específico
11 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoVerifique mais do que a saída final
- 2ExplicaçãoVerifique um compromisso interno com uma asserção
- 3DesafioMantenha a operação em todas as versões compiladas
- 4ExplicaçãoCompile um executável instrumentado
- 5ExemploLeia a região relatada
- 6DesafioCorrija o laço correspondente
- 7Ideia-chaveA ausência de um relatório tem várias explicações
- 8ExemploFaça uma falha aritmética interromper o teste
- 9ExplicaçãoEscolha outras ferramentas de verificação com critério
- 10DesafioMelhore uma rotina de verificação incompleta
- 11RecapitulaçãoTransforme relatórios em verificações reproduzíveis
5Mantenha explícitos a entrada, os tamanhos e a responsabilidade pela liberação
15 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoAcompanhe os dados pelo programa
- 2ExplicaçãoUse a regra como uma pergunta
- 3DesafioMantenha a entrada como dados
- 4ExemploPasse a capacidade a partir da função chamadora
- 5ExemploDiferencie um prefixo seguro de uma operação bem-sucedida
- 6DesafioTrate uma linha incompleta
- 7ExemploInspecione o fim da conversão
- 8ExemploAplique a faixa do programa
- 9ExemploPreserve a responsabilidade pela alocação ao redimensionar
- 10DesafioPlaneje o caminho de falha
- 11ExplicaçãoIdentifique o limite de segurança
- 12ConexãoEscolha camadas com funções diferentes
- 13DesafioEscolha uma linguagem
- 14DesafioRevise o caminho completo da entrada
- 15RecapitulaçãoMantenha cada limite visível
Questões de Depuração em C
Conjunto 1: Pistas do compilador e decisões no depurador10 Questões de Prática
Questão 1 de 10
-Wall converte silenciosamente uma contagem double para int. Qual opção adicional de aviso visa essa alteração implícita de valor?Mostrar respostaOcultar resposta
-Wconversion visa conversões implícitas que podem alterar um valor, incluindo o descarte de uma parte fracionária de ponto flutuante. -Wshadow diz respeito a uma declaração ocultada com o mesmo nome, não a uma mudança de tipo. Um aviso deve levar à verificação do valor pretendido antes de adicionar uma conversão explícita.Questão 2 de 10
double reading = 7.25; e chama printf("%d\n", reading);. Qual correção preserva esse objetivo?Mostrar respostaOcultar resposta
printf espera um double para %f, portanto alterar o formato preserva o valor fracionário e corresponde ao argumento. Converter para int poderia atender a %d, mas descartaria a parte fracionária que o programa deve imprimir. A incompatibilidade original tem comportamento indefinido; o formato não é uma conversão numérica automática.Questão 3 de 10
Mostrar respostaOcultar resposta
-g adiciona as informações de depuração que mapeiam a execução para localizações no código-fonte, tipos e nomes. -Wextra ativa diagnósticos adicionais durante a compilação, mas não fornece esses mapeamentos. A otimização continua sendo uma escolha separada e ainda pode remover valores mesmo quando há informações de depuração.Questão 4 de 10
Mostrar respostaOcultar resposta
Questão 5 de 10
Mostrar respostaOcultar resposta
next normalmente executa a linha atual do código-fonte sem entrar em suas chamadas de função. step é destinado a entrar em uma função chamada quando há informações úteis do código-fonte. Um ponto de parada dentro da chamada ainda pode interromper next, portanto o comando descreve o modo de avançar, não a desativação de todas as paradas.Questão 6 de 10
result = compute();. Imprimir result mostra seu valor anterior, e nenhuma chamada foi executada nessa parada. Como você deve inspecionar o resultado calculado?Mostrar respostaOcultar resposta
result ainda contém seu valor anterior. Executar a linha com next permite que a chamada e a atribuição sejam concluídas antes da inspeção. Repetir uma impressão sem retomar observa o mesmo estado e não fornece evidência de que a atribuição falhou.Questão 7 de 10
strlen e o registro dois na sua função render. Você quer inspecionar o ponteiro caption de render no GDB. O que deve fazer?Mostrar respostaOcultar resposta
frame 2 seleciona render, onde caption pode ser inspecionado. Permanecer no registro zero examina o contexto da biblioteca, e retomar a execução é desnecessário quando a evidência necessária já está no registro de uma função chamadora.Questão 8 de 10
Mostrar respostaOcultar resposta
Questão 9 de 10
-O0 e -O2. Com -O2, uma variável intermediária também fica indisponível no depurador. Qual investigação aborda as duas observações?Mostrar respostaOcultar resposta
Questão 10 de 10
free após ler muitos registros. Aumentar um buffer de nome interrompe o encerramento visível, mas a entrada salva ainda faz o ASan relatar uma escrita além desse buffer. Qual ação resolve a causa?Mostrar respostaOcultar resposta
Conjunto 2: Tipos, faixas e objetos com tempo de vida ativo10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
int a com sinal e int b positivo, qual condição detecta um estouro em a + b sem avaliar essa soma?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
merge(left(), right()). Qualquer ordem de avaliação é permitida e nenhum estado compartilhado cria conflito. Como essa ordem é classificada em C?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
unsigned int de 32 bits. Qual valor unsigned int slots = 0u - 1u; armazena?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
unsigned int de 32 bits. Uma entrada fornece shift = 32. Qual é o status de 1u << shift?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
local após declarar char local[24] = "ready";. Por que ler o resultado na função chamadora é inválido?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
int old = *p; antes de if (p == NULL) return -1;. Um teste com entrada nula parece alcançar o retorno de erro com baixa otimização, mas é encerrado inesperadamente na versão para distribuição. Qual correção estabelece um caminho definido para entrada nula?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
int de 32 bits e long long de 64 bits, ambos os fatores são int: units = 70000, rate = 40000. Qual expressão calcula corretamente seu produto em long long?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
p; alias aponta para seu interior. Uma chamada de realloc com tamanho positivo tem sucesso e o bloco se move. A função atribui o resultado a p, escreve por meio de alias e depois libera tanto p quanto alias. Qual correção da responsabilidade pela liberação é necessária?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
-fsanitize=undefined. Uma multiplicação de size_t sofre redução modular, mas não produz relatório. Por que isso pode acontecer?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 3: Asserções e relatórios de sanitizadores10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
assert(user_count > 0); como única forma de rejeitar contagens inválidas do usuário. As versões para distribuição definem NDEBUG. O que deve substituir essa proteção?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
-fsanitize=undefined não informam nada, mas o valor vizinho e o tamanho da alocação estão errados. Qual investigação aborda os dois defeitos?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
assert(++used < capacity); antes de armazenar no índice used. Os testes passam, mas com NDEBUG ela sobrescreve repetidamente uma posição. Qual alteração no código-fonte preserva a atualização de estado pretendida em ambas as versões compiladas?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
size_t, aloca um buffer dependente da contagem e o preenche. Ele não verifica nem a conclusão da conversão nem a faixa permitida pela aplicação. Qual ordem de correções aborda o caminho da entrada até a memória?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 4: Cadeias de caracteres, entrada numérica e responsabilidade pela liberação de recursos10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
char line[48] para read_line(char line[]) em um sistema com ponteiros de oito bytes. O que sizeof line mede dentro da função?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
snprintf é chamada com uma capacidade de destino de 11 e retorna 11. O que isso estabelece?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
errno como zero e converte “64kg” com strtol. A conversão retorna 64 e errno permanece em zero. O que deve causar a rejeição?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
strtol, verifica apenas errno, converte para size_t e multiplica pelo tamanho do elemento para alocação. Quais verificações devem preceder a alocação?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
a. Após uma contagem positiva de entrada ser lida em new_n, o código executa a = realloc(a, new_n * sizeof *a); a[new_n - 1] = 0;. Toda contagem válida deve ser tratada, e uma falha no redimensionamento deve preservar o arranjo antigo. Qual sequência é necessária?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
fgets, sem armazenar uma quebra de linha. A aplicação exige um registro completo por linha. Qual tratamento evita interpretar um possível restante como um novo registro?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
strncpy(dst, src, sizeof dst) e depois imprime dst com %s. Considere ponteiros de oito bytes. Qual reformulação atende ao requisito?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
size_t count de um arquivo e executa imediatamente a = realloc(a, count * sizeof *a);. Arranjos vazios são permitidos, e uma solicitação que falhe deve preservar o arranjo antigo. Qual reformulação atende a esses requisitos para toda contagem?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 5: Corrija defeitos que interagem10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
int i = 4; em C17, um programador escreve printf("%d %d\n", i, ++i);. Qual correção torna bem definida a saída pretendida “valor antigo, depois valor novo”?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
word, calcula int bytes = strlen(word);, aloca essa quantidade de bytes, copia a cadeia completa, libera o novo bloco e retorna seu endereço. As entradas podem ser longas demais para int. Qual solução aborda o tamanho e o tempo de vida?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
strtol e verifica apenas errno != ERANGE. Depois, converte o resultado para int para usá-lo como índice de arranjo. Quais verificações ausentes ainda podem permitir uma entrada inválida?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Travou em uma questão sua? Ajuda com Tarefas
Termos-Chave
A
Acesso fora dos limites
AddressSanitizer
Análise estática
Aritmética com verificação de estouro
ckd_add do C23 informa se uma soma cabe no tipo para que o programa possa tratar a falha explicitamente.ASLR
Asserção
Asserção estática
static_assert de C17 pode rejeitar uma plataforma em que uma suposição necessária sobre o tamanho de int seja falsa.Avaliação de expressão
count observa um escalar; avaliar *p tenta um acesso por meio desse ponteiro.Aviso do compilador
C
Cadeia de formatação
printf usa %zu para size_t; o texto do usuário pertence a um argumento de dados separado.Caminho de liberação de recursos
Capacidade do buffer
Caso mínimo de reprodução
Código não conforme
printf(input) permite que dados externos forneçam conversões de formato e viola FIO30-C.Comportamento definido pela implementação
char simples tem ou não sinal.Comportamento indefinido
Comportamento não especificado
Comprimento da cadeia de caracteres
strlen("safe") é quatro, enquanto armazenar a cadeia completa exige cinco bytes.Conversão implícita
double a um int descarta a parte fracionária quando o valor truncado cabe no tipo.Corrida de dados
D
Defeito
Defesa em profundidade
Depurador
Despejo de memória do processo
Diagnóstico do compilador
%d e identificar o argumento double que não corresponde a ele.E
Efeitos colaterais não sequenciados
i quanto ++i a uma chamada combina uma leitura e uma alteração sem ordenação segura.Entrada não confiável
Erro de compilação
Erro de faixa
errno como zero, um erro de faixa de strtol define ERANGE, mesmo que um valor-limite seja retornado.Estouro de inteiro com sinal
int de 32 bits, somar um a INT_MAX não pode produzir um próximo valor portável e definido.Execução passo a passo
next para avançar sobre uma chamada e step quando precisar entrar no código da própria função.F
Falha
free pode ser a falha visível causada por uma escrita muito anterior na execução.H
Hipótese
I
Informações de depuração
-g ajuda um depurador a identificar a linha do código-fonte e as variáveis locais em um ponto de parada comum.Injeção de comandos do shell
system() a partir de um argumento não verificado permite que um shell interprete a sintaxe fornecida.Instrumentação
Invariante
L
Liberação dupla
Linguagem com segurança de memória
M
Memória sombra
MemorySanitizer
O
Otimização
-O2, um depurador pode não mostrar um valor separado para uma variável local removida pelo compilador.P
Padrão de codificação segura
Padrão de codificação segura em C do SEI CERT
Pilha de chamadas
main.Ponteiro de fim da conversão
strtol identifica o primeiro caractere não convertido ou o início da entrada se nenhum algarismo foi consumido.Ponteiro pendente
Ponto de observação
Ponto de parada
sum += a[i] permite inspecionar i sem avaliar o acesso ao arranjo.Ponto de parada condicional
i == 650 evita avançar manualmente pelas iterações anteriores do laço.Portabilidade
INT_MAX evita presumir que toda implementação atribui o mesmo valor máximo a int.Pós-condição
Pré-condição
Prioridade de risco
Promoção inteira
int ou unsigned int antes de muitas operações aritméticas e de deslocamento de bits.Q
Quarentena
R
Rastreamento da pilha de chamadas
bt ajuda você a localizar o primeiro registro referente ao seu código-fonte.Redução modular sem sinal
Reforço de segurança
Regiões de memória sobrepostas
memmove porque memcpy proíbe sobreposição.Registro de ativação
name, selecionar o registro da função chamadora após um encerramento inesperado disponibiliza esse ponteiro para inspeção.Responsabilidade pela liberação
realloc com tamanho positivo falha.S
Segurança espacial de memória
Segurança temporal de memória
free viola a segurança de tempo de vida mesmo quando seu endereço ainda parece familiar.Solução conforme
%s mantém o texto fornecido no argumento de dados de uma chamada de printf.Sombreamento de variáveis
count de um laço interno pode receber atualizações enquanto o count externo permanece inalterado.T
Tempo de vida do objeto
Terminador nulo
Teste de regressão
Teste por fuzzing
ThreadSanitizer
Truncamento
snprintf pelo menos igual à capacidade indica que o texto completo não coube.U
UndefinedBehaviorSanitizer
Uso após a liberação
V
Valor indeterminado
int sum; e usar imediatamente sum += x; lê um valor que nunca foi inicializado.Versão para depuração
-g -O0 facilita acompanhar um laço defeituoso uma instrução do código-fonte por vez.Versão para distribuição
NDEBUG remove suas asserções.Z
Zona de proteção
Sobre esta aula
Crie seus próprios Materiais de Estudo
Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.