Depuração post-mortem no Cortex-M

Depuração post-mortem no Cortex-M



Antecedentes:


Recentemente participei do desenvolvimento de um dispositivo atípico para mim da classe de eletrônicos de consumo. Parece nada complicado, uma caixa que às vezes deve sair do modo de suspensão, reportar ao servidor e adormecer.


A prática mostrou rapidamente que o depurador não ajuda muito ao trabalhar com um microcontrolador que constantemente entra no modo de suspensão profunda ou corta sua energia. Basicamente, porque a caixa no modo de teste estava sem um depurador e sem mim por perto e às vezes era de buggy. Cerca de uma vez a cada poucos dias.


O UART de depuração foi parafusado nos bicos, nos quais comecei a produzir logs. Tornou-se mais fácil, alguns dos problemas foram resolvidos. Mas então uma afirmação aconteceu e tudo aconteceu.


No meu caso, a macro para a afirmação é algo como isto:
#define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) 

__BKPT(0xAB) é um ponto de interrupção de software; se a afirmação ocorrer durante a depuração, o depurador apenas para na linha do problema, é muito conveniente.


Para algumas afirmações, fica imediatamente claro o que as causou - porque o log mostra o nome do arquivo e o número da linha em que a declaração funcionou.


Mas, de acordo com a afirmação, ficou claro que a matriz estava transbordando - mais precisamente, um invólucro improvisado sobre a matriz, que verifica a saída. Por esse motivo, apenas o nome do arquivo “super_array.h” e o número da linha estavam visíveis no log. E que matriz específica não está clara. Dos logs ao redor, também não está claro.


É claro que alguém poderia simplesmente morder a bala e ler seu código, mas eu estava com preguiça e o artigo não funcionou.


Desde que eu escrevi no uVision Keil 5 com o compilador armcc, outro código foi verificado apenas sob ele. Eu também usei o C ++ 11, porque já é 2019 no quintal, é hora.


Stacktrace


Obviamente, a primeira coisa que vem à mente é que se lembre, porque quando uma declaração ocorre em um computador desktop normal, um rastreamento de pilha é gerado no console, como no KDPV. No rastreamento da pilha, geralmente é possível entender qual sequência de chamadas levou ao erro.
Ok, então eu também preciso de uma faixa furtiva. Como fazer isso?


Talvez se você der uma exceção, ele será deduzido?


Lançamos uma exceção e não a capturamos; vemos a saída de "SIGABRT" e a chamada para _sys_exit . Não é um passeio, bem, tudo bem, não realmente, e eu realmente queria permitir exceções.


Pesquisando como outras pessoas fazem isso.


Todos os métodos são execinfo.h plataforma (não muito surpreendente), para o gcc no POSIX há backtrace() e execinfo.h . Não havia nada inteligível para Cale. Soltamos uma lágrima média. Você tem que subir na pilha com as mãos.


Subimos para as mãos da pilha


Teoricamente, tudo é bem simples.


  1. O endereço de retorno da função atual está no registro LR, o endereço do topo atual da pilha (no sentido do último elemento da pilha) está no registro SP, o endereço do comando atual está no registro PC.
  2. De alguma forma, encontramos o tamanho do quadro da pilha para a função atual, percorremos a pilha a essa distância, localizamos o endereço de retorno da função anterior e a repetimos até avançarmos pela pilha até o fim.
  3. De alguma forma, combinamos os endereços de retorno com os números de linha nos arquivos com o código-fonte.

Ok, para iniciantes - como sei o tamanho do quadro da pilha?


Nas opções por padrão - aparentemente, nada, é simplesmente codificado pelo compilador no "prólogo" e no "epílogo" de cada função, em comandos que alocam e liberam um pedaço da pilha para o quadro.
Mas, felizmente, o armcc tem a opção --use_frame_pointer , que aloca o registro R11 no Frame Pointer - ou seja, ponteiro para o quadro de pilha da função anterior. Ótimo, agora você pode percorrer todos os quadros da pilha.


