Qual a qualidade do código-fonte dos fabricantes de chip?
- por Sergio Prado
Não é das melhores, é verdade. E o objetivo deste artigo é mostrar para vocês a importância de se preocupar com isso.
Cada vez mais os fabricantes de chip tem nos poupado um bom tempo de programação oferecendo pilhas completas de software que incluem drivers de dispositivo de hardware, protocolos de comunicação, pilhas gráficas, bibliotecas utilitárias, etc. O grande problema é que eles são fabricantes de chip, e qualidade de software não é, digamos, algo extremamente prioritário para eles.
É claro que não existe um código perfeito ou livre de bugs, mas boa parte das situações de erro em código-fonte podem ser previnidas com uma boa ferramenta de análise estática de código (mais sobre este assunto no artigo “Análise estática de código“).
Resolvi então utilizar a ferramenta cppcheck para analisar o código-fonte fornecido por alguns fabricantes de chip (mais informações sobre o cppcheck no artigo “Analisando código-fonte C/C++ com a ferramenta Cppcheck“).
Para não ser injusto com determinado fabricante de hardware, resolvi testar as pilhas de software de quatro fabricantes diferentes:
-
KSDK versão 1.3 da
FreescaleNXP - MPLAB Harmony Integrated Software Framework versão 1.06.02 da Microchip
- Atmel Software Framework versão 3.29 da Atmel
- STM32CubeF4 versão 1.10.0 da ST
Os testes foram feitos em uma distribuição GNU/Linux. Basicamente baixei a última versão de cada biblioteca e executei o cppcheck no diretório raiz do código-fonte, conforme abaixo:
$ cppcheck . 2>&1 | tee cppcheck.log |
Resultado?
- KSDK: 8 erros críticos.
- MPLAB Harmony: 14 erros críticos.
- ASF: 96 erros críticos.
- STM32CubeF4: 46 erros críticos.
Encontrei exemplos dos principais tipos de erro em código-fonte C/C++, incluindo dereferenciamento de ponteiro nulo, uso de variáveis não inicializadas, indexação fora dos limites de um vetor, vazamento de memória, divisão por zero, etc!
Vamos então a alguns exemplos.
No KSDK, o código de inicialização só funcionará com compiladores GNU. Veja abaixo que a variável n, declarada na linha 3 e acessada na linha 19, só será inicializada na linha 15 se __GNUC__ estiver definido.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void init_data_bss(void) { uint32_t n; [...] /* Get the addresses for the .data section (initialized data section) */ #if defined(__GNUC__) extern uint32_t __DATA_ROM[]; extern uint32_t __DATA_RAM[]; extern char __DATA_END[]; data_ram = (uint8_t *)__DATA_RAM; data_rom = (uint8_t *)__DATA_ROM; data_rom_end = (uint8_t *)__DATA_END; n = data_rom_end - data_rom; #endif /* Copy initialized data from ROM to RAM */ while (n--) { *data_ram++ = *data_rom++; } |
Este é um outro exemplo mais crítico de variável não inicializada no KSDK. O código abaixo cria uma estrutura do tipo sdhc_hal_config_t chamada config na linha 4 e passa o endereço desta estrutura para a função SDHC_HAL_ConfigSdClock() na linha 8, que acessa os membros desta estrutura sem que tenham sido previamente inicializados (linha 20).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void SDHC_HAL_Init(SDHC_Type * base) { sdhc_hal_sdclk_config_t sdClkConf; sdhc_hal_config_t config; sdClkConf.enable = false; SDHC_HAL_ConfigSdClock(base, &sdClkConf); /* Load default configuration */ SDHC_HAL_Config(base, &config); [...] void SDHC_HAL_Config(SDHC_Type * base, const sdhc_hal_config_t* initConfig) { uint32_t proctlReg; assert(base); assert(initConfig); proctlReg = SDHC_RD_PROCTL(base); /* Sets the LED state. */ proctlReg &= (~SDHC_PROCTL_LCTL_MASK); proctlReg |= SDHC_PROCTL_LCTL(initConfig->ledState); |
Que tal este código do MPLAB Harmony, responsável por inserir um elemento em uma fila? O loop for na linha 8 é responsável por encontrar uma posição livre na fila e inicializar o ponteiro queue_element na linha 13 com o endereço de uma posição livre na fila. E se não for encontrada uma posição livre na fila? Neste caso, o ponteiro queue_element não será inicializado, e será dereferenciado nas linhas 21 e 22 com o valor NULL!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int8_t QUEUE_Push(QUEUE_OBJECT *queue, void *data) { int8_t i =0; int8_t elementIndex = 0; QUEUE_ELEMENT_OBJECT *tail = NULL; QUEUE_ELEMENT_OBJECT *queue_element = NULL; for(i = 0 ; i < queue->size ; i++) { if(!queue->qElementPool[i].inUse) { queue->qElementPool[i].inUse = true; queue_element = &queue->qElementPool[i]; elementIndex = i; break; } } //SYS_ASSERT((queue_element!= (QUEUE_ELEMENT_OBJECT *)NULL), "Unable to create queue elements "); queue_element->next = NULL; queue_element->data = data; |
Agora um erro clássico de vazamento de memória no MPLAB Harmony. Um bloco de memória é alocado com a função malloc() na linha 3, e mais abaixo na linha 23, em um condição específica, a função retorna erro sem desalocar a memória previamente alocada!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[...] hSysMsg = (SYS_MSG_MESSAGING_OBJECT*)malloc(sizeof(SYS_MSG_MESSAGING_OBJECT)); if ( NULL == hSysMsg ) { return SYS_OBJ_HANDLE_INVALID; } memset(hSysMsg,0x00,sizeof(SYS_MSG_MESSAGING_OBJECT)); if ( NULL != pInitSysMsg ) { // Pointer to init structure provided, use init structure values. // Initialize Sytem Message Object hSysMsg->nMaxMsgsDelivered = pInitSysMsg->nMaxMsgsDelivered; hSysMsg->nMessagePriorities = pInitSysMsg->nMessagePriorities; // Initialize individual message queues, one for each priority for (iPriority=0;iPrioritynMessagePriorities;iPriority++) { initResult = SYS_MSGQ_Init(hSysMsg->msgQueues + iPriority,pInitSysMsg->nQSizes[iPriority]); if (SYS_MSGQ_Success !=initResult ) { return SYS_OBJ_HANDLE_INVALID; } } } |
E que tal um erro de divisão por zero? No ASF da Atmel, a função abaixo irá realizar uma divisão por zero caso não entre na condição da linha 8!
1 2 3 4 5 6 7 8 9 10 11 12 |
unsigned long abdac_get_dac_hz(volatile avr32_abdac_t *abdac, const unsigned long bus_hz) { volatile avr32_pm_t *pm = &AVR32_PM; unsigned short div = 0; if (pm->gcctrl[ABDAC_GCLK] & GCLK_BIT(DIVEN)) { div = 2 * (GCLK_BFEXT(DIV, pm->gcctrl[ABDAC_GCLK]) + 1); } return (bus_hz / div); } |
Acessos fora dos limites de um vetor costumam ser um dos problemas mais críticos em software. É o que pode acontecer no código abaixo, também no ASF da Atmel. Primeiro o código passa por um loop na linha 3 que percorre o índice idx de 0 a 4. Se não entrar na condição da linha 4, sairá do loop com idx == 5. Na linha 20 mais abaixo, como idx == 5, será indexado o sexto elemento do vetor ble_dev_info, que possui apenas 5 elementos!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[...] for (idx = 0; idx < BLE_MAX_DEVICE_CONNECTED; idx++) { if((ble_dev_info[idx].conn_info.handle == slave_sec_req->handle) && (ble_dev_info[idx].conn_state == BLE_DEVICE_CONNECTED)) { ble_dev_info[idx].conn_state = BLE_DEVICE_PAIRING; break; } } features.desired_auth = BLE_AUTHENTICATION_LEVEL; features.bond = slave_sec_req->bond; features.mitm_protection = slave_sec_req->mitm_protection; /* Device capabilities is display only , key will be generated and displayed */ features.io_cababilities = AT_BLE_IO_CAP_KB_DISPLAY; features.oob_avaiable = false; /* Distribution of LTK is required */ if (ble_dev_info[idx].conn_info.peer_addr.type == AT_BLE_ADDRESS_RANDOM_PRIVATE_RESOLVABLE) { features.initiator_keys = (at_ble_key_dis_t)(AT_BLE_KEY_DIST_ENC | AT_BLE_KEY_DIST_ID); features.responder_keys = (at_ble_key_dis_t)(AT_BLE_KEY_DIST_ENC | AT_BLE_KEY_DIST_ID); |
Que tal retornar lixo do stack? É isso que faz esta função dentro do STM32CubeF4 (dá uma olhada na variável ret).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int mpi_copy( mpi *X, const mpi *Y ) { int ret; size_t i; if( X == Y ) return( 0 ); for( i = Y->n - 1; i > 0; i-- ) if( Y->p[i] != 0 ) break; i++; X->s = Y->s; MPI_CHK( mpi_grow( X, i ) ); memset( X->p, 0, X->n * ciL ); memcpy( X->p, Y->p, i * ciL ); cleanup: return( ret ); } |
Mais um acesso fora dos limites de um vetor, desta vez no STM32CubeF4 usando uma função de formatação de string (veja o tamanho da variável str na linha 6 e a chamada à função sprintf() na linha 20).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
AUDIO_ErrorTypeDef AUDIO_PLAYER_Process(void) { uint32_t bytesread, elapsed_time; AUDIO_ErrorTypeDef audio_error = AUDIO_ERROR_NONE; static uint32_t prev_elapsed_time = 0xFFFFFFFF; uint8_t str[10]; switch(AudioState) { [...] case AUDIO_STATE_VOLUME_UP: if( uwVolume <= 90) { uwVolume += 10; } BSP_AUDIO_OUT_SetVolume(uwVolume); BSP_LCD_SetTextColor(LCD_COLOR_WHITE); sprintf((char *)str, "Volume : %lu ", uwVolume); BSP_LCD_DisplayStringAtLine(9, str); AudioState = AUDIO_STATE_PLAY; break; |
SOLUÇÃO?
Bom, primeiramente espero ter convencido você a não confiar cegamente no código-fonte fornecido por terceiros. Mas o que devemos fazer então? Parar de usar? Não necessariamente.
Mas devemos ser muito mais críticos com relação ao código-fonte que utilizamos nos produtos que desenvolvemos. E isso vale para qualquer código-fonte, seja de um fabricante de hardware, adquirido de uma empresa terceira ou desenvolvido pela comunidade.
Isso significa olhar e analisar o código-fonte com cuidado. E significa também utilizar ferramentas que possam ajudar no processo.
Portanto, invista alguns minutos para executar uma ferramenta de análise estática de código nos seus projetos. Melhor ainda, integre a ferramenta no sistema de build da sua aplicação, de forma que o código não compile se a ferramenta encontrar algum erro. Isso irá te poupar horas e horas de depuração e muitas dores de cabeça principalmente quando o problema acontecer em campo.
Aqui vale aquele velho ditado atribuído a Abraham Lincoln: “Se eu tivesse 8 horas para cortar uma árvore, gastaria seis afiando meu machado“.
Happy coding!
Sergio Prado