Olá caro leitor, saindo um pouco da minha tradicional linha de artigos voltados a processamento de sinais, neste pequeno texto vou ilustrar a vocês um problema que ocorre muito quando estamos em processo de desenvolvimento de firmware, as exceções! Aquele famoso loop infinito que ocorre quando algo deu errado durante um acesso de memória, instrução ou operação matemática indefinida. Tenho visto nas minhas andanças com cada vez mais frequência dúvidas de colegas de como proceder quando o microcontrolador apronta uma dessas, uma vez que o código do vetor de exceção quase nunca está implementado. Vamos ilustrar nosso caso especifico assumindo que o processador alvo seja um popular ARM Cortex-M3/M4.
Hardfault ou exceção de hardware
Bem comum em microcontroladores ARM Cortex, o hardfault é aquela exceção não mascarável (não pode ser desabilitada) que vai ocorrer quando o processador realizar ações como:
- Acesso desalinhado de memória;
- Acesso à instrução não definida ou ilegal;
- Execução de instrução ARM em modo Thumb (ou vice-versa);
- Operação matemática ilegal (divisão por 0).
A grande verdade é que pra cada um desses eventos existem vetores de exceção próprios, porém mascaráveis, ou seja, podem ser desligados caso aquele programador mais despreocupado não queira uma exceção “enchendo o saco”. O hardfault propriamente dito só vai ocorrer quando o processador verificar no NVIC que uma das demais exceções ocorreu e ao mesmo tempo que essas estão mascaradas para somente então desviar para o vetor de hardfault, que contém aquele triste loop infinito, e em várias situações deixando o desenvolvedor frustrado.
Como conseguir pistas de “crash” do Firmware pelo Hardfault?
Agora que já sabemos da existência da exceção e como funciona nos processadores derivados do ARMv7M (ARM Cortex), fica a questão de como iniciar a busca por um eventual bug que esteja causando o disparo da exceção (ao leitor mais curioso, o modelo de exceção apresentado é muito similar a outras arquiteturas de 16/32bits). O primeiro ponto é escrever um código de tratamento dentro do vetor de hardfault, de modo que em modo debug o processador faça algumas ações e pare a execução (sim, como se fosse um breakpoint) para permitir que o programador comece a buscar o problema.
O primeiro passo para escrever um bom código de notificação de exceção consiste primeiro em entender o Stack Frame gerado do processador cada vez que uma interrupção ou exceção é gerada. O Stack Frame é um conjunto de registradores que imediatamente após a ocorrência de uma exceção é acessado e salvo na pilha corrente (estando apontado pelo registrador MSP em sistemas bare-metal ou PSP em sistemas com uso de sistema operacional). No caso dos ARM Cortex-M, o Stack Frame gerado pode ser verificado na figura abaixo:
Os registradores em questão são:
- xPSR : Registrador de status e controle da aplicação;
- PC : Contador de programa, aponta para o endereço de ocorrência da interrupção + 1;
- LR : Chamado de link register, utilizado também para salvamento de endereço de retorno;
- R12~R0: Registradores de uso geral, frequentemente usados como área de trabalho de um compilador C.
Basicamente, temos acima um snapshot completo do contexto corrente da nossa aplicação. Agora imagine, caro leitor, que seu programa escrito com todo esmero inesperadamente trava e cai dentro do vetor de Hardfault, concorda que podemos acessar o último stack frame gerado e rastrear de que lugar a falha veio?
Para isso, vamos pegar alguns trechos do tradicional startup.S. Esse arquivo é na verdade um “early code” que prepara toda a aplicação C para rodar (basicamente inicializa todas as memórias do microcontrolador e chama a função main). Vejam como ele é originalmente:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
/****************************************************************************** * * The minimal vector table for a Cortex M3. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * *******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word DebugMon_Handler .word 0 .word PendSV_Handler .word SysTick_Handler /* External Interrupts */ .word WWDG_IRQHandler /* Window WatchDog */ .word PVD_IRQHandler /* PVD through EXTI Line detection */ .word TAMP_STAMP_IRQHandler /* Tamper and TimeStamps through the EXTI line */ .word RTC_WKUP_IRQHandler /* RTC Wakeup through the EXTI line */ .word FLASH_IRQHandler /* FLASH */ .word RCC_IRQHandler /* RCC */ .word EXTI0_IRQHandler /* EXTI Line0 */ .word EXTI1_IRQHandler /* EXTI Line1 */ .word EXTI2_IRQHandler /* EXTI Line2 */ .word EXTI3_IRQHandler /* EXTI Line3 */ .word EXTI4_IRQHandler /* EXTI Line4 */ .word DMA1_Stream0_IRQHandler /* DMA1 Stream 0 */ .word DMA1_Stream1_IRQHandler /* DMA1 Stream 1 */ .word DMA1_Stream2_IRQHandler /* DMA1 Stream 2 */ .word DMA1_Stream3_IRQHandler /* DMA1 Stream 3 */ .word DMA1_Stream4_IRQHandler /* DMA1 Stream 4 */ .word DMA1_Stream5_IRQHandler /* DMA1 Stream 5 */ .word DMA1_Stream6_IRQHandler /* DMA1 Stream 6 */ .word ADC_IRQHandler /* ADC1, ADC2 and ADC3s */ .word CAN1_TX_IRQHandler /* CAN1 TX */ .word CAN1_RX0_IRQHandler /* CAN1 RX0 */ .word CAN1_RX1_IRQHandler /* CAN1 RX1 */ .word CAN1_SCE_IRQHandler /* CAN1 SCE */ .word EXTI9_5_IRQHandler /* External Line[9:5]s */ .word TIM1_BRK_TIM9_IRQHandler /* TIM1 Break and TIM9 */ .word TIM1_UP_TIM10_IRQHandler /* TIM1 Update and TIM10 */ .word TIM1_TRG_COM_TIM11_IRQHandler /* TIM1 Trigger and Commutation and TIM11 */ .word TIM1_CC_IRQHandler /* TIM1 Capture Compare */ .word TIM2_IRQHandler /* TIM2 */ .word TIM3_IRQHandler /* TIM3 */ .word TIM4_IRQHandler /* TIM4 */ .word I2C1_EV_IRQHandler /* I2C1 Event */ .word I2C1_ER_IRQHandler /* I2C1 Error */ .word I2C2_EV_IRQHandler /* I2C2 Event */ .word I2C2_ER_IRQHandler /* I2C2 Error */ .word SPI1_IRQHandler /* SPI1 */ .word SPI2_IRQHandler /* SPI2 */ .word USART1_IRQHandler /* USART1 */ .word USART2_IRQHandler /* USART2 */ .word USART3_IRQHandler /* USART3 */ .word EXTI15_10_IRQHandler /* External Line[15:10]s */ .word RTC_Alarm_IRQHandler /* RTC Alarm (A and B) through EXTI Line */ .word OTG_FS_WKUP_IRQHandler /* USB OTG FS Wakeup through EXTI line */ .word TIM8_BRK_TIM12_IRQHandler /* TIM8 Break and TIM12 */ .word TIM8_UP_TIM13_IRQHandler /* TIM8 Update and TIM13 */ .word TIM8_TRG_COM_TIM14_IRQHandler /* TIM8 Trigger and Commutation and TIM14 */ .word TIM8_CC_IRQHandler /* TIM8 Capture Compare */ .word DMA1_Stream7_IRQHandler /* DMA1 Stream7 */ .word FSMC_IRQHandler /* FSMC */ .word SDIO_IRQHandler /* SDIO */ .word TIM5_IRQHandler /* TIM5 */ .word SPI3_IRQHandler /* SPI3 */ .word UART4_IRQHandler /* UART4 */ .word UART5_IRQHandler /* UART5 */ .word TIM6_DAC_IRQHandler /* TIM6 and DAC1&2 underrun errors */ .word TIM7_IRQHandler /* TIM7 */ .word DMA2_Stream0_IRQHandler /* DMA2 Stream 0 */ .word DMA2_Stream1_IRQHandler /* DMA2 Stream 1 */ .word DMA2_Stream2_IRQHandler /* DMA2 Stream 2 */ .word DMA2_Stream3_IRQHandler /* DMA2 Stream 3 */ .word DMA2_Stream4_IRQHandler /* DMA2 Stream 4 */ .word ETH_IRQHandler /* Ethernet */ .word ETH_WKUP_IRQHandler /* Ethernet Wakeup through EXTI line */ .word CAN2_TX_IRQHandler /* CAN2 TX */ .word CAN2_RX0_IRQHandler /* CAN2 RX0 */ .word CAN2_RX1_IRQHandler /* CAN2 RX1 */ .word CAN2_SCE_IRQHandler /* CAN2 SCE */ .word OTG_FS_IRQHandler /* USB OTG FS */ .word DMA2_Stream5_IRQHandler /* DMA2 Stream 5 */ .word DMA2_Stream6_IRQHandler /* DMA2 Stream 6 */ .word DMA2_Stream7_IRQHandler /* DMA2 Stream 7 */ .word USART6_IRQHandler /* USART6 */ .word I2C3_EV_IRQHandler /* I2C3 event */ .word I2C3_ER_IRQHandler /* I2C3 error */ .word OTG_HS_EP1_OUT_IRQHandler /* USB OTG HS End Point 1 Out */ .word OTG_HS_EP1_IN_IRQHandler /* USB OTG HS End Point 1 In */ .word OTG_HS_WKUP_IRQHandler /* USB OTG HS Wakeup through EXTI */ .word OTG_HS_IRQHandler /* USB OTG HS */ .word DCMI_IRQHandler /* DCMI */ .word CRYP_IRQHandler /* CRYP crypto */ .word HASH_RNG_IRQHandler /* Hash and Rng */ .word F2DUMMY /* Era FPU no F4 */ |
Acima temos o trecho que implementa a estrutura da tabela de vetores dos microcontroladores Cortex, reparem que essa possui as primeiras 16 entradas separadas das demais. São essas as entradas das exceções donde tem-se a quarta entrada, o vetor que nos interessa, o de hardfault. Sabendo agora onde ele fica, vamos implementar uma função que consiga extrair o último stack frame gerado antes do firmware entrar nesse ponto, vejam como está:
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * @param None * @retval None */ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop .size Default_Handler, .-Default_Handler |
Como o foco aqui é simplificar, iremos implementar nosso código de debug dentro do próprio Default_Handler, vejam como fica:
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 |
/** * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * @param None * @retval None */ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: cpsid ;shutdown interrupts movs r0, 0x02 ; msr control, r0 ;enters in privilegied mode isb ; dsb ;flushes pipeline, ensring the next instruction ;will be executed in thread mode ; mrs r0, psp ;uses msp for bare-metal systems ldmia r0!,{r1 - r7} ;pops the last stack frame on our ;register workspace. bpkt #0 ;halts the execution (proceed the analysis) ; b . ;traps the code here .size Default_Handler, .-Default_Handler |
Eu sei, eu sei, Assembly, apesar do susto inicial veja que o código de startup é geralmente implementado em baixo nível tornando muito mais fácil embarcar código extra nesse módulo do que criar um arquivo separado e usar Assembly inline. Veja também que tal solução pode ser implementada em C com o auxílio das funções implementadas na CMSIS. Sobre o nosso módulo de rastreio de “crash”, iniciamos, ao entrar no vetor de exceção, o desligamento de toda e qualquer interrupção futura com o cpsid e inibimos qualquer tipo de preempção (até mesmo de interrupções de altíssima prioridade). Em seguida garantimos que o processador fique no chamado thread mode, nesse modo o acesso a qualquer registrador e memória é permitido, para isso modificamos o registrador especial control. Com o uso das instruções isb e dsb fazemos o que se chama de esvaziamento do pipeline do microcontrolador, isso é necessário pois garantimos que a próxima instrução a ser executada seja em thread mode, evitando por exemplo que o acesso ao ponteiro de pilha principal MSP, seja inibido pelo hardware, e falando em pilha, esse é o passo seguinte. Com o uso da instrução de carga multipla, acessamos o stack frame composto de 7 registradores e os colocamos na área de trabalho. Agora eis que o leitor me pergunta: Por que na área de trabalho e não em uma estrutura com acesso pelo watch de variáveis? Simples, o acesso aos registradores do microcontrolador é o mínimo invasivo possível, não existe nenhum pós processamento que poderia mascarar os dados lidos, além disso, no work podemos acessar de todo tipo de ferramenta de debug, de um completíssimo ARM DS Studio a um terminal conectado via OpenOCD. Ao final a instrução bkpt é chamada fazendo com que o programa pare sem qualquer necessidade de ação do desenvolvedor, ou seja, toda a ação é transparente e, se uma falha for gerada, o programa automaticamente tomará todas as providências necessárias para análise e através do evento de parada irá notificar o usuário.
Tenho os registradores, e agora?
Uma vez de posse do stack frame, precisamos saber o que fazer, é nesse ponto que deixo a cargo de cada desenvolvedor tomar as suas providências de análise, porém alguns procedimentos que podem ajudar e muito:
- Sempre cheque o contador de programa (PC), ele incialmente contém o endereço do exato local onde a exceção foi chamada;
- Se no endereço apontado pelo PC você encontrar words de 0x00000000 ou 0xFFFFFFFF, desconfie, esses são tratados por undefined instruction pelo ARM;
- Seguindo o raciocínio do tópico anterior, caso essa suspeita se confirme, cheque também o conteúdo do link register LR, e subtraia 1 do valor. O endereço resultante é o ponto de chamada de uma subrotina que contém entre suas instruções a que gerou a falha;
- Um hardfault muito comum é o associado ao modo de operação do microcontrolador, assim no lugar do LR extraído da pilha, checar o LR que já está no work é muito útil, pois algo que pode ter acontecido é o microcontrolador Cortex, que só executa instruções thumb, ter feito algum acesso em modo ARM. Assim, cheque o modo de operação através desses códigos disponibilizados no guia de usuário da ARM.
Um exemplo prático, a movimentação de memória defeituosa
Vamos a um exemplo bem comum de hardfault, overflow de memória, considere o seguinte trecho de código:
1 2 3 4 5 6 7 8 |
#define SIMPLE_ARRAY_SIZE 512 int simpleArray[SIMPLE_ARRAY_SIZE]; int complexArray[SIMPLE_ARRAY_SIZE]; memcpy(&simpleArray, &complexArray, (SIMPLE_ARRAY_SIZE + 1)); |
Se executarmos esse código será quase certeza (salvo alguma providência tomada na linkagem) que veremos o código travado lá naquele breakpoint. Vamos seguir os passos básicos, vamos checar o PC, é provável que encontremos algo como: 0x2AC00000, 0x0, ou 0xFFFFFFFF, o que não diz muita coisa. Então vamos ao LR. Se pegarmos seu valor, subtrairmos 1, e jogar no disassembly, estaremos provavelmente no ponto onde o memcpy() foi chamado. Ainda com o disassembly aberto, poderemos observar as instruções até chegar em algo parecido com isso:
1 2 |
ldr r0, [r1], #4 str r0, [r2], #4 |
Um processo de load e store, e aqui entra a personalidade de quem esta com a tarefa de busca do bug. Alguns logo de cara ja irão desconfiar do overflow, esse que vos escrever, vai olhar provavelmente no conteúdo dos registradores r0~r12 onde aparecerá alguma operação suspeita, porém relacionada ao ciclo de load e store, concluindo que está havendo estouro de um ou dois dos vetores. Mas vejam que o bug propriamente vem desse ponto, mas a causa é outra, levando-nos a procurar na chamada do memcpy() por erros nos argumentos, corrigindo assim o erro.
Conclusão
Debug é uma tarefa que pode consumir minutos ou dias, porém um fator bem determinante do tempo gasto na procura por um problema de firmware está na forma que o desenvolvedor aproveita as ferramentas que seu ambiente de desenvolvimento oferece, seja linha de comando ou uma IDE completa. Assim, o uso de um código de trace no vetor de hardfault se torna útil pois atende aos dois extremos do desenvolvedor, reduzindo o tempo de busca de problemas de firmware fornecendo no mínimo uma rota incial de onde buscar a causa de falhas.
Referências
Manual de usuário dos processadores ARM Cortex
YIU, Joseph – The Definitive Guide for ARM Cortex M3 and M4 Processors
Muito bom o material. HardFault é um terror, porém pode ser muito útil quando acontece no momento do desenvolvimento…
Parabéns pelo material, o artigo é muito bom.