Agora - como combinar endereços de retorno com seqüências de caracteres nos arquivos de origem?


Porra, de jeito nenhum novamente. As informações de depuração não são exibidas no microcontrolador (o que não é surpreendente, porque ocupa lugares decentes). Cale ainda pode levá-la a piscar lá, não sei, não consegui encontrar.


Nós suspiramos. Portanto, o stackrace honesto - de forma que os nomes das funções e os números de linha sejam imediatamente enviados para a saída de depuração - não funcionará. Mas você pode exibir os endereços e, em seguida, no computador compará-los com funções e números de linha, pois ainda há informações de depuração no projeto.


Mas parece muito triste, porque você precisa analisar o arquivo .map, que indica os intervalos de endereços que cada função ocupa. E, em seguida, analise separadamente os arquivos com código desmontado para encontrar uma linha específica. Há um forte desejo de marcar.


Além disso, olhar atentamente a documentação para a opção --use_frame_pointer permite que --use_frame_pointer veja esta página , que diz que essa opção pode causar falhas no HardFault em momentos aleatórios. Hmm.
Ok, pense mais.


Como o depurador faz isso?


Mas o depurador, de alguma forma, mostra a pilha de chamadas mesmo sem um frame pointer'a . Bem, está claro como, o IDE tem todas as informações de depuração disponíveis, é fácil para ela comparar os endereços e nomes das funções. Hum.


Ao mesmo tempo, o mesmo Visual Studio tem uma coisa - minidump - quando o aplicativo travado gera um arquivo pequeno, que você alimenta o estúdio e restaura o estado do aplicativo no momento do travamento. E você pode considerar todas as variáveis, andar na pilha com conforto. Hum de novo.


Mas é bem simples. Só precisa esfregar uma continuação soviética grossa nas nádegas todos os dias preencha a pilha com os valores que estavam lá no momento do outono e, aparentemente, restaure o estado dos registros. E isso é tudo, parece?


Novamente, divida essa ideia em subtarefas.


  1. No microcontrolador, você precisa percorrer a pilha, para isso, precisa obter o valor atual do SP e o endereço do início da pilha.
  2. No microcontrolador, você precisa exibir os valores dos registradores.
  3. No IDE, você precisa, de alguma forma, enviar todos os valores do "minidump" de volta à pilha. E os valores dos registros também.

Como obter o valor atual de SP?


De preferência, não saqueando as mãos do montador. Em Cale, felizmente, existe uma função especial (intrínseca) - __current_sp() . O Gcc não funcionará, mas não preciso.


Como obter o endereço do início da pilha? Como uso meu script para proteger contra estouro (sobre o qual escrevi aqui ), minha pilha fica em uma seção de vinculador separada, que chamei de REGION_STACK .
Isso significa que seu endereço pode ser encontrado no vinculador usando variáveis ​​estranhas com dólares nos nomes .


Por tentativa e erro, selecionamos o nome desejado - Image$$REGION_STACK$$ZI$$Limit , verifique, ele funciona.


Explicação

Este é um símbolo mágico criado no estágio de vinculação; portanto, estritamente falando, não é uma constante do estágio de compilação.
Para usá-lo, é necessário remover a referência:


 extern unsigned int Image$$REGION_STACK$$ZI$$Limit; using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &( Image$$REGION_STACK$$ZI$$Limit ); 

Se você não quiser incomodar, basta codificar o tamanho da pilha, pois ela muda muito raramente. Na pior das hipóteses, vemos na janela da pilha de chamadas nem todas as chamadas, mas um esboço.


Como exibir os valores do registro?


No começo, pensei que era necessário exibir todos os registros de uso geral em geral. Comecei a me confundir com o assembler, mas rapidamente percebi que não haveria sentido nisso. Afinal, a saída do minidump será feita por uma função especial para mim, não há sentido em registrar valores em seu contexto.


Nós realmente precisamos apenas do Link Register (LR), que armazena o endereço de retorno da função atual, o SP, com o qual já lidamos, e o Program Counter (PC), que armazena o endereço do comando atual.


