Depuração, comportamento indefinido e C seguro

Aprenda a encontrar e evitar os defeitos que fazem programas em C travarem, produzirem respostas erradas ou se tornarem inseguros. Ative os avisos do compilador e entenda seu significado, investigue encerramentos inesperados e valores incorretos passo a passo no gdb ou lldb e reconheça comportamento indefinido em operações aritméticas, ponteiros e memória. Verifique suas suposições com asserções, detecte erros de memória e comportamento indefinido com AddressSanitizer e UndefinedBehaviorSanitizer e escreva código mais seguro seguindo regras de codificação segura para cadeias de caracteres, entrada e erros. Por fim, avalie os custos da falta de segurança de memória em C em relação ao controle que a linguagem oferece.

BásicoAula 8 de 8 do curso Curso de Programação em C

O que você vai aprender

  1. 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 compilador
      Compile 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ção
      Use 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.
  2. 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ótese
      Transforme 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 lldb
      Inicie 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 imprevistos
      Use 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.
  3. 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 indefinido
      Diferencie 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ões
      Reconheç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ória
      Reconheç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.
  4. 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ções
      Use 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 AddressSanitizer
      Compile 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ção
      Use UndefinedBehaviorSanitizer e conheça os outros sanitizadores; depois, combine avisos, sanitizadores e testes em uma rotina que detecte erros cedo.
  5. 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 segura
      Explique 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ça
      Saiba 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 erros
      Trate 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ória
      Explique 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.

Guia de estudo de Depuração em C

5 capítulos

1

Use 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çãoO que ajuda a revelar
-WconversionConversões implícitas que podem alterar um valor
-WshadowUma declaração local que oculta outra variável
-Wformat=2Verificações adicionais de cadeias de formatação
-WerrorFaz 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.
  • -Wall nã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çãoFinalidade usualO que você pode ver
-O0Depuração diretaA ordem do código-fonte geralmente é mais fácil de acompanhar
-OgDepuração com otimização leveAlgumas transformações permanecem
-O2Versão eficiente para distribuiçãoOperaçõ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 -g para obter informações de depuração relacionadas ao código-fonte.
  • Depure com -O0 ou -Og e teste também a versão para distribuição.
  • Mantenha as verificações de entrada ativas quando NDEBUG remover 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.
2

Encontre 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
ComandoFinalidade no GDB e no LLDB
next ou nExecutar a linha atual, normalmente sem entrar nas chamadas
step ou sEntrar em uma função chamada quando as informações do código-fonte permitirem
finishExecutar até a função selecionada retornar
continue ou cRetomar 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, finish e continue para 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 bt para 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.
3

Reconheç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
TipoO que a linguagem permiteExemplo em C17
IndefinidoNenhum comportamento exigidoEstouro de inteiro com sinal
Definido pela implementaçãoUma escolha documentada da implementaçãoSe char simples tem sinal
Não especificadoUma escolha permitida que não precisa ser documentadaOrdem 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.
4

Detecte 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:

TipoO que estabeleceExemplo
Pré-condiçãoExigido na entradaUm arranjo de entrada não vazio
Pós-condiçãoGarantido após a operaçãoUm arranjo está ordenado
InvarianteMantido ao longo das etapasUma 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.
  • NDEBUG desativa 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:

  1. Identifique a categoria do erro e se o acesso foi uma leitura ou escrita.
  2. Encontre o primeiro registro de ativação no seu código no rastreamento do acesso.
  3. Compare o acesso com a região alocada e seu tamanho de elemento.
  4. 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=address e 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
FerramentaFinalidade principalLimite importante
UBSanMuitas operações indefinidas de aritmética, deslocamento de bits, ponteiros nulos, alinhamento e arranjosNão detecta todos os tipos de UB
ASanMuitos erros de limites e de tempo de vidaNão acompanha valores comuns não inicializados
MemorySanitizerUsos de memória não inicializadaPrecisa de dependências instrumentadas para resultados confiáveis
ThreadSanitizerCorridas de dadosExige 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.
5

Escreva 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 snprintf apresentou falha ou truncamento.
  • Use memmove para 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 errno como zero antes de strtol e 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 realloc com 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.

AbordagemProteção de memóriaCusto ou responsabilidade
CLimites e tempos de vida garantidos pelo programadorRaciocínio, verificações e testes manuais
Rust seguroResponsabilidade, empréstimo e verificações de limitesAprender suas regras de responsabilidade
Go, Java, C#, PythonTempos de vida gerenciados e verificações de limitesCustos de execução e memória variam
SwiftContagem de referências e acesso com verificaçãoCustos 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

5 Lições Interativas

1

Compile um programa que você possa inspecionar

