PVS-Studio inclui suporte para o GNU Arm Embedded Toolchain

Cadeia de ferramentas embarcada GNU Arm + PVS-Studio

Os sistemas embarcados há muito e firmemente entraram em nossas vidas. Os requisitos para sua estabilidade e confiabilidade são muito altos e a correção de erros é cara. Portanto, é especialmente importante que os desenvolvedores incorporados usem regularmente ferramentas especializadas para garantir a qualidade do código-fonte. Este artigo abordará o suporte da GNU Arm Embedded Toolchain no analisador PVS-Studio e os defeitos de código encontrados no projeto Mbed OS.

1. Introdução


O analisador PVS-Studio já suporta vários compiladores comerciais para sistemas embarcados, por exemplo:


Agora, outra ferramenta de desenvolvedor foi adicionada para oferecer suporte - o GNU Embedded Toolchain.

O GNU Embedded Toolchain é uma coleção de compiladores da Arm baseados na GNU Compiler Collection. O primeiro lançamento oficial ocorreu em 2012 e, desde então, o projeto vem sendo desenvolvido em conjunto com o GCC.

O principal objetivo do GNU Embedded Toolchain é gerar código que é executado no bare metal, ou seja, diretamente no processador sem um intercalar na forma de um sistema operacional. O pacote inclui compiladores para C e C ++, assembler, um conjunto de utilitários GNU Binutils e a biblioteca Newlib . O código fonte de todos os componentes é totalmente aberto e licenciado sob a GNU GPL. No site oficial, você pode baixar versões para Windows, Linux e macOS.

Mbed OS


Para testar o analisador, você precisa do código-fonte possível. Geralmente, não há problema com isso, mas quando estamos lidando com desenvolvimento incorporado voltado principalmente para dispositivos incluídos na IoT, pode ser difícil encontrar um número suficiente de projetos grandes. Felizmente, esse problema foi resolvido por sistemas operacionais especializados, cujo código fonte na maioria dos casos está aberto. Além disso, falaremos sobre um deles.

Mbed OS + PVS-Studio


Embora o principal objetivo deste artigo seja falar sobre o suporte ao GNU Embedded Toolchain, é difícil escrever muito sobre isso. Além disso, os leitores de nossos artigos provavelmente estão aguardando uma descrição de alguns erros interessantes. Bem, não vamos enganar as expectativas deles e executar o analisador no projeto Mbed OS. Este é um sistema operacional de código aberto desenvolvido com a assistência da Arm.

Site oficial: https://www.mbed.com/

Código fonte: https://github.com/ARMmbed/mbed-os

A escolha no Mbed OS não caiu por acidente, eis como os autores descrevem o projeto:

O Arm Mbed OS é um sistema operacional incorporado de código aberto projetado especificamente para as "coisas" na Internet das Coisas. Inclui todos os recursos necessários para desenvolver um produto conectado com base em um microcontrolador Arm Cortex-M, incluindo segurança, conectividade, um RTOS e drivers para sensores e dispositivos de E / S.

Este é um projeto de construção ideal usando o GNU Embedded Toolchain, especialmente considerando o envolvimento da Arm em seu desenvolvimento. Farei uma reserva imediatamente que não tinha o objetivo de encontrar e mostrar o maior número possível de erros em um projeto específico, para que os resultados da revisão sejam revisados ​​brevemente.

Erros


Durante a verificação do código do Mbed OS, o analisador PVS-Studio gerou 693 avisos, 86 deles com alta prioridade. Não vou considerá-los todos em detalhes, principalmente porque muitos deles são repetidos ou não têm interesse particular. Por exemplo, o analisador gerou muitos avisos V547 (Expressão sempre verdadeira / falsa) relacionados aos mesmos fragmentos de código. O analisador pode ser configurado para reduzir significativamente o número de respostas falsas e desinteressantes, mas essa tarefa não foi definida ao escrever um artigo. Quem desejar pode ver um exemplo dessa configuração descrito no artigo " Especificações do analisador PVS-Studio usando o exemplo das bibliotecas principais da EFL, 10 a 15% dos falsos positivos ".

Para o artigo, selecionei alguns erros interessantes para demonstrar a operação do analisador.

