No início de 2018, nosso blog foi complementado com uma série de artigos sobre a sexta verificação do código-fonte do projeto Chromium. A série inclui 8 artigos sobre erros e recomendações para sua prevenção. Dois artigos provocaram discussões acaloradas e, ocasionalmente, ainda recebo comentários por correio sobre os tópicos abordados neles. Talvez eu deva dar explicações adicionais e, como eles dizem, esclarecer as coisas.
Um ano se passou desde que escrevemos uma série de artigos sobre uma verificação regular do código-fonte do projeto Chromium:
- Cromo: a sexta verificação do projeto e 250 bugs
- Cromo agradável e Memset desajeitado
- quebra e avanço
- Cromo: vazamentos de memória
- Chromium: Typos
- Crómio: uso de dados não confiáveis
- Por que é importante verificar o que a função malloc retornou
- Crómio: outros erros
Os artigos dedicados ao
memset e ao
malloc causaram e continuam causando debates, o que me parece estranho. Aparentemente, houve alguma confusão devido ao fato de eu ter sido insuficientemente preciso ao verbalizar meus pensamentos. Decidi voltar a esses artigos e fazer alguns esclarecimentos.
memset
Vamos começar com um artigo sobre
memset , porque aqui tudo é simples. Alguns argumentos apareceram sobre a melhor maneira de inicializar estruturas. Muitos programadores escreveram que seria melhor dar a recomendação de não escrever:
HDHITTESTINFO hhti = {};
mas para escrever da seguinte maneira:
HDHITTESTINFO hhti = { 0 };
Razões:
- A construção {0} é mais fácil de notar ao ler o código do que {}.
- A construção {0} é mais intuitivamente compreensível que {}. O que significa que 0 sugere que a estrutura é preenchida com zeros.
Dessa forma, os leitores sugerem que eu mude esse exemplo de inicialização no artigo. Não concordo com os argumentos e não pretendo fazer nenhuma edição no artigo. Agora vou explicar minha opinião e apresentar alguns motivos.
Quanto à visibilidade, acho, é uma questão de gosto e hábito. Não acho que a presença de 0 entre parênteses mude fundamentalmente a situação.
Quanto ao segundo argumento, eu discordo totalmente dele. O registro do tipo {0} fornece um motivo para perceber incorretamente o código. Por exemplo, você pode supor que, se você substituir 0 por 1, todos os campos serão inicializados por um. Portanto, é mais provável que esse estilo de escrita seja prejudicial ao invés de útil.
O analisador PVS-Studio ainda possui um diagnóstico relacionado
V1009 , cuja descrição é citada abaixo.
V1009. Verifique a inicialização do array. Somente o primeiro elemento é inicializado explicitamente.O analisador detectou um possível erro relacionado ao fato de que, ao declarar uma matriz, o valor é especificado apenas para um elemento. Assim, os elementos restantes serão inicializados implicitamente por zero ou por um construtor padrão.
Vamos considerar o exemplo de código suspeito:
int arr[3] = {1};
Talvez o programador esperado que
arr seja composto inteiramente por um, mas não é. A matriz será composta pelos valores 1, 0, 0.
Código correto:
int arr[3] = {1, 1, 1};
Essa confusão pode ocorrer devido à semelhança com a construção
arr = {0} , que inicializa toda a matriz com zeros.
Se essas construções forem usadas ativamente em seu projeto, você poderá desativar este diagnóstico.
Também recomendamos não negligenciar a clareza do seu código.
Por exemplo, o código para codificar valores de uma cor é registrado da seguinte maneira:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff };
Graças à inicialização implícita, todas as cores são especificadas corretamente, mas é melhor reescrever o código mais claramente:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 };
malloc
Antes de ler mais, lembre-se do conteúdo do artigo "
Por que é importante verificar o que a função malloc retornou ". Este artigo deu origem a muitos debates e críticas. Aqui estão algumas das discussões:
reddit.com/r/cpp ,
reddit.com/r/C_Programming ,
habr.com (pt). Ocasionalmente, os leitores ainda me enviam um e-mail sobre este artigo.
O artigo é criticado pelos leitores pelos seguintes pontos:
1. Se o malloc retornou NULL , é melhor encerrar o programa imediatamente, do que escrever um monte de if -s e tentar manipular a memória de alguma forma, devido à qual a execução do programa é frequentemente impossível de qualquer maneira.Não lutei até o fim com as consequências do vazamento de memória, passando o erro cada vez mais alto. Se for permitido que seu aplicativo encerre seu trabalho sem aviso, deixe que seja. Para esse fim, basta uma única verificação logo após o
malloc ou o uso do
xmalloc (consulte o próximo ponto).
Eu opus e avisei sobre a falta de verificações, por causa da qual o programa continua funcionando como se nada tivesse acontecido. É um caso completamente diferente. É perigoso, porque leva a um comportamento indefinido, corrupção de dados e assim por diante.
2. Não há descrição de uma solução que esteja na função de gravar wrapper para alocar memória com uma verificação após ela ou usar funções já existentes, como xmalloc .Concordo, eu perdi esse ponto. Ao escrever o artigo, não estava pensando em como remediar a situação. Era mais importante para mim transmitir ao leitor o perigo da ausência de cheque. Como corrigir um erro é uma questão de gosto e detalhes de implementação.
A função
xmalloc não faz parte da biblioteca C padrão (consulte "
Qual é a diferença entre xmalloc e malloc? "). No entanto, essa função pode ser declarada em outras bibliotecas, por exemplo, na biblioteca GNU utils (
GNU libiberty ).
O ponto principal da função é que o programa falha quando falha ao alocar memória. A implementação desta função pode ter a seguinte aparência:
void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; }
Assim, chamando uma função
xmalloc em vez de
malloc toda vez, você pode ter certeza de que um comportamento indefinido não ocorrerá no programa devido ao uso de um ponteiro nulo.
Infelizmente, o
xmalloc também não é uma cura para todos. Deve-se lembrar que o uso do
xmalloc é inaceitável quando se trata de escrever código de bibliotecas. Eu falo sobre isso mais tarde.
3. A maioria dos comentários foi a seguinte: "na prática, malloc nunca retorna NULL ".Felizmente, não sou o único que entende que esta é a abordagem errada. Eu realmente gostei deste
comentário no meu suporte:
De acordo com minha experiência em discutir esse tópico, sinto que há duas seitas na Internet. Os que aderiram ao primeiro acreditam firmemente que o malloc nunca retorna NULL no Linux. Os apoiadores do segundo afirmam sinceramente que, se a memória não puder ser alocada no seu programa, nada poderá ser feito, você poderá travar. Não há como super-persuadi-los. Especialmente quando essas duas seitas se cruzam. Você só pode tomá-lo como um dado. E nem é importante em qual recurso especializado uma discussão ocorre.Pensei um pouco e decidi seguir o conselho, para não tentar convencer ninguém :). Felizmente, esses grupos de desenvolvedores gravam apenas programas não fatais. Se, por exemplo, alguns dados do jogo forem corrompidos, não há nada crucial nele.
A única coisa que importa é que os desenvolvedores de bibliotecas, bancos de dados não devem fazer assim.
Apelar para os desenvolvedores de código e bibliotecas altamente dependentes
Se você estiver desenvolvendo uma biblioteca ou outro código altamente dependente, sempre verifique o valor do ponteiro retornado pela função
malloc / realloc e retorne um código de erro se a memória não puder ser alocada.
Nas bibliotecas, você não pode chamar a função de
saída , se a alocação de memória falhar. Pelo mesmo motivo, você não pode usar
xmalloc . Para muitas aplicações, é inaceitável simplesmente abortá-las. Por esse motivo, por exemplo, um banco de dados pode estar corrompido. Pode-se perder dados que foram avaliados por muitas horas. Por esse motivo, o programa pode ser alcançado para vulnerabilidades de "negação de serviço", quando, em vez de manipular corretamente a crescente carga de trabalho, um aplicativo multithread simplesmente termina.
Não pode ser assumido, de que maneiras e em quais projetos a biblioteca será usada. Portanto, deve-se supor que o aplicativo possa resolver tarefas muito críticas. É por isso que apenas matá-lo chamando
exit não é bom. Provavelmente, esse programa foi escrito levando em consideração a possibilidade de falta de memória e pode fazer algo nesse caso. Por exemplo, um sistema CAD não pode alocar um buffer de memória apropriado que seja suficiente para operação regular devido à forte fragmentação da memória. Nesse caso, não é o motivo para esmagar no modo de emergência com perda de dados. O programa pode oferecer uma oportunidade para salvar o projeto e reiniciar-se normalmente.
Em nenhum caso, é impossível confiar no
malloc, que sempre poderá alocar memória. Não se sabe em qual plataforma e como a biblioteca será usada. Se a situação de pouca memória em uma plataforma for exótica, pode ser uma situação bastante comum na outra.
Não podemos esperar que, se
malloc retornar
NULL , o programa falhará. Tudo pode acontecer. Como descrevi no
artigo , o programa pode gravar dados não pelo endereço nulo. Como resultado, alguns dados podem estar corrompidos, o que leva a consequências imprevisíveis. Até o
memset é perigoso. Se o preenchimento dos dados for na ordem inversa, primeiro alguns dados serão corrompidos e o programa falhará. Mas o acidente pode ocorrer tarde demais. Se dados contaminados forem usados em threads paralelos enquanto a função
memset estiver funcionando, as consequências poderão ser fatais. Você pode obter uma transação corrompida em um banco de dados ou enviar comandos para a remoção de arquivos "desnecessários". Qualquer coisa tem uma chance de acontecer. Sugiro que um leitor sonhe com você, o que pode acontecer devido ao uso de lixo na memória.
Assim, a biblioteca possui apenas uma maneira correta de trabalhar com as funções
malloc . Você precisa verificar IMEDIATAMENTE se a função retornou e, se for NULL, retorne um status de erro.
Links adicionais
- Manuseio de OOM
- Diversão com ponteiros NULL: parte 1 , parte 2
- O que todo programador C deve saber sobre comportamento indefinido: parte 1 , parte 2 , parte 3