9 passos
Leia as mensagens do compilador, corrija o código identificado por elas e escolha configurações de compilação separadas para depuração e distribuição.
  1. 1
    Introduçã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.

  2. 2
    Explicaçã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.

    -Wall não significa todos os avisos. Adicione -Wconversion para conversões implícitas que alteram valores e -Wformat=2 para verificações de formato mais rigorosas. -Werror transforma os avisos ativados em erros de compilação.

  3. 3
    ExemploLeia 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 items e 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.

  4. 4
    DesafioCorrija o tipo da saída

    double ratio = 2.5; é impresso com printf("%d\n", ratio);. O que deve mudar e por quê?

    ---reveal---

    Use printf("%f\n", ratio);. %d espera um int, enquanto %f espera um double em printf. Um formato incompatível tem comportamento indefinido. Converter para int alteraria o número impresso ao descartar sua parte fracionária, portanto não atenderia ao objetivo de imprimir a razão.

  5. 5
    DicaLeia 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.

  6. 6
    Explicaçã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.

    -g nã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.

  7. 7
    Conexã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.

  8. 8
    DesafioMantenha 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 usar assert(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. NDEBUG remove 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.

  9. 9
    Recapitulaçã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.

2

Rastreie um total incorreto e um encerramento inesperado

12 passos
Passe da reprodução à execução passo a passo, aos rastreamentos da pilha de chamadas e às paradas direcionadas que identificam a origem de um valor incorreto.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoTransforme um sintoma em uma pergunta
  2. 2
    ExplicaçãoPreserve o caso que falha
  3. 3
    DesafioEscolha um experimento controlado
  4. 4
    ExemploPause no corpo do laço
  5. 5
    ExemploAcompanhe as passagens válidas
  6. 6
    DesafioCorrija e verifique a passagem extra
  7. 7
    DicaEntre apenas em código útil
  8. 8
    ExemploVá para a função chamadora
  9. 9
    ExplicaçãoDetecte uma alteração quando ela acontecer
  10. 10
    DesafioInvestigue um encerramento no alocador
  11. 11
    ConexãoUse um estado salvo do encerramento
  12. 12
    RecapitulaçãoEscolha a visão que responde à pergunta
3

Verifique a operação antes que ela se torne inválida

12 passos
Raciocine sobre otimização, limites aritméticos e tempos de vida de objetos sem depender de uma saída acidental.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoPergunte o que C garante
  2. 2
    ExplicaçãoDiferencie variação de defeito
  3. 3
    ExemploUm teste de estouro que chega tarde demais
  4. 4
    DesafioPosicione a verificação do ponteiro
  5. 5
    ExemploProteja uma soma com sinal
  6. 6
    Ideia-chaveUma redução modular definida ainda pode prejudicar o programa
  7. 7
    DesafioConverta para um tipo de maior faixa antes de multiplicar
  8. 8
    DicaVerifique vários limites das expressões
  9. 9
    ExemploDiferencie um marcador de fim de um elemento
  10. 10
    ExemploAcompanhe cada referência ao mesmo objeto após a liberação
  11. 11
    DesafioCorrija um total e seu tempo de vida
  12. 12
    RecapitulaçãoVerifique tipo, faixa, limites e tempo de vida
4

Leia as verificações como evidências de um defeito específico

11 passos
Escolha verificações com asserções, ASan e UBSan e depois relacione cada relatório a uma correção duradoura.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoVerifique mais do que a saída final
  2. 2
    ExplicaçãoVerifique um compromisso interno com uma asserção
  3. 3
    DesafioMantenha a operação em todas as versões compiladas
  4. 4
    ExplicaçãoCompile um executável instrumentado
  5. 5
    ExemploLeia a região relatada
  6. 6
    DesafioCorrija o laço correspondente
  7. 7
    Ideia-chaveA ausência de um relatório tem várias explicações
  8. 8
    ExemploFaça uma falha aritmética interromper o teste
  9. 9
    ExplicaçãoEscolha outras ferramentas de verificação com critério
  10. 10
    DesafioMelhore uma rotina de verificação incompleta
  11. 11
    RecapitulaçãoTransforme relatórios em verificações reproduzíveis
5

Mantenha explícitos a entrada, os tamanhos e a responsabilidade pela liberação

15 passos
Aplique regras de codificação a um pequeno caminho de processamento de entrada e depois avalie os custos de segurança da escolha de C.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoAcompanhe os dados pelo programa
  2. 2
    ExplicaçãoUse a regra como uma pergunta
  3. 3
    DesafioMantenha a entrada como dados
  4. 4
    ExemploPasse a capacidade a partir da função chamadora
  5. 5
    ExemploDiferencie um prefixo seguro de uma operação bem-sucedida
  6. 6
    DesafioTrate uma linha incompleta
  7. 7
    ExemploInspecione o fim da conversão
  8. 8
    ExemploAplique a faixa do programa
  9. 9
    ExemploPreserve a responsabilidade pela alocação ao redimensionar
  10. 10
    DesafioPlaneje o caminho de falha
  11. 11
    ExplicaçãoIdentifique o limite de segurança
  12. 12
    ConexãoEscolha camadas com funções diferentes
  13. 13
    DesafioEscolha uma linguagem
  14. 14
    DesafioRevise o caminho completo da entrada
  15. 15
    RecapitulaçãoMantenha cada limite visível

Questões de Depuração em C

50 Questões de Prática em 5 conjuntos

Conjunto 1: Pistas do compilador e decisões no depurador10 Questões de Prática

Questão 1 de 10

Uma compilação sem avisos com -Wall converte silenciosamente uma contagem double para int. Qual opção adicional de aviso visa essa alteração implícita de valor?
  1. -Wshadow
  2. -Wformat=2
  3. -Wconversion
  4. -Wpedantic
Mostrar respostaOcultar resposta
Resposta: C
-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

Um programa deve imprimir uma leitura com parte fracionária. Ele declara double reading = 7.25; e chama printf("%d\n", reading);. Qual correção preserva esse objetivo?
  1. Use %f com reading.
  2. Converta reading para int.
  3. Use %ld com reading.
  4. Converta reading para long.
Mostrar respostaOcultar resposta
Resposta: A
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

Um depurador mostra endereços de funções, mas não consegue exibir linhas do código-fonte nem nomes de variáveis locais. Qual opção do compilador fornece as informações do código-fonte que faltam?
  1. -Wextra
  2. -DNDEBUG
  3. -Werror
  4. -g
Mostrar respostaOcultar resposta
Resposta: D
-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

Uma entrada com 240 registros causa um encerramento inesperado, mas cada metade é processada com sucesso quando executada sozinha. Qual é a estratégia de redução mais útil em seguida?
  1. Mantenha apenas o último registro e teste-o como causa independente.
  2. Remova partes menores enquanto preserva a sequência que falha.
  3. Dobre cada buffer e descarte a entrada original.
  4. Execute uma das metades bem-sucedidas até a falha retornar.
Mostrar respostaOcultar resposta
Resposta: B
A falha pode depender de vários registros em conjunto ou de estado acumulado, portanto mantenha uma sequência que falha enquanto remove partes menores. Selecionar um único registro apenas por ser o último pressupõe uma independência que as duas metades bem-sucedidas não comprovam. Toda redução deve ser verificada em relação à falha real.

Questão 5 de 10

A execução está pausada em uma chamada que você quer tratar como um único passo no código-fonte. Qual comando do depurador normalmente avança sobre essa chamada?
  1. step
  2. next
  3. print
  4. backtrace
Mostrar respostaOcultar resposta
Resposta: B
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

Há um ponto de parada em result = compute();. Imprimir result mostra seu valor anterior, e nenhuma chamada foi executada nessa parada. Como você deve inspecionar o resultado calculado?
  1. Digite print result novamente sem retomar.
  2. Exclua o ponto de parada e edite o inicializador.
  3. Vá para a função chamadora sem executar a chamada.
  4. Use next e inspecione o novo result.
Mostrar respostaOcultar resposta
Resposta: D
A parada ocorre antes da atribuição, portanto 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

Um rastreamento da pilha mostra o registro de ativação zero em strlen e o registro dois na sua função render. Você quer inspecionar o ponteiro caption de render no GDB. O que deve fazer?
  1. Execute frame 2 e depois p caption.
  2. Execute frame 0 e depois p caption.
  3. Execute next e depois p caption.
  4. Execute continue e depois p caption.
Mostrar respostaOcultar resposta
Resposta: A
A seleção de um registro de ativação disponibiliza o contexto local da função escolhida sem executar mais código. 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

Uma contagem se torna incorreta entre duas observações conhecidas, uma correta e outra incorreta, mas o local da escrita é desconhecido. Qual recurso do depurador localiza melhor essa escrita?
  1. Um ponto de parada na chamada final de printf.
  2. Um rastreamento da pilha após o término normal.
  3. Um ponto de observação na variável de contagem ativa.
  4. Uma listagem do código-fonte ao redor da declaração.
Mostrar respostaOcultar resposta
Resposta: C
Um ponto de observação para quando o armazenamento monitorado muda, revelando a operação responsável e os valores antigo e novo. Um ponto de parada na impressão final apenas observa o estado já incorreto. Compare cada escrita que causa uma parada com a regra pretendida para a contagem, pois algumas alterações nessa variável podem ser legítimas.

Questão 9 de 10

Um programa sem avisos produz totais diferentes com -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?
  1. Remova a otimização permanentemente e aceite o total original da depuração.
  2. Mantenha -O2 e trate a variável ausente como corrupção.
  3. Desative os símbolos de depuração e compare os endereços impressos.
  4. Investigue UB com sanitizadores e use -g -O0 para avançar passo a passo.
Mostrar respostaOcultar resposta
Resposta: D
A otimização pode remover legitimamente valores intermediários, enquanto a mudança nos totais é um motivo para investigar operações indefinidas no código. Uma versão compilada com sanitizadores ajuda a encontrar essas operações, e uma versão para depuração com baixa otimização facilita a inspeção. Aceitar apenas o resultado da depuração deixa sem solução a causa da diferença na versão para distribuição.

Questão 10 de 10

Um analisador é encerrado inesperadamente em 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?
  1. Mantenha o buffer maior e desative o ASan nesse teste.
  2. Rastreie e corrija o limite da escrita e mantenha a entrada.
  3. Substitua a chamada do alocador por uma asserção e mantenha o buffer maior.
  4. Intercepte o encerramento no alocador e continue com os registros.
Mostrar respostaOcultar resposta
Resposta: B
O relatório do ASan identifica uma escrita inválida que permanece apesar da mudança no sintoma. Corrija o índice ou o limite da cópia, execute novamente a entrada original e mantenha-a como teste de regressão. Aumentar o armazenamento próximo pode ocultar a corrupção de uma verificação posterior do alocador sem tornar válido o acesso original.
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

Para int a com sinal e int b positivo, qual condição detecta um estouro em a + b sem avaliar essa soma?
  1. a > INT_MAX - b
  2. a + b > INT_MAX
  3. a > INT_MAX + b
  4. a - b > INT_MAX
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Duas chamadas independentes são passadas como argumentos em merge(left(), right()). Qualquer ordem de avaliação é permitida e nenhum estado compartilhado cria conflito. Como essa ordem é classificada em C?
  1. Comportamento indefinido.
  2. Comportamento não especificado.
  3. Comportamento definido pela implementação.
  4. Uma ordem obrigatória da esquerda para a direita.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Considere um unsigned int de 32 bits. Qual valor unsigned int slots = 0u - 1u; armazena?
  1. Zero, pois valores sem sinal não podem ser negativos.
  2. Menos um, preservando o resultado matemático.
  3. Nenhum valor definido, pois a subtração causa estouro.
  4. 4.294.967.295, pela redução modular sem sinal definida.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Considere um unsigned int de 32 bits. Uma entrada fornece shift = 32. Qual é o status de 1u << shift?
  1. Indefinido: a quantidade de deslocamento atinge a largura do tipo.
  2. Zero definido: todos os bits ativos são descartados além da largura do resultado.
  3. Um definido: a quantidade sofre redução modular pela largura.
  4. Definido pela implementação: deslocamentos sem sinal variam.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um arranjo tem sete elementos. Qual é o maior índice válido de elemento?
  1. 7
  2. 8
  3. 6
  4. 5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma função retorna local após declarar char local[24] = "ready";. Por que ler o resultado na função chamadora é inválido?
  1. O arranjo tem espaço não usado demais para uma cadeia de caracteres.
  2. O retorno converte o arranjo em um literal de cadeia de caracteres.
  3. O tempo de vida do arranjo local terminou no retorno da função.
  4. A função chamadora deve aplicar sizeof antes de qualquer leitura da cadeia.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma função auxiliar executa 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?
  1. Mantenha a ordem original e torne volatile a variável local que recebeu o valor.
  2. Mantenha a ordem e compile sem -Werror.
  3. Imprima p entre a leitura e a verificação.
  4. Mova o teste de nulidade para antes da primeira desreferenciação.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Em uma plataforma com 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?
  1. (long long)(units * rate)
  2. (long long)units * rate
  3. (int)((long long)units * rate)
  4. (long long)(int)(units * rate)
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma função C é responsável por um buffer no heap por meio de 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?
  1. Descarte a referência antiga, use a alocação retornada e libere-a uma vez.
  2. Mantenha a referência antiga, aumente o novo bloco e libere os dois ponteiros.
  3. Verifique se a referência é nula, escreva por ela e libere apenas esse ponteiro.
  4. Use a alocação retornada e libere a referência antes do ponteiro responsável.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma versão compilada com verificações usa apenas -fsanitize=undefined. Uma multiplicação de size_t sofre redução modular, mas não produz relatório. Por que isso pode acontecer?
  1. A verificação só executa quando NDEBUG está definido.
  2. A redução fica oculta porque size_t é um ponteiro.
  3. A redução modular sem sinal é definida e está fora desse grupo.
  4. O UBSan verifica apenas escritas em arranjos da pilha e do heap.
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

Uma função auxiliar privada de lista promete que sua contagem armazenada sempre será igual ao número de nós após cada atualização. Que tipo de condição de programação é essa?
  1. Uma invariante.
  2. Um formato de entrada.
  3. Um resultado de alocação.
  4. Uma opção do compilador.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma aplicação usa 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?
  1. Uma asserção estática sobre a mesma variável de tempo de execução.
  2. Uma asserção adicional após a alocação ter sucesso.
  3. Uma conversão de user_count para um tipo sem sinal.
  4. Uma verificação de faixa comum com um caminho de erro.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

O ASan relata uma escrita de quatro bytes “0 bytes after 28-byte region”. Considere que cada elemento do arranjo tenha quatro bytes. Qual índice tentado começa nesse endereço?
  1. Índice 6.
  2. Índice 7.
  3. Índice 8.
  4. Índice 28.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um teste precisa identificar usos comuns de valores não inicializados. Por que uma versão compilada apenas com ASan é insuficiente?
  1. O ASan acompanha esses usos apenas no heap.
  2. O ASan acompanha esses usos apenas em variáveis globais.
  3. O ASan não acompanha esses usos de valores.
  4. O ASan precisa do modo de recuperação para acompanhá-los.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um teste automatizado com UBSan registra um estouro de inteiro com sinal, mas continua e retorna sucesso. Qual opção faz um erro de UB detectado interromper a execução?
  1. -fno-omit-frame-pointer: preserva registros de ativação.
  2. -Wconversion: diagnostica alterações implícitas de valor.
  3. -fno-sanitize-recover=undefined
  4. -D_FORTIFY_SOURCE=2: verifica cópias compatíveis.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um teste escreve além de um membro de arranjo de uma estrutura, atingindo o membro seguinte, e também provoca redução modular em um produto sem sinal de quantidade de bytes. ASan e -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?
  1. Inspecione os limites do membro e proteja o produto de tamanho sem sinal antes do uso.
  2. Aceite os limites do membro e inspecione apenas a aritmética com sinal no total.
  3. Aumente a estrutura e aceite como verificado o tamanho de alocação reduzido modularmente.
  4. Mantenha o índice do membro e transforme cada contagem armazenada em uma asserção.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Uma equipe quer investigar tanto acessos além dos limites no heap quanto usos de valores não inicializados. Sua versão atual com ASan liga bibliotecas de terceiros compiladas normalmente. Qual organização das verificações atende aos requisitos das duas ferramentas?
  1. Combine a instrumentação de ASan e MSan, mas mantenha todas as bibliotecas de terceiros inalteradas.
  2. Use UBSan no lugar de ambos e mantenha as bibliotecas inalteradas.
  3. Execute o executável com ASan no Valgrind em todos os testes.
  4. Mantenha ASan e compile MSan separadamente com dependências compatíveis.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Você tem um despejo de memória de um processo que foi encerrado inesperadamente e cuja falha não é mais reproduzida. O que você pode fazer usando o executável correspondente?
  1. Continuar a partir do encerramento e alterar a entrada.
  2. Inspecionar registros de ativação e variáveis salvas disponíveis.
  3. Executar novamente escritas anteriores a partir do rastreamento salvo.
  4. Recompilar o despejo em um executável corrigido.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma função auxiliar executa 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?
  1. Incremente separadamente e depois verifique o limite interno com uma asserção.
  2. Mantenha o incremento em assert e use -O0.
  3. Substitua a asserção por static_assert sem alterá-la.
  4. Torne used sem sinal e mantenha a asserção.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um teste por fuzzing encontra um encerramento inesperado quando um analisador converte “-3”, converte-o para 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?
  1. Aumente a alocação, converta o tipo primeiro e aceite qualquer conversão diferente de zero.
  2. Verifique o preenchimento após a execução e decida se a conversão teve sucesso.
  3. Valide a conversão e a contagem, proteja o produto do tamanho e verifique a alocação.
  4. Torne a contagem de preenchimento sem sinal e mantenha a conversão e a alocação existentes.
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

Um programa deve imprimir uma cadeia terminada em nulo fornecida em um arquivo sem interpretar conversões de formato contidas nela. Qual chamada faz isso?
  1. printf(label);
  2. printf("%s", label);
  3. printf("%d", label);
  4. printf("%p", label);
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma função chamadora passa 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?
  1. O arranjo de 48 bytes da função chamadora.
  2. O comprimento atual da cadeia de caracteres.
  3. O tamanho do arranjo mais um terminador.
  4. O parâmetro ponteiro de oito bytes.
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?
  1. A saída completa não coube.
  2. Exatamente onze caracteres foram armazenados.
  3. A saída coube com seu terminador.
  4. Um erro de codificação impediu a saída.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um analisador decimal rigoroso define 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?
  1. O valor numérico retornado é diferente de zero.
  2. O argumento de base é decimal.
  3. O ponteiro de fim da conversão identifica texto ao final.
  4. O resultado cabe na faixa de long.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um programa aceita contagens decimais de 1 a 900. Ele analisa “ -4rest” com 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?
  1. Converta o prefixo, multiplique e rejeite resultado zero e falha de alocação.
  2. Exija conversão completa e faixa aceita e proteja o produto sem sinal.
  3. Aceite o prefixo quando errno for zero e imponha apenas a capacidade do buffer.
  4. Remova o sinal de menos, aceite o prefixo e use uma asserção para o tamanho.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um arranjo está sob responsabilidade apenas de 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?
  1. Proteja o produto, redimensione por um temporário e escreva apenas após o sucesso.
  2. Calcule o produto primeiro, redimensione diretamente em a e verifique o tamanho antigo.
  3. Redimensione por um temporário, ignore um resultado nulo e escreva pelo a original.
  4. Verifique apenas nulidade, substitua a e valide o produto após a escrita.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Um buffer de linha de 12 bytes recebe 11 caracteres de 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?
  1. Remova o último caractere armazenado e aceite o prefixo como linha completa.
  2. Analise o prefixo e deixe a próxima chamada criar um registro.
  3. Acrescente um terminador e declare a linha completa.
  4. Determine a entrada restante e aplique a política para linhas.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma função recebe um ponteiro de destino e capacidade 7. Ela deve preservar uma entrada completa de nove caracteres ou informar falha. Ela usa strncpy(dst, src, sizeof dst) e depois imprime dst com %s. Considere ponteiros de oito bytes. Qual reformulação atende ao requisito?
  1. Mantenha sizeof dst, acrescente um terminador após oito bytes e imprima o prefixo.
  2. Copie seis caracteres, acrescente um terminador e informe sucesso para o prefixo.
  3. Use a capacidade fornecida, verifique o resultado da cópia limitada e rejeite truncamento.
  4. Use a entrada como formato de printf para dispensar a verificação do tamanho do destino.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um programa agora é compilado em C23. Ele lê um 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?
  1. Chame realloc para todo tamanho, atribua ao responsável e verifique o estouro do produto.
  2. Trate zero separadamente, proteja o produto e use um ponteiro temporário.
  3. Converta o produto para int com sinal e chame realloc.
  4. Use assert(new_n > 0) e substitua diretamente o ponteiro responsável.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um novo analisador de arquivos manipula dados da internet e não tem restrições de integração, hardware ou recursos que exijam C. Qual escolha de projeto reduz mais diretamente o risco de gerenciar manualmente limites e tempos de vida?
  1. Use uma linguagem com segurança de memória para o analisador.
  2. Use C apenas com um buffer padrão maior.
  3. Use C e desative todas as alocações dinâmicas.
  4. Use C apenas com randomização de endereços.
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

Um ponteiro é usado após seu objeto ter sido liberado. Qual parte da segurança de memória é violada?
  1. Segurança de cadeias de formatação.
  2. Segurança espacial.
  3. Segurança numérica.
  4. Segurança temporal.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Para 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”?
  1. Salve i em old, incremente i e imprima old e i.
  2. Troque a ordem dos dois argumentos e mantenha o incremento dentro da chamada.
  3. Adicione parênteses ao redor de ++i e mantenha os dois argumentos.
  4. Converta ambos os argumentos para unsigned na mesma chamada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Uma avaliação de risco CERT atribui gravidade 3, probabilidade 2 e pontuação de correção 2. Qual é a prioridade e o nível resultantes?
  1. Prioridade 7, nível L2.
  2. Prioridade 9, nível L2.
  3. Prioridade 12, nível L1.
  4. Prioridade 18, nível L1.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Um texto é deslocado para a esquerda dentro do mesmo arranjo, e as regiões de bytes de origem e destino se sobrepõem. Qual função fornece cópia definida nessa situação?
  1. memcpy
  2. memmove
  3. strncpy
  4. snprintf
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Uma função é responsável por 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?
  1. Mantenha a contagem em int, adicione um byte e libere antes de retornar.
  2. Use size_t, mas aloque apenas os bytes do comprimento e transfira o bloco resultante.
  3. Use comprimento size_t verificado, espaço para o terminador e transfira uma alocação válida e verificada.
  4. Mantenha a contagem em int e a alocação, mas remova apenas a liberação antecipada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um analisador remove a quebra de linha de uma linha completa, chama 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?
  1. Se a entrada tem uma vírgula decimal e se algum algarismo a precede.
  2. Apenas se o resultado é igual a LONG_MAX.
  3. Apenas se a conversão usou base decimal.
  4. Ausência de algarismos, texto ao final e faixa válida de índices.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Um laço dependente do tamanho é testado apenas com um elemento sob ASan e UBSan. Ele passa. Em produção, falha com um buffer cheio e asserções desativadas. Qual plano de verificação aborda essas diferenças?
  1. Exercite entradas de valores-limite em versões com verificações e para distribuição.
  2. Repita o teste existente de um elemento com verificações e impressões de diagnóstico adicionais.
  3. Mantenha as asserções ativadas e ignore o caso de buffer cheio.
  4. Execute análise estática e abandone todos os testes dinâmicos.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um analisador legado em C ativa proteção de pilha e ASLR. Uma entrada gerada por fuzzing ainda desencadeia uma escrita além de um bloco do heap detectada pelo ASan. Qual resposta combina corretamente prevenção e mitigação?
  1. Remova o sanitizador porque o reforço de segurança define a escrita.
  2. Corrija o limite no heap e mantenha as proteções para distribuição.
  3. Desative ASLR para tornar válida a escrita inválida.
  4. Aumente o bloco do heap até o encerramento inesperado ser adiado.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Após um encerramento inesperado em uma função de cadeia de caracteres, o registro de ativação zero não tem variáveis locais úteis. O registro três no seu código passa um ponteiro retornado por uma busca. Esse ponteiro é nulo, e entradas ausentes são válidas. Qual correção e verificação devem ser feitas?
  1. Adicione uma asserção em tempo de execução na chamada à biblioteca e mantenha a função chamadora.
  2. Substitua a busca por um arranjo alocado maior.
  3. Use continue até o ponteiro se tornar não nulo.
  4. Trate a ausência de resultado antes da chamada e teste essa entrada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma função abre um arquivo de saída e aloca dois buffers. A segunda alocação falha. Sua rotina de liberação fecha o arquivo e retorna, mas nunca libera o primeiro buffer. Qual correção preserva tanto a responsabilidade pela liberação quanto a informação de erro?
  1. Informe a falha e feche o arquivo, mas mantenha o primeiro buffer sem função chamadora para recebê-lo.
  2. Libere o primeiro buffer e o arquivo, mas retorne sucesso pelo trabalho incompleto.
  3. Libere o buffer adquirido, verifique o fechamento e informe que a operação falhou.
  4. Libere o segundo resultado nulo e feche o arquivo, deixando a primeira alocação sem liberação.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

78 Termos-Chave, cada um com sua definição

A

Acesso fora dos limites
Um acesso fora dos limites lê ou escreve fora dos elementos válidos de um objeto, mesmo que o endereço esteja mapeado.
O índice seis está fora de um arranjo de seis elementos porque seu último elemento válido está no índice cinco.
AddressSanitizer
O AddressSanitizer é uma instrumentação baseada no compilador que detecta, em tempo de execução, muitas operações inválidas envolvendo limites, tempo de vida e liberação de memória.
Um relatório pode identificar a escrita inválida e a alocação cujos limites essa escrita ultrapassou.
Análise estática
A análise estática examina o código-fonte e possíveis caminhos de execução sem executar o programa com entradas de teste.
Um analisador pode encontrar um caminho de liberação dupla que nenhum teste de regressão atual alcança.
Aritmética com verificação de estouro
A aritmética com verificação de estouro detecta um resultado matemático que não pode ser representado sem exigir que o programa execute um estouro indefinido.
ckd_add do C23 informa se uma soma cabe no tipo para que o programa possa tratar a falha explicitamente.
ASLR
A randomização do leiaute do espaço de endereçamento (ASLR) varia as posições de memória entre execuções para dificultar alguns ataques que exploram defeitos de memória restantes.
ASLR pode dificultar a exploração de um acesso inválido, deixando sem correção a escrita original fora dos limites.
Asserção
Uma asserção é uma verificação executável de uma suposição de programação que informa a falha e aborta quando está ativada e é falsa.
Uma função auxiliar interna que calcula médias pode usar uma asserção para verificar se sua função chamadora validada fornece uma contagem diferente de zero.
Asserção estática
Uma asserção estática verifica uma expressão constante durante a compilação e faz a compilação falhar quando a condição é falsa.
Um 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
A avaliação de expressão no depurador calcula uma expressão usando o estado atual do registro de ativação selecionado enquanto a execução está pausada.
Imprimir count observa um escalar; avaliar *p tenta um acesso por meio desse ponteiro.
Aviso do compilador
Um aviso do compilador informa código suspeito ou um problema detectado, normalmente permitindo que a compilação continue.
Uma atribuição usada como condição pode compilar com sucesso e ainda receber um aviso.

C

Cadeia de formatação
Uma cadeia de formatação informa a uma função de entrada e saída formatada como interpretar os argumentos, portanto suas conversões devem corresponder aos tipos reais deles.
Um formato de printf usa %zu para size_t; o texto do usuário pertence a um argumento de dados separado.
Caminho de liberação de recursos
Um caminho de liberação de recursos libera os recursos realmente adquiridos antes do retorno de uma função, inclusive após uma operação parcialmente concluída.
Uma seção compartilhada de liberação pode liberar um buffer e fechar um arquivo após uma alocação posterior falhar.
Capacidade do buffer
A capacidade do buffer é a quantidade total de armazenamento disponível para escrita, incluindo qualquer espaço necessário para o terminador de uma cadeia de caracteres.
Um arranjo de dez bytes pode armazenar no máximo nove caracteres de um byte como uma cadeia de caracteres em C terminada em nulo.
Caso mínimo de reprodução
Um caso mínimo de reprodução é uma pequena entrada e um caso de programa que ainda desencadeiam a falha investigada.
A remoção de registros sem relação com o problema da entrada de um analisador com encerramento inesperado deixa um campo vazio que ainda desencadeia consistentemente esse encerramento.
Código não conforme
Código não conforme é um exemplo que viola uma regra específica de codificação nas condições descritas por essa regra.
Usar printf(input) permite que dados externos forneçam conversões de formato e viola FIO30-C.
Comportamento definido pela implementação
O comportamento definido pela implementação permite uma escolha que cada implementação de C deve documentar para que os programas possam levá-la em conta.
Uma implementação de C17 documenta se seu tipo char simples tem ou não sinal.
Comportamento indefinido
Comportamento indefinido é um comportamento para o qual o padrão C não impõe requisitos após a ocorrência de uma operação inválida.
Um estouro de inteiro com sinal pode parecer retornar ao início da faixa em uma versão compilada e se comportar de forma diferente após a otimização.
Comportamento não especificado
O comportamento não especificado permite uma escolha entre resultados válidos sem exigir que a implementação documente qual escolhe.
Chamadas independentes usadas como argumentos podem ser avaliadas em qualquer ordem sem invalidar a chamada.
Comprimento da cadeia de caracteres
O comprimento da cadeia de caracteres é o número de caracteres antes do primeiro terminador nulo, excluindo o próprio caractere terminador.
strlen("safe") é quatro, enquanto armazenar a cadeia completa exige cinco bytes.
Conversão implícita
Uma conversão implícita altera o tipo de um valor sem uma conversão explícita escrita e pode perder informações ou alterar o valor.
Atribuir um double a um int descarta a parte fracionária quando o valor truncado cabe no tipo.
Corrida de dados
Uma corrida de dados ocorre quando threads fazem acessos conflitantes sem a sincronização necessária, sendo que pelo menos um dos acessos escreve.
Duas atualizações sem sincronização em um estado compartilhado não precisam acontecer no mesmo instante para formar uma corrida.

D

Defeito
Um defeito é um erro no código que pode causar comportamento incorreto quando um caminho ou uma entrada específica o alcança.
Uma condição de laço pode estar errada mesmo em execuções nas quais o acesso inválido que ela causa não produz encerramento inesperado visível.
Defesa em profundidade
A defesa em profundidade combina prevenção, detecção e redução de danos para que várias proteções tratem dos defeitos restantes.
Avisos e revisão evitam defeitos, sanitizadores os detectam em testes e o reforço de segurança limita parte dos danos.
Depurador
Um depurador controla a execução de um programa para que você possa pausá-lo, inspecionar seu estado e avançar por operações selecionadas.
GDB e LLDB podem parar antes do corpo de um laço para que você compare o índice atual com o limite.
Despejo de memória do processo
Um despejo de memória do processo é um retrato salvo de um processo no momento de um encerramento inesperado que um depurador pode inspecionar usando o executável correspondente.
Um despejo salvo permite rastrear a pilha e inspecionar valores locais depois que o processo que falhou terminou.
Diagnóstico do compilador
Um diagnóstico do compilador é uma mensagem que identifica uma localização no código-fonte e um problema suspeito ou confirmado encontrado durante a compilação.
Um diagnóstico de formato pode apontar para %d e identificar o argumento double que não corresponde a ele.

E

Efeitos colaterais não sequenciados
Efeitos colaterais não sequenciados são alterações e usos conflitantes do mesmo escalar sem a ordenação que C exige para uma avaliação definida.
Passar tanto i quanto ++i a uma chamada combina uma leitura e uma alteração sem ordenação segura.
Entrada não confiável
Entrada não confiável é um dado fora do controle do programa cujo formato, comprimento e faixa devem ser verificados antes de um uso sensível.
Uma contagem de linha de comando precisa ser validada antes de controlar uma alocação de arranjo ou um limite de laço.
Erro de compilação
Um erro de compilação é um diagnóstico que impede uma compilação bem-sucedida até que o problema relatado seja resolvido.
Corrigir a falta de um ponto e vírgula pode eliminar várias mensagens posteriores causadas pelo mesmo erro de sintaxe.
Erro de faixa
Um erro de faixa ocorre quando o valor matemático de uma conversão está fora da faixa representável do tipo de resultado.
Após definir errno como zero, um erro de faixa de strtol define ERANGE, mesmo que um valor-limite seja retornado.
Estouro de inteiro com sinal
O estouro de inteiro com sinal ocorre quando um resultado aritmético não cabe no tipo com sinal de destino e tem comportamento indefinido.
Com 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
A execução passo a passo avança a execução por uma quantidade escolhida no código-fonte, permitindo inspeção entre instruções ou chamadas de função.
Use next para avançar sobre uma chamada e step quando precisar entrar no código da própria função.

F

Falha
Uma falha é um desvio observado em relação ao comportamento pretendido, como um encerramento inesperado, um resultado errado ou uma saída ausente.
Um encerramento inesperado em free pode ser a falha visível causada por uma escrita muito anterior na execução.

H

Hipótese
Uma hipótese de depuração é uma explicação proposta para uma falha que faz uma previsão testável por meio de uma observação ou alteração.
Suspeitar de um campo ausente sugere inspecionar seu ponteiro imediatamente antes do processamento da cadeia de caracteres.

I

Informações de depuração
As informações de depuração descrevem nomes, tipos e localizações do código-fonte em um executável para que ferramentas de inspeção possam relacionar instruções ao código.
A opçã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
A injeção de comandos do shell ocorre quando texto externo altera a estrutura de um comando do shell e causa comandos ou operações não pretendidos.
Construir uma cadeia para system() a partir de um argumento não verificado permite que um shell interprete a sintaxe fornecida.
Instrumentação
A instrumentação adiciona código de verificação ou observação a um programa para que ferramentas possam inspecionar operações durante a execução.
Uma versão compilada com ASan precisa de arquivos-objeto instrumentados e do ambiente de execução do sanitizador na ligação final.
Invariante
Uma invariante é uma propriedade mantida ao longo de etapas repetidas ou estados válidos de um laço ou estrutura de dados.
A contagem armazenada de uma lista deve corresponder ao número de seus nós após cada inserção concluída.

L

Liberação dupla
Uma liberação dupla tenta liberar a mesma alocação mais de uma vez sem que uma nova alocação válida estabeleça a responsabilidade por ela.
Um ramo de liberação de recursos que libera um bloco já liberado pode desencadear um relatório do alocador ou do ASan.
Linguagem com segurança de memória
Uma linguagem com segurança de memória impõe regras de limites e tempo de vida para suas operações seguras, reduzindo a necessidade de verificações manuais de segurança de memória.
Rust seguro combina regras de responsabilidade e empréstimo com verificações de limites; chamar código C inseguro ainda exige cuidado.

M

Memória sombra
A memória sombra é um armazenamento auxiliar que uma ferramenta de verificação usa para registrar informações sobre os bytes de memória da aplicação.
O ASan consulta informações da memória sombra para verificar se um acesso instrumentado toca em armazenamento permitido.
MemorySanitizer
O MemorySanitizer é a ferramenta do Clang que rastreia em tempo de execução usos de valores não inicializados e exige instrumentação compatível das dependências.
Ele visa uma classe de erros de valores não inicializados que a verificação comum do ASan não acompanha.

O

Otimização
A otimização transforma um programa para melhorar a execução ou o uso de armazenamento, preservando o comportamento exigido do código definido.
Com -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
Um padrão de codificação segura é um conjunto de requisitos e recomendações aplicáveis para evitar defeitos e vulnerabilidades conhecidos.
Uma lista de verificação de revisão pode relacionar cada verificação de tamanho de alocação ao defeito que ela deve evitar.
Padrão de codificação segura em C do SEI CERT
O Padrão de codificação segura em C do SEI CERT é a coleção de regras, recomendações, exemplos e avaliações de risco para C do SEI da Carnegie Mellon.
Sua regra MEM35-C ajuda revisores a perguntar se uma alocação contém bytes suficientes para seus objetos.
Pilha de chamadas
A pilha de chamadas é a cadeia de chamadas de função ativas, da função atual para fora, passando pelas funções que a chamaram.
Uma chamada da biblioteca de cadeias de caracteres no registro zero pode ter recebido seu ponteiro inválido de main.
Ponteiro de fim da conversão
Um ponteiro de fim da conversão fornecido por strtol identifica o primeiro caractere não convertido ou o início da entrada se nenhum algarismo foi consumido.
Para “73x”, o ponteiro de fim da conversão identifica o “x” final que um analisador numérico rigoroso rejeita.
Ponteiro pendente
Um ponteiro pendente refere-se a armazenamento cujo objeto teve seu tempo de vida encerrado, portanto não pode fornecer acesso válido ao objeto antigo.
Retornar o endereço de um arranjo local deixa a função chamadora com um ponteiro para armazenamento cujo tempo de vida terminou.
Ponto de observação
Um ponto de observação interrompe a execução quando uma posição de memória monitorada muda, ajudando a identificar a instrução que escreveu seu valor.
Observar a contagem de uma estrutura pode revelar uma atribuição inesperada muito antes de um encerramento posterior.
Ponto de parada
Um ponto de parada é um local de execução escolhido onde um depurador interrompe o programa, normalmente antes da execução da instrução indicada.
Um ponto de parada antes de sum += a[i] permite inspecionar i sem avaliar o acesso ao arranjo.
Ponto de parada condicional
Um ponto de parada condicional interrompe a execução em um local escolhido apenas quando sua condição é avaliada como verdadeira no estado atual do programa.
Uma condição como i == 650 evita avançar manualmente pelas iterações anteriores do laço.
Portabilidade
A portabilidade é a capacidade de um código se comportar como pretendido em diferentes implementações sem depender de suposições não garantidas sobre a plataforma.
Usar INT_MAX evita presumir que toda implementação atribui o mesmo valor máximo a int.
Pós-condição
Uma pós-condição é uma propriedade que uma operação promete estabelecer quando é concluída com sucesso.
Após a ordenação de um arranjo, uma pós-condição pode estabelecer que cada elemento não seja maior que o seguinte.
Pré-condição
Uma pré-condição é um requisito que deve ser verdadeiro quando uma função ou operação começa, estabelecido por sua função chamadora ou por uma validação anterior.
Uma função auxiliar privada pode exigir um ponteiro não nulo antes de examinar o primeiro elemento do arranjo.
Prioridade de risco
Uma prioridade de risco do CERT é o produto das pontuações de gravidade, probabilidade e correção, usado para ajudar a ordenar o trabalho de aplicação de regras ou de correção.
Uma pontuação maior de correção aumenta a prioridade porque a correção correspondente é mais barata de executar.
Promoção inteira
Uma promoção inteira converte um tipo inteiro menor para int ou unsigned int antes de muitas operações aritméticas e de deslocamento de bits.
A validade da quantidade de deslocamento depende da largura do operando esquerdo após a promoção, não apenas de sua declaração original.

Q

Quarentena
Uma quarentena do ASan atrasa a reutilização de blocos liberados, aumentando a chance de que acessos antigos atinjam memória ainda marcada como inválida.
Um ponteiro pendente pode ser detectado antes que o bloco liberado seja reutilizado para outra alocação.

R

Rastreamento da pilha de chamadas
Um rastreamento da pilha de chamadas é um relatório do depurador sobre os registros de chamadas ativos, normalmente numerados a partir do registro mais interno como zero.
Após um encerramento inesperado, bt ajuda você a localizar o primeiro registro referente ao seu código-fonte.
Redução modular sem sinal
A redução modular sem sinal reduz um resultado aritmético à faixa do tipo sem sinal; ela é definida, mas pode violar as suposições de tamanho de um programa.
Um produto de quantidade de bytes que sofre estouro pode solicitar uma alocação muito menor que a necessária para o arranjo pretendido.
Reforço de segurança
O reforço de segurança adiciona proteções destinadas a detectar alguma corrupção ou limitar os danos de defeitos que permanecem em um programa distribuído.
Uma verificação do protetor de pilha pode interromper uma função após detectar uma sobrescrita de seu canário.
Regiões de memória sobrepostas
Regiões de memória sobrepostas compartilham pelo menos um byte de armazenamento, portanto copiar entre elas exige uma operação que preserve os dados originais da origem.
Deslocar texto dentro do mesmo arranjo pode exigir memmove porque memcpy proíbe sobreposição.
Registro de ativação
Um registro de ativação é o contexto de execução de uma chamada de função ativa, com seus argumentos disponíveis, variáveis locais e informações de retorno.
Quando uma função chamadora passa um ponteiro name, selecionar o registro da função chamadora após um encerramento inesperado disponibiliza esse ponteiro para inspeção.
Responsabilidade pela liberação
A responsabilidade pela liberação é a responsabilidade de um programa por gerenciar o tempo de vida de um recurso, incluindo liberá-lo e manter uma referência válida a ele.
Um resultado temporário preserva o ponteiro responsável pela alocação original quando realloc com tamanho positivo falha.

S

Segurança espacial de memória
A segurança espacial de memória exige que todo acesso à memória permaneça dentro dos limites do objeto que ele tem permissão para acessar.
Uma verificação de limites antes de uma escrita em arranjo impede que um índice selecione um elemento fora desse arranjo.
Segurança temporal de memória
A segurança temporal de memória exige acesso apenas enquanto um objeto existe e está válido para esse acesso.
Uma referência antiga após free viola a segurança de tempo de vida mesmo quando seu endereço ainda parece familiar.
Solução conforme
Uma solução conforme é um projeto ou exemplo de código que atende ao requisito de uma regra nas condições estabelecidas pela solução.
Usar um formato fixo %s mantém o texto fornecido no argumento de dados de uma chamada de printf.
Sombreamento de variáveis
O sombreamento de variáveis ocorre quando uma declaração em um escopo interno oculta outra declaração com o mesmo nome.
O count de um laço interno pode receber atualizações enquanto o count externo permanece inalterado.

T

Tempo de vida do objeto
O tempo de vida do objeto é o período durante a execução em que um objeto existe e seu armazenamento pode ser acessado segundo as regras de C.
Um arranjo automático local existe durante a chamada de sua função, enquanto uma alocação dura até ser liberada.
Terminador nulo
Um terminador nulo é o caractere zero que marca o fim de uma cadeia de caracteres em C e exige espaço de armazenamento além de seus caracteres visíveis.
A cadeia “ready” precisa de seis bytes para que funções de cadeia de caracteres encontrem um terminador após suas cinco letras.
Teste de regressão
Um teste de regressão verifica um comportamento que falhou anteriormente, para que alterações posteriores possam ser verificadas quanto ao retorno desse defeito.
Uma entrada salva com um registro vazio que antes causou encerramento inesperado de um analisador é executada novamente após cada alteração no analisador para detectar o retorno desse encerramento.
Teste por fuzzing
O teste por fuzzing testa um programa com muitas entradas geradas ou modificadas automaticamente para descobrir casos de falha não encontrados por testes planejados.
Um registro modificado pode alcançar um ramo do analisador no qual um sanitizador detecta um acesso inválido.
ThreadSanitizer
O ThreadSanitizer é uma ferramenta de verificação em tempo de execução que identifica acessos conflitantes de threads à memória compartilhada sem sincronização.
Ele precisa de uma versão compilada separada do ASan ao investigar possíveis corridas de dados.
Truncamento
O truncamento descarta a parte do texto que não cabe em um destino, mesmo quando o prefixo armazenado está corretamente terminado em nulo.
Um retorno não negativo de snprintf pelo menos igual à capacidade indica que o texto completo não coube.

U

UndefinedBehaviorSanitizer
O UndefinedBehaviorSanitizer insere verificações para muitas operações indefinidas, incluindo estouro de inteiro com sinal e deslocamentos de bits inválidos, durante a execução.
Um relatório do UBSan pode identificar uma soma que causa estouro mesmo quando o programa imprime um total plausível.
Uso após a liberação
O uso após a liberação acessa uma alocação depois que ela foi liberada, violando o tempo de vida do objeto original.
Liberar um ponteiro responsável também encerra o acesso válido por meio de uma referência alternativa que ainda contém seu endereço antigo.

V

Valor indeterminado
Um valor indeterminado é o estado de um objeto cujo valor utilizável não foi estabelecido, como ocorre com muitas variáveis automáticas não inicializadas.
Declarar int sum; e usar imediatamente sum += x; lê um valor que nunca foi inicializado.
Versão para depuração
Uma versão para depuração é um executável configurado para inspeção, normalmente com informações do código-fonte e pouca otimização.
Uma versão compilada com -g -O0 facilita acompanhar um laço defeituoso uma instrução do código-fonte por vez.
Versão para distribuição
Uma versão para distribuição é um executável configurado para uso comum, normalmente com otimização e reforço de segurança compatível ativados.
Uma versão para distribuição deve manter as verificações de entrada mesmo quando NDEBUG remove suas asserções.

Z

Zona de proteção
Uma zona de proteção é uma área de memória proibida colocada ao redor de um objeto para o qual há suporte, para que uma ferramenta de verificação possa detectar acessos que ultrapassem seu limite.
Uma escrita extra imediatamente após um bloco do heap pode entrar em sua zona de proteção e acionar o ASan.

Sobre esta aula

Por StudeliRelatar um erro

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.

Começar