Vazamentos de memória


Vamos começar com a classe comum de erros em C e C ++ - vazamentos de memória.

Aviso do analisador: V773 CWE-401 A função foi encerrada sem liberar o ponteiro 'read_buf'. É possível um vazamento de memória. cfstore_test.c 565

int32_t cfstore_test_init_1(void) { .... read_buf = (char*) malloc(max_len); if(read_buf == NULL) { CFSTORE_ERRLOG(....); return ret; } .... while(node->key_name != NULL) { .... ret = drv->Create(....); if(ret < ARM_DRIVER_OK){ CFSTORE_ERRLOG(....); return ret; // <= } .... free(read_buf); return ret; } 

A situação clássica ao trabalhar com memória dinâmica. O buffer alocado em malloc é usado apenas dentro da função e liberado antes de sair. O problema é que isso não acontece se a função parar de funcionar mais cedo. Observe o mesmo código nos blocos if . Provavelmente, o autor copiou o fragmento superior e esqueceu de adicionar uma chamada gratuita .

Outro exemplo semelhante ao anterior.

Aviso do analisador: V773 CWE-401 A função foi encerrada sem liberar o ponteiro da 'interface'. É possível um vazamento de memória. nanostackemacinterface.cpp 204

 nsapi_error_t Nanostack::add_ethernet_interface( EMAC &emac, bool default_if, Nanostack::EthernetInterface **interface_out, const uint8_t *mac_addr) { .... Nanostack::EthernetInterface *interface; interface = new (nothrow) Nanostack::EthernetInterface(*single_phy); if (!interface) { return NSAPI_ERROR_NO_MEMORY; } nsapi_error_t err = interface->initialize(); if (err) { return err; // <= } *interface_out = interface; return NSAPI_ERROR_OK; } 

O ponteiro para a memória alocada é retornado através do parâmetro de saída, mas somente se a chamada de inicialização for bem-sucedida e, no caso de um erro, ocorrerá um vazamento porque a variável de interface local fica fora do escopo e o ponteiro é simplesmente perdido. Aqui, deve-se chamar delete ou, pelo menos, fornecer o endereço armazenado na variável da interface para o exterior em qualquer caso, para que o código de chamada possa cuidar disso.

Memset


O uso da função memset geralmente leva a erros; exemplos dos problemas associados a ela podem ser encontrados no artigo " A função mais perigosa do mundo C / C ++ ".

Considere o seguinte aviso do analisador:

V575 CWE-628 A função 'memset' processa elementos '0'. Inspecione o terceiro argumento. mbed_error.c 282

 mbed_error_status_t mbed_clear_all_errors(void) { .... //Clear the error and context capturing buffer memset(&last_error_ctx, sizeof(mbed_error_ctx), 0); //reset error count to 0 error_count = 0; .... } 

O programador pretendia redefinir a memória ocupada pela estrutura last_error_ctx , mas misturou o segundo e o terceiro argumentos. Como resultado, 0 bytes são preenchidos com o valor sizeof (mbed_error_ctx) .

Exatamente o mesmo erro está presente centenas de linhas acima:

V575 CWE-628 A função 'memset' processa elementos '0'. Inspecione o terceiro argumento. mbed_error.c 123

Instrução 'return' incondicional em um loop


Aviso do analisador: V612 CWE-670 Um 'retorno' incondicional dentro de um loop. thread_network_data_storage.c 2348

 bool thread_nd_service_anycast_address_mapping_from_network_data ( thread_network_data_cache_entry_t *networkDataList, uint16_t *rlocAddress, uint8_t S_id) { ns_list_foreach(thread_network_data_service_cache_entry_t, curService, &networkDataList->service_list) { // Go through all services if (curService->S_id != S_id) { continue; } ns_list_foreach(thread_network_data_service_server_entry_t, curServiceServer, &curService->server_list) { *rlocAddress = curServiceServer->router_id; return true; // <= } } return false; } 

Nesse snippet, ns_list_foreach é a macro que se expande na instrução for . O loop interno executa não mais de uma iteração devido à chamada para retornar imediatamente após a linha na qual o parâmetro de saída da função é inicializado. Talvez esse código funcione como pretendido, mas o uso do loop interno parece bastante estranho nesse contexto. Provavelmente, a inicialização e a saída do rlocAddress devem ser executadas por condição, ou você pode se livrar do loop interno.

Erros nas condições


Como eu disse acima, o analisador gerou um número bastante grande de avisos desinteressantes do V547 , então os estudei com fluência e escrevi apenas dois casos para o artigo.

V547 CWE-570 A expressão 'pcb-> state == LISTEN' é sempre falsa. lwip_tcp.c 689

 enum tcp_state { CLOSED = 0, LISTEN = 1, .... }; struct tcp_pcb * tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err) { .... LWIP_ERROR("tcp_listen: pcb already connected", pcb->state == CLOSED, res = ERR_CLSD; goto done); /* already listening? */ if (pcb->state == LISTEN) { // <= lpcb = (struct tcp_pcb_listen*)pcb; res = ERR_ALREADY; goto done; } .... } 

O analisador considera que a condição pcb-> state == LISTEN é sempre falsa, vamos ver o porquê.

Antes da instrução if , é usada a macro LWIP_ERROR , que, de acordo com a lógica de sua operação, se assemelha a assert . O anúncio dele fica assim:

 #define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \ LWIP_PLATFORM_ERROR(message); handler;}} while(0) 

Se a condição for falsa, a macro relata um erro e executa o código passado pelo parâmetro manipulador ; nesse fragmento de código, há um salto incondicional usando goto .

Neste exemplo, a condição 'pcb-> state == CLOSED' está marcada, ou seja, a transição para o rótulo concluído ocorre quando pcb-> state possui outro valor. A instrução if após a chamada para LWIP_ERROR verifica pcb-> state para LISTEN , mas essa condição nunca é atendida, porque o estado nessa linha pode conter apenas o valor CLOSED .

Considere mais um aviso relacionado às condições: V517 CWE-570 O uso do padrão 'if (A) {...} else if (A) {...}' foi detectado. Há uma probabilidade de presença de erro lógico. Verifique as linhas: 62, 65. libdhcpv6_server.c 62

 static void libdhcpv6_address_generate(....) { .... if (entry->linkType == DHCPV6_DUID_HARDWARE_EUI64_TYPE) // <= { memcpy(ptr, entry->linkId, 8); *ptr ^= 2; } else if (entry->linkType == DHCPV6_DUID_HARDWARE_EUI64_TYPE)// <= { *ptr++ = entry->linkId[0] ^ 2; *ptr++ = entry->linkId[1]; .... } } 

Aqui, if e else if, verifique a mesma condição, como resultado, o código no corpo else if nunca é executado. Esses erros geralmente ocorrem ao escrever código usando o método copiar e colar .

Expressão sem dono


Vamos dar uma olhada em um pedaço divertido de código.

Aviso do analisador: V607 Expressão sem dono '& discover_response_tlv'. thread_discovery.c 562

 static int thread_discovery_response_send( thread_discovery_class_t *class, thread_discovery_response_msg_t *msg_buffers) { .... thread_extension_discover_response_tlv_write( &discover_response_tlv, class->version, linkConfiguration->securityPolicy); .... } 

Agora, vamos dar uma olhada na declaração da macro thread_extension_discover_response_tlv_write :

 #define thread_extension_discover_response_tlv_write \ ( data, version, extension_bit)\ (data) 

A macro é expandida para o argumento de dados, ou seja, sua chamada dentro da função thread_discovery_response_send após o pré-processamento se transformar em uma expressão (& discover_response_tlv) .

Espera o que


Não tenho comentários. Provavelmente não é um erro, mas esse código sempre me coloca em um estado semelhante à imagem na figura :).

Conclusão


A lista de compiladores suportados no PVS-Studio foi expandida. Se você tem um projeto destinado à montagem usando o GNU Arm Embedded Toolchain, sugiro tentar testá-lo usando nosso analisador. Faça o download da demo aqui . Preste atenção também à opção de licença gratuita , adequada para alguns projetos pequenos.



Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Yuri Minaev. O PVS-Studio agora oferece suporte à cadeia de ferramentas incorporada GNU Arm .

Source: https://habr.com/ru/post/pt427447/


All Articles