Mais uma vez, não consegui encontrar uma opção que funcionasse com qualquer compilador, mas há novamente funções intrínsecas para o Cale: __return_address() para LR e __current_pc() para PC.
Ótimo. Resta empurrar todos os valores do minidump de volta para a pilha e os valores dos registradores nos registradores.


Como carregar um minidump na memória?


Inicialmente, planejei usar o comando LOAD debugger, que permite carregar valores de um arquivo .hex ou .bin na memória, mas descobri rapidamente que LOAD, por algum motivo, não carrega valores na RAM.
E eu ainda não seria capaz de concluir os registros com este comando.


Bem, tudo bem, ainda seria necessário muitos gestos, converter texto em bin, converter bin em hexadecimal ...


Felizmente, o Cale possui um simulador e, para o simulador, você pode escrever scripts em uma linguagem miserável do tipo C. E nesta língua há uma oportunidade de escrever na memória! Existem funções especiais como _WDWORD e _WBYTE . Coletamos todas as idéias em uma pilha e obtemos esse código.


Todo o código:
 #define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ print_minidump(); \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$ZI$$Limit; void print_minidump() { //   - armcc  arm-clang #if __CC_ARM || ( (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)) using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &(Image$$REGION_STACK$$ZI$$Limit ); //      , ..      //  SP  stack_upper_address auto LR = __return_address(); auto PC = __current_pc(); auto SP = __current_sp(); auto i = 0; DEBUG_PRINTF("\nCopy the following function for simulator to .ini-file, \n" "start fresh debug session in simulator and call __load_minidump() from command window.\n" "You should be able to see the call stack in CallStack window\n\n"); DEBUG_PRINTF("func void __load_minidump() {\n "); for( MemPointer stack = (MemPointer)SP; stack <= stack_upper_address; stack++ ) { DEBUG_PRINTF("_WDWORD (0x%p, 0x%08x); ", stack, *stack ); //         if( i == 1 ) { DEBUG_PRINTF("\n "); i=0; } else { i++; } } DEBUG_PRINTF("\n LR = 0x%08x;", LR ); DEBUG_PRINTF("\n PC = 0x%08x;", PC ); DEBUG_PRINTF("\n SP = 0x%08x;", SP ); DEBUG_PRINTF("\n}\n"); #endif } 

Para carregar o minidump, precisamos criar um arquivo .ini, copiar a função __load_minidump , adicionar esse arquivo à execução automática - Project -> Options for Target -> Debug e escrever esse arquivo .ini na seção "Arquivo de inicialização" na seção Use Simulator.


Agora entramos na depuração no simulador e, sem iniciar a depuração, chamamos a função __load_minidump() na janela de comando.
E pronto, nós nos teleportamos para a função print_minidump na linha em que o PC foi salvo. E na janela Callstack + Locals, você pode ver a pilha de chamadas.


Nota:

A função é nomeada especificamente com dois sublinhados no início, porque se o nome da função ou variável no script de simulação coincidir acidentalmente com o nome no código do projeto, a Cale não poderá chamá-la. O padrão C ++ proíbe o uso de nomes com dois sublinhados no início, portanto, a probabilidade de nomes correspondentes é reduzida.


Em princípio, é tudo. Tanto quanto pude verificar, o minidump funciona tanto para funções regulares quanto para manipuladores de interrupção. Se funcionará para todos os tipos de perversões com setjmp/longjmp ou alloca - não sei, porque não pratico perversões.


Estou bastante satisfeito com o que aconteceu; pouco código, sobrecarga - macro ligeiramente inchada para afirmar. Nesse caso, todo o trabalho chato de analisar a pilha caiu sobre os ombros do IDE, onde ele pertence.


Pesquisei um pouco no google e encontrei algo semelhante para o gcc e o gdb - CrashCatcher .


Entendo que não inventei nada de novo, mas não consegui encontrar uma receita pronta levando a um resultado semelhante. Ficaria grato se eles me dissessem o que poderia ser feito melhor.

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


All Articles