
Foi-nos oferecido o uso do analisador PVS-Studio para verificar a coleção de bibliotecas PMDK abertas destinadas ao desenvolvimento e depuração de aplicativos com suporte a memória não volátil. Na verdade, por que não. Além disso, este é um pequeno projeto em C e C ++ com um tamanho de base de código total de cerca de 170 KLOC, excluindo comentários. Isso significa que revisar os resultados da análise não levará muito tempo e esforço. Vamos.
Para analisar o código-fonte, será utilizada a ferramenta PVS-Studio versão 7.08. Os leitores de nosso blog, naturalmente, estão familiarizados com nossa ferramenta há muito tempo e não vou me alongar sobre isso. Para aqueles que nos procuraram pela primeira vez, sugiro consultar o artigo “ Como visualizar rapidamente avisos interessantes que o analisador PVS-Studio dá para códigos C e C ++? ” E tente uma versão de teste gratuita do analisador.
Desta vez, vou olhar dentro do projeto PMDK e falar sobre os erros e deficiências que percebi. No meu sentimento interior não eram muitos, o que indica a boa qualidade do código deste projeto. Das coisas interessantes, podemos notar que vários fragmentos do código errado foram encontrados, que no entanto funciona corretamente :). O que quero dizer ficará mais claro com uma narração posterior.
Em resumo, o PMDK é uma coleção de bibliotecas e ferramentas de software livre projetadas para simplificar o desenvolvimento, a depuração e o gerenciamento de aplicativos habilitados para memória não volátil. Mais detalhes aqui: Introdução ao PMDK . Fontes aqui: pmdk .
Vamos ver quais erros e falhas posso encontrar nele. Devo dizer desde já que estive longe de estar sempre atento na análise do relatório e poderia ter perdido muitas coisas. Portanto, peço aos autores do projeto que não sejam guiados exclusivamente por este artigo ao consertar defeitos, mas que verifiquem o código por conta própria. E para escrever um artigo, o que escrevi, olhando a lista de avisos, será suficiente para mim :).
Código errado que funciona
Tamanho de alocação de memória
Não é incomum para os programadores perderem tempo depurando o código quando o programa não se comporta da maneira que deveria. No entanto, às vezes há situações em que o programa funciona corretamente, mas o código contém um erro. O programador tem apenas sorte e o erro não se manifesta. No projeto PMDK, encontrei várias situações interessantes ao mesmo tempo e, portanto, decidi colocá-las juntas em um capítulo separado.
int main(int argc, char *argv[])
{
....
struct pool *pop = malloc(sizeof(pop));
....
}
Aviso do PVS-Studio: V568 É estranho que o operador 'sizeof ()' avalie o tamanho de um ponteiro para uma classe, mas não o tamanho do objeto de classe 'pop'. util_ctl.c 717
Um erro de digitação clássico que faz com que a quantidade errada de memória seja alocada. O operador sizeof retornará não o tamanho da estrutura, mas o tamanho do ponteiro para essa estrutura. A opção correta seria:
struct pool *pop = malloc(sizeof(pool));
ou
struct pool *pop = malloc(sizeof(*pop));
No entanto, esse código escrito incorretamente funciona muito bem. A questão é que a estrutura do pool contém exatamente um ponteiro:
struct pool {
struct ctl *ctl;
};
Acontece que a estrutura ocupa exatamente tanto quanto o ponteiro. As coisas estão bem.
Comprimento da linha
Vamos passar para o próximo caso, onde o erro foi cometido novamente usando o operador sizeof .
typedef void *(*pmem2_memcpy_fn)(void *pmemdest, const void *src, size_t len,
unsigned flags);
static const char *initial_state = "No code.";
static int
test_rwx_prot_map_priv_do_execute(const struct test_case *tc,
int argc, char *argv[])
{
....
char *addr_map = pmem2_map_get_address(map);
map->memcpy_fn(addr_map, initial_state, sizeof(initial_state), 0);
....
}
Aviso de PVS-Studio: V579 [CWE-687] A função memcpy_fn recebe o ponteiro e seu tamanho como argumentos. É possivelmente um erro. Inspecione o terceiro argumento. pmem2_map_prot.c 513
Um ponteiro para uma função de cópia especial é usado para copiar uma string. Preste atenção à chamada para esta função, ou melhor, ao seu terceiro argumento.
O programador assume que o operador sizeof calculará o tamanho do literal da string. Mas, na verdade, o tamanho do ponteiro é calculado novamente.
Felizmente, a string consiste em 8 caracteres e seu tamanho é o mesmo do ponteiro, se estiver criando um aplicativo de 64 bits. Como resultado, todos os 8 caracteres da string "Sem código". será copiado com sucesso.
Na verdade, a situação é ainda mais complicada e interessante. A interpretação desse erro depende se você deseja copiar o terminal zero ou não. Vamos considerar dois cenários.
Cenário 1. Foi necessário copiar o terminal zero. Então estou errado, e isso não é um erro inofensivo que não se manifesta. Não foram copiados 9 bytes, mas apenas 8. Não há terminal zero e as consequências não podem ser previstas. Nesse caso, o código pode ser corrigido alterando a definição da string constante initial_state da seguinte maneira:
static const char initial_state [] = "No code.";
Agora, o valor sizeof (initial_state) é 9.
Cenário 2. O terminal zero não é necessário. Por exemplo, abaixo você pode ver a seguinte linha de código:
UT_ASSERTeq(memcmp(addr_map, initial_state, strlen(initial_state)), 0);
Como você pode ver, a função strlen retornará o valor 8 e o terminal zero não será incluído na comparação. Então é realmente sorte e está tudo bem.
Mudança de bit
O próximo exemplo está relacionado a uma operação de deslocamento bit a bit.
static int
clo_parse_single_uint(struct benchmark_clo *clo, const char *arg, void *ptr)
{
....
uint64_t tmax = ~0 >> (64 - 8 * clo->type_uint.size);
....
}
Aviso PVS-Studio: V610 [CWE-758] Comportamento não especificado. Verifique o operador de mudança '>>'. O operando esquerdo '~ 0' é negativo. clo.cpp 205
O resultado de deslocar um valor negativo para a direita depende da implementação do compilador. Portanto, embora esse código possa funcionar corretamente e conforme o esperado em todos os modos de compilação de aplicativos existentes atualmente, ainda é sorte.
Prioridade de operações
E considere o último caso relacionado à prioridade das operações.
#define BTT_CREATE_DEF_SIZE (20 * 1UL << 20) /* 20 MB */
Aviso PVS-Studio: V634 [CWE-783] A prioridade da operação '*' é maior do que a da operação '<<'. É possível que parênteses sejam usados na expressão. bttcreate.c 204
Para obter uma constante igual a 20 MB, o programador decidiu fazer o seguinte:
- Deslocado 1 por 20 bits para obter o valor 1048576, ou seja, 1 MB.
- Multiplicado 1 MB por 20.
Em outras palavras, o programador pensa que os cálculos são feitos assim: (20 * (1UL << 20))
Mas, na verdade, a prioridade do operador de multiplicação é maior do que a do operador de deslocamento, e a expressão é avaliada assim: ((20 * 1UL) << 20).
Concordo, o programador dificilmente queria que a expressão fosse avaliada em tal sequência. Não faz sentido multiplicar 20 por 1. Portanto, este é o caso quando o código não funciona da maneira que o programador pretendia.
Mas esse erro não se manifestará de forma alguma. Não importa como você escreve:
- (20 * 1UL << 20)
- (20 * (1UL << 20))
- ((20 * 1UL) << 20)
O resultado é sempre o mesmo ! O valor desejado é sempre 20971520 e o programa funciona perfeitamente e corretamente.
Outros erros
Parênteses no lugar errado
#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004
static void
enum_handles(int op)
{
....
NTSTATUS status;
while ((status = NtQuerySystemInformation(
SystemExtendedHandleInformation,
hndl_info, hi_size, &req_size)
== STATUS_INFO_LENGTH_MISMATCH)) {
hi_size = req_size + 4096;
hndl_info = (PSYSTEM_HANDLE_INFORMATION_EX)REALLOC(hndl_info,
hi_size);
}
UT_ASSERT(status >= 0);
....
}
Aviso do PVS-Studio: V593 [CWE-783] Considere revisar a expressão do tipo 'A = B == C'. A expressão é calculada da seguinte forma: 'A = (B == C)'. ut.c 641
Dê uma olhada aqui:
while ((status = NtQuerySystemInformation(....) == STATUS_INFO_LENGTH_MISMATCH))
O programador queria armazenar na variável de status o valor que a função NtQuerySystemInformation retorna e depois compará-lo com uma constante.
O programador provavelmente sabia que o operador de comparação (==) tem uma prioridade mais alta do que o operador de atribuição (=) e, portanto, devem ser usados parênteses. Mas ele se selou e os colocou no lugar errado. Como resultado, os parênteses não ajudam de forma alguma. Código correto:
while ((status = NtQuerySystemInformation(....)) == STATUS_INFO_LENGTH_MISMATCH)
Devido a esse erro, a macro UT_ASSERT nunca funcionará. Na verdade, a variável de status sempre contém o resultado da comparação, ou seja, falso (0) ou verdadeiro (1). Portanto, a condição ([0..1]> = 0) é sempre verdadeira.
Possível vazamento de memória
static enum pocli_ret
pocli_args_obj_root(struct pocli_ctx *ctx, char *in, PMEMoid **oidp)
{
char *input = strdup(in);
if (!input)
return POCLI_ERR_MALLOC;
if (!oidp)
return POCLI_ERR_PARS;
....
}
Aviso PVS-Studio: V773 [CWE-401] A função foi encerrada sem liberar o ponteiro de 'entrada'. Um vazamento de memória é possível. pmemobjcli.c 238
Se oidp for um ponteiro nulo, a cópia da string criada ao chamar a função strdup será perdida . Seria melhor adiar a verificação antes de alocar memória:
static enum pocli_ret
pocli_args_obj_root(struct pocli_ctx *ctx, char *in, PMEMoid **oidp)
{
if (!oidp)
return POCLI_ERR_PARS;
char *input = strdup(in);
if (!input)
return POCLI_ERR_MALLOC;
....
}
Ou você pode liberar a memória explicitamente:
static enum pocli_ret
pocli_args_obj_root(struct pocli_ctx *ctx, char *in, PMEMoid **oidp)
{
char *input = strdup(in);
if (!input)
return POCLI_ERR_MALLOC;
if (!oidp)
{
free(input);
return POCLI_ERR_PARS;
}
....
}
Potencial transbordamento
typedef long long os_off_t;
void
do_memcpy(...., int dest_off, ....., size_t mapped_len, .....)
{
....
LSEEK(fd, (os_off_t)(dest_off + (int)(mapped_len / 2)), SEEK_SET);
....
}
Aviso PVS-Studio: V1028 [CWE-190] Possível estouro. Considere a conversão de operandos, não o resultado. memcpy_common.c 62 Não faz sentido
lançar explicitamente o resultado da adição ao tipo os_off_t . Primeiro, ele não protege contra o estouro potencial que pode ocorrer ao adicionar dois valores int . Em segundo lugar, o resultado da adição teria sido expandido de forma implícita e perfeita para o tipo os_off_t . A conversão de tipo explícito é simplesmente redundante.
Acho que seria mais correto escrever assim:
LSEEK(fd, dest_off + (os_off_t)(mapped_len) / 2, SEEK_SET);
Aqui, um valor sem sinal do tipo size_t é convertido em um valor com sinal (para que não haja nenhum aviso do compilador). E, ao mesmo tempo, definitivamente não haverá estouro durante a adição.
Proteção incorreta contra estouro
static DWORD
get_rel_wait(const struct timespec *abstime)
{
struct __timeb64 t;
_ftime64_s(&t);
time_t now_ms = t.time * 1000 + t.millitm;
time_t ms = (time_t)(abstime->tv_sec * 1000 +
abstime->tv_nsec / 1000000);
DWORD rel_wait = (DWORD)(ms - now_ms);
return rel_wait < 0 ? 0 : rel_wait;
}
Aviso do PVS-Studio: V547 [CWE-570] A expressão 'rel_wait <0' é sempre falsa. O valor do tipo sem sinal nunca é <0. os_thread_windows.c 359
Não estou muito certo sobre qual situação a verificação deve proteger, mas não funciona de qualquer maneira. A variável rel_wait é do tipo DWORD não assinado . Isso significa que a comparação rel_wait <0 não tem sentido, pois o resultado é sempre verdadeiro.
Falta de verificação de que a memória foi alocada com sucesso
A verificação de que a memória está alocada é feita usando macros assert , que não fazem nada se a versão de lançamento do aplicativo for compilada. Portanto, podemos dizer que não há tratamento da situação em que as funções malloc retornam NULL . Exemplo:
static void
remove_extra_node(TOID(struct tree_map_node) *node)
{
....
unsigned char *new_key = (unsigned char *)malloc(new_key_size);
assert(new_key != NULL);
memcpy(new_key, D_RO(tmp)->key, D_RO(tmp)->key_size);
....
}
Aviso PVS-Studio: V575 [CWE-628] O ponteiro nulo potencial é passado para a função 'memcpy'. Inspecione o primeiro argumento. Verifique as linhas: 340, 338. rtree_map.c 340
Em outro lugar, não há nem mesmo uma declaração :
static void
calc_pi_mt(void)
{
....
HANDLE *workers = (HANDLE *) malloc(sizeof(HANDLE) * pending);
for (i = 0; i < pending; ++i) {
workers[i] = CreateThread(NULL, 0, calc_pi,
&tasks[i], 0, NULL);
if (workers[i] == NULL)
break;
}
....
}
Aviso de PVS-Studio: V522 [CWE-690] Pode haver desreferenciamento de um ponteiro nulo em potencial 'trabalhadores'. Verifique as linhas: 126, 124. pi.c 126
Contei pelo menos 37 desses fragmentos de código. Portanto, não vejo razão para listar todos eles no artigo.
À primeira vista, a falta de verificações pode ser considerada apenas desleixo e dizer que este é um código com um cheiro. Eu não concordo com esta posição. Os programadores subestimam o perigo de perder essas verificações. Um ponteiro nulo não se manifesta necessariamente imediatamente como uma falha do programa ao tentar desreferenciá-lo. As consequências podem ser mais bizarras e perigosas, especialmente em programas multithread. Para entender mais detalhadamente qual é o problema e por que as verificações são necessárias, recomendo fortemente que todos leiam o artigo "Por que é importante verificar o que a função malloc retornou ".
Código olfativo
Double Call CloseHandle
static void
prepare_map(struct pmem2_map **map_ptr,
struct pmem2_config *cfg, struct pmem2_source *src)
{
....
HANDLE mh = CreateFileMapping(....);
....
UT_ASSERTne(CloseHandle(mh), 0);
....
}
Aviso PVS-Studio: V586 [CWE-675] A função 'CloseHandle' é chamada duas vezes para desalocação do mesmo recurso. pmem2_map.c 76
Olhando para este código e o aviso PVS-Studio, fica claro que nada está claro. Onde posso chamar CloseHandle novamente ? Para encontrar a resposta, vamos examinar a implementação da macro UT_ASSERTne .
#define UT_ASSERTne(lhs, rhs)\
do {\
/* See comment in UT_ASSERT. */\
if (__builtin_constant_p(lhs) && __builtin_constant_p(rhs))\
UT_ASSERT_COMPILE_ERROR_ON((lhs) != (rhs));\
UT_ASSERTne_rt(lhs, rhs);\
} while (0)
Não ficou muito mais claro. O que é UT_ASSERT_COMPILE_ERROR_ON ? O que é UT_ASSERTne_rt ?
Não vou bagunçar o artigo com uma descrição de cada macro e atormentar o leitor, forçando-o a inserir uma macro em outras em sua cabeça. Vamos ver imediatamente a versão final do código aberto obtido do arquivo pré-processado.
do {
if (0 && 0) (void)((CloseHandle(mh)) != (0));
((void)(((CloseHandle(mh)) != (0)) ||
(ut_fatal(".....", 76, __FUNCTION__, "......: %s (0x%llx) != %s (0x%llx)",
"CloseHandle(mh)", (unsigned long long)(CloseHandle(mh)), "0",
(unsigned long long)(0)), 0))); } while (0);
Vamos remover a condição sempre falsa (0 && 0) e, em geral, tudo irrelevante. Acontece que:
((void)(((CloseHandle(mh)) != (0)) ||
(ut_fatal(...., "assertion failure: %s (0x%llx) != %s (0x%llx)",
....., (unsigned long long)(CloseHandle(mh)), .... ), 0)));
A maçaneta está fechada. Se ocorrer um erro, uma mensagem de depuração é gerada e, para obter o código de erro novamente, CloseHandle é chamado para o mesmo identificador incorreto.
Erros, mais ou menos, e não. Como o identificador é inválido, não há problema em que a função CloseHandle seja chamada duas vezes para ele . No entanto, esse código é inodoro. Seria mais ideologicamente correto chamar a função apenas uma vez e salvar o status que ela retornou, para então, se necessário, exibir seu valor na mensagem.
Interface de implementação incompatível (remoção de const)
static int
status_push(PMEMpoolcheck *ppc, struct check_status *st, uint32_t question)
{
....
} else {
status_msg_info_and_question(st->msg); // <=
st->question = question;
ppc->result = CHECK_RESULT_ASK_QUESTIONS;
st->answer = PMEMPOOL_CHECK_ANSWER_EMPTY;
PMDK_TAILQ_INSERT_TAIL(&ppc->data->questions, st, next);
}
....
}
O analisador exibe a mensagem: V530 [CWE-252] O valor de retorno da função 'status_msg_info_and_question' deve ser utilizado. check_util.c 293
O motivo é que a função status_msg_info_and_question , do ponto de vista do analisador, não altera o estado dos objetos externos a ele, incluindo a string constante passada. Essa. a função apenas calcula algo e retorna o resultado. E se for assim, é estranho não usar o resultado que essa função retorna. E, embora o analisador esteja errado desta vez, ele aponta para um código com um cheiro. Vamos ver como funciona a função chamada status_msg_info_and_question .
static inline int
status_msg_info_and_question(const char *msg)
{
char *sep = strchr(msg, MSG_SEPARATOR);
if (sep) {
*sep = ' ';
return 0;
}
return -1;
}
Ao chamar a função strchr , ocorre uma remoção implícita da constante. O fato é que em C é declarado assim:
char * strchr ( const char *, int );
Não é a melhor solução. Mas a linguagem C é o que é :).
O analisador ficou confuso e não entendeu que a string passada está realmente mudando. Nesse caso, o valor de retorno não é a coisa mais importante e você não pode usá-lo.
No entanto, embora o analisador esteja confuso, ele aponta para um código com um cheiro. O que está confundindo o analisador também pode confundir a pessoa que mantém o código. Seria melhor declarar a função de forma mais honesta removendo const :
static inline int
status_msg_info_and_question(char *msg)
{
char *sep = strchr(msg, MSG_SEPARATOR);
if (sep) {
*sep = ' ';
return 0;
}
return -1;
}
Assim, as intenções são imediatamente mais claras e o analisador ficará em silêncio.
Código muito complicado
static struct memory_block
heap_coalesce(struct palloc_heap *heap,
const struct memory_block *blocks[], int n)
{
struct memory_block ret = MEMORY_BLOCK_NONE;
const struct memory_block *b = NULL;
ret.size_idx = 0;
for (int i = 0; i < n; ++i) {
if (blocks[i] == NULL)
continue;
b = b ? b : blocks[i];
ret.size_idx += blocks[i] ? blocks[i]->size_idx : 0;
}
....
}
Aviso do PVS-Studio: V547 [CWE-571] A expressão 'blocos [i]' é sempre verdadeira. heap.c 1054
If blocks [i] == NULL , então a instrução continue será disparada e o loop iniciará a próxima iteração. Portanto, verificar novamente os blocos de elemento [i] não faz sentido e o operador ternário é redundante. O código pode ser simplificado:
....
for (int i = 0; i < n; ++i) {
if (blocks[i] == NULL)
continue;
b = b ? b : blocks[i];
ret.size_idx += blocks[i]->size_idx;
}
....
Uso suspeito de um ponteiro nulo
void win_mmap_fini(void)
{
....
if (mt->BaseAddress != NULL)
UnmapViewOfFile(mt->BaseAddress);
size_t release_size =
(char *)mt->EndAddress - (char *)mt->BaseAddress;
void *release_addr = (char *)mt->BaseAddress + mt->FileLen;
mmap_unreserve(release_addr, release_size - mt->FileLen);
....
}
Aviso do PVS-Studio: V1004 [CWE-119] O ponteiro '(char *) mt-> BaseAddress' foi usado sem segurança após ter sido verificado em relação a nullptr. Verifique as linhas: 226, 235. win_mmap.c 235
O ponteiro mt-> BaseAddress pode ser nulo, conforme evidenciado pela verificação:
if (mt->BaseAddress != NULL)
No entanto, abaixo deste ponteiro já é usado em operações aritméticas sem verificação. Por exemplo, aqui:
size_t release_size =
(char *)mt->EndAddress - (char *)mt->BaseAddress;
Algum valor inteiro grande será recebido, que é na verdade o valor do ponteiro mt-> EndAddress . Isso pode não ser um bug, mas tudo parece muito suspeito e me parece que o código deve ser verificado novamente. O cheiro está no fato de que o código é incompreensível e claramente carece de comentários explicativos.
Nomes curtos de variáveis globais
Acredito que o código cheira mal se contiver variáveis globais com nomes curtos. É fácil selar e acidentalmente usar não uma variável local, mas uma variável global em alguma função. Exemplo:
static struct critnib *c;
Avisos do PVS-Studio para tais variáveis:
- V707 Dar nomes curtos a variáveis globais é considerado uma prática inadequada. É sugerido renomear a variável 'ri'. map.c 131
- V707 Dar nomes curtos a variáveis globais é considerado uma prática inadequada. É sugerido renomear a variável 'c'. obj_critnib_mt.c 56
- V707 Dar nomes curtos a variáveis globais é considerado uma prática inadequada. Sugere-se renomear a variável 'Id'. obj_list.h 68
- V707 Dar nomes curtos a variáveis globais é considerado uma prática inadequada. Sugere-se renomear a variável 'Id'. obj_list.c 34
Estranho

O código mais estranho que encontrei está na função do_memmove . O analisador deu dois positivos, que indicam erros muito graves ou que simplesmente não entendi o que quero dizer. Como o código é muito estranho, decidi considerar os avisos emitidos em uma seção separada do artigo. Portanto, o primeiro aviso é emitido aqui.
void
do_memmove(char *dst, char *src, const char *file_name,
size_t dest_off, size_t src_off, size_t bytes,
memmove_fn fn, unsigned flags, persist_fn persist)
{
....
/* do the same using regular memmove and verify that buffers match */
memmove(dstshadow + dest_off, dstshadow + dest_off, bytes / 2);
verify_contents(file_name, 0, dstshadow, dst, bytes);
verify_contents(file_name, 1, srcshadow, src, bytes);
....
}
Aviso PVS-Studio: V549 [CWE-688] O primeiro argumento da função 'memmove' é igual ao segundo argumento. memmove_common.c 71
Observe que o primeiro e o segundo argumentos da função são iguais. Assim, a função não faz nada de fato. Que opções vêm à minha mente:
- Eu queria "tocar" o bloco de memória. Mas isso vai acontecer na realidade? O compilador de otimização removerá o código que copia o bloco de memória para si mesmo?
- Este é algum tipo de teste de unidade para a função memmove .
- O código contém um erro de digitação.
E aqui está um snippet igualmente estranho na mesma função:
void
do_memmove(char *dst, char *src, const char *file_name,
size_t dest_off, size_t src_off, size_t bytes,
memmove_fn fn, unsigned flags, persist_fn persist)
{
....
/* do the same using regular memmove and verify that buffers match */
memmove(dstshadow + dest_off, srcshadow + src_off, 0);
verify_contents(file_name, 2, dstshadow, dst, bytes);
verify_contents(file_name, 3, srcshadow, src, bytes);
....
}
Aviso PVS-Studio: V575 [CWE-628] A função 'memmove' processa elementos '0'. Inspecione o terceiro argumento. memmove_common.c 82
A função move 0 bytes. O que é isso? Teste de unidade? Erro de digitação?
Para mim, esse código é incompreensível e estranho.
Por que usar analisadores de código?
Pode parecer que, uma vez que poucos erros foram encontrados, a introdução do analisador no processo de desenvolvimento de código não é razoável. Mas o objetivo de usar ferramentas de análise estática não está em verificações únicas, mas na detecção regular de erros no estágio de escrita do código. Caso contrário, esses erros são detectados de maneiras mais caras e mais lentas (depuração, teste, revisões do usuário e assim por diante). Esta ideia é descrita com mais detalhes no artigo " Erros que a análise estática de código não consegue encontrar, porque não é utilizada ", que recomendo conhecer. E então venha ao nosso site para baixar e experimentar o PVS-Studio para verificar seus projetos.
Obrigado pela atenção!

Se você deseja compartilhar este artigo com um público que fala inglês, por favor, use o link de tradução: Andrey Karpov. Análise de código estático da coleção da biblioteca PMDK pela Intel e erros que não são erros reais .