
Olá, meu nome é Eugene e estou cansado de escrever firmware para microcontroladores. Como isso aconteceu e o que fazer com isso, vamos descobrir.
Depois de trabalhar em grande programação C ++, Java, Python, etc., você não sente vontade de voltar para microcontroladores pequenos e com barriguinha. Para suas escassas ferramentas e bibliotecas. Mas às vezes não há nada a fazer, as tarefas de tempo real e autonomia não deixam uma escolha. Mas existem alguns tipos de tarefas que simplesmente ficam loucas nessa área para resolver.
Por exemplo, equipamentos de teste, algo mais chato e chato em programação incorporada, dificilmente podem ser imaginados. Em geral, bem como ferramentas convenientes para isso. Você escreve ... Você pisca ... pisca ... um LED (às vezes faz logon no UART). Todas as canetas, sem ferramentas de teste especializadas.
Também é deprimente que não haja testes instrumentais para nossos pequenos microcontroladores. Tudo é apenas através do firmware e através do depurador para testar.
E o estudo do trabalho com novos dispositivos e periféricos requer muito esforço e tempo. Um erro e o programa devem ser recompilados e executados novamente a cada vez.
Para tais experiências, algo como REPL é mais adequado, para que você possa fazer de maneira simples e indolor, pelo menos trivial:
\
Como chegar a isso, esta série de artigos é dedicada.
E desta vez me deparei com um projeto em que era necessário testar um dispositivo bastante complicado, com muitos tipos de sensores e outros chips desconhecidos para mim anteriormente, que usavam muitos periféricos MK e várias interfaces diferentes. A diversão especial foi que eu não tinha os códigos-fonte do firmware para a placa, então todos os testes precisavam ser escritos do zero, sem usar o tempo de operação do código-fonte.
O projeto prometeu um bom toastmaster e as competições são interessantes por mais ou menos dois meses (e provavelmente mais).
Ok, aqui não vamos chorar. É preciso mergulhar nos confins do C e no firmware sem fim novamente, ou recusar ou criar algo para facilitar esta lição. No final, preguiça e curiosidade são o motor do progresso.
Na última vez, quando entendi o OpenOCD, deparei com um ponto tão interessante na documentação como
http://openocd.org/doc/html/General-Commands.html 15.4 Memory access commands mdw, mdh, mdb — mww, mwh, mwb —
Interessante ... E é possível ler e gravar registros periféricos com eles? ... acaba sendo possível e, além disso, esses comandos podem ser executados remotamente através do servidor TCL, que inicia quando o openOCD é iniciado.
Aqui está um exemplo de um LED piscando para stm32f103C8T6
e uma sequência semelhante de comandos openOCD
mww 0x40021018 0x10 mww 0x40011004 0x300000 mww 0x40011010 0x2000 mww 0x40011010 0x20000000
E agora, se você pensar no eterno e considerar o firmware para o MK ... então o principal objetivo desses programas é escrever nos registradores de chips; O firmware que fará apenas algo e funcionará apenas com o núcleo do processador não tem uso prático!
NotaEmbora, é claro, você possa considerar a cripta (=
Muitos vão se lembrar mais sobre como trabalhar com interrupções. Mas eles nem sempre são necessários e, no meu caso, você pode ficar sem eles.
E assim, a vida está melhorando. No código-fonte do openOCD você pode até encontrar um exemplo interessante de usar essa interface.
Muito bom espaço em branco em python.
É bem possível converter endereços de registro de arquivos de cabeçalho e começar a escrever em uma linguagem de script kosher. Você já pode preparar champanhe, mas me pareceu insuficiente, porque quero usar a Biblioteca de Periféricos Padrão ou o novo HAL para trabalhar com periféricos em vez de mexer nos registros.
Portando bibliotecas para python ... em algum pesadelo, faremos isso. Então você precisa usar essas bibliotecas em C ou ... C ++. E nos profissionais, você pode substituir quase todos os operadores ... por suas classes.
E os endereços base nos arquivos de cabeçalho, substitua pelos objetos de suas classes.
Por exemplo, no arquivo stm32f10x.h
#define PERIPH_BB_BASE ((uint32_t)0x42000000)
Substitua por
class InterceptAddr; InterceptAddr addr; #define PERIPH_BB_BASE (addr)
Mas jogos com ponteiros na biblioteca cortam essa idéia pela raiz ...
Aqui está um exemplo de arquivo stm32f10x_i2c.c:
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG) { __IO uint32_t i2creg = 0, i2cxbase = 0; …. i2cxbase = (uint32_t)I2Cx; ….
Portanto, é necessário interceptar endereços para endereços de alguma maneira diferente. Como fazer isso provavelmente vale a pena dar uma olhada em Valgrind, não à toa que ele tenha um verificador de memórias. Bem, ele realmente deve saber como interceptar endereços.
Olhando para o futuro, direi que é melhor não olhar para lá ... quase consegui interceptar chamadas para endereços. Para quase todos os casos, exceto este
Int * p = ... *p = 0x123;
É possível interceptar o endereço, mas não foi mais possível interceptar os dados gravados. Somente o nome do registro interno no qual esse valor está, mas que não pode ser acessado na verificação de memórias.
De fato, Valgrind me surpreendeu, dentro do antigo monstro libVEX é usado, sobre o qual eu não encontrei nenhuma informação na Internet. É bom que tenha sido encontrada alguma documentação nos arquivos de cabeçalho.
Depois, havia outras ferramentas DBI.
Frida, Dynamic RIO, um pouco mais e finalmente conseguiu o Pintool.
O PinTool tinha uma boa documentação e exemplos. Embora eu ainda não tivesse o suficiente, tive que fazer experimentos com algumas coisas. A ferramenta acabou sendo muito poderosa, apenas altera o código fechado e a restrição apenas para a plataforma intel (embora no futuro isso possa ser contornado)
Portanto, precisamos interceptar a gravação e a leitura em endereços específicos. Vamos ver quais instruções são responsáveis por este https://godbolt.org/z/nJS9ci .
Para x64, este será um MOV para ambas as operações.
E para x86, será MOV para escrita e MOVZ para leitura.
Nota: é melhor não ativar a otimização; caso contrário, outras instruções podem surgir.
Título de spoiler INS_AddInstrumentFunction(EmulateLoad, 0); INS_AddInstrumentFunction(EmulateStore, 0); ..... static VOID EmulateLoad(INS ins, VOID *v) {
No caso de leitura do endereço, chamamos a função loadAddr2Reg e excluímos a instrução original. Com base nisso, loadAddr2Reg deve retornar o valor necessário para nós.
Com um registro, é cada vez mais difícil ... os argumentos podem ser de tipos diferentes e também podem ser transmitidos de maneiras diferentes; portanto, é necessário chamar funções diferentes antes do comando. Em uma plataforma de 32 bits, multiMemAccessStore e em 64 storeReg2Addr serão chamados. E aqui não excluímos as instruções da linha de montagem. Não há problemas para removê-lo, mas em alguns casos não é possível imitar sua ação. O programa, por algum motivo, às vezes colide com sigfault. Isso não é crítico para nós, escreva para si mesmo, o principal é que existe a possibilidade de interceptar argumentos.
Em seguida, precisamos ver quais endereços precisamos interceptar; veja o mapa de memória do chip stm32f103C8T6:

Estamos interessados em endereços com SRAM e PERIPH_BASE, ou seja, de 0x20000000 a 0x20000000 + 128 * 1024 e de 0x40000000 a 0x40030000. Bem, ou melhor, não exatamente, como lembramos as instruções de gravação, não conseguimos excluir. Portanto, o registro nesses endereços cairá em sigfault. Além disso, é improvável que esses endereços tenham dados de nosso programa, e não esse chip. Portanto, definitivamente precisamos corrigi-los em algum lugar. Digamos em algum tipo de matriz.
Criamos matrizes do tamanho necessário e, em seguida, substituímos seus ponteiros no endereço base definido.
Em nosso programa, nas manchetes
#define SRAM_BASE ((uint32_t)0x20000000) #define PERIPH_BASE ((uint32_t)0x40000000)
Fazer
#define SRAM_BASE ((AddrType)pAddrSRAM) #define PERIPH_BASE ((AddrType)pAddrPERIPH)
e onde pAddrSRAM e pAddrPERIPH são ponteiros para matrizes pré-alocadas.
Agora, nosso cliente PinTool precisa, de alguma forma, transmitir como reparamos os endereços necessários.
A coisa mais simples que me pareceu fazer isso foi interceptar uma função que retorna uma estrutura de matriz deste formato:
typedef struct { addr_t start_addr;
Por exemplo, para o nosso chip, ele estará tão cheio
map->start_addr = (addr_t)pAddrSRAM; map->end_addr = 96*1024; map->reference_addr = (addr_t)0x20000000U;
Não é difícil interceptar a função e obter os valores necessários:
IMG_AddInstrumentFunction(ImageReplace, 0); .... static memoryTranslate *replaceMemoryMapFun(CONTEXT *context, AFUNPTR orgFuncptr, sizeMemoryTranslate_t *size) { PIN_CallApplicationFunction(context, PIN_ThreadId(), CALLINGSTD_DEFAULT, orgFuncptr, NULL, PIN_PARG(memoryTranslate *), &addrMap, PIN_PARG(sizeMemoryTranslate_t *), size, PIN_PARG_END()); sizeMap = *size; return addrMap; } static VOID ImageReplace(IMG img, VOID *v) { RTN freeRtn = RTN_FindByName(img, NAME_MEMORY_MAP_FUNCTION); if (RTN_Valid(freeRtn)) { PROTO proto_free = PROTO_Allocate(PIN_PARG(memoryTranslate *), CALLINGSTD_DEFAULT, NAME_MEMORY_MAP_FUNCTION, PIN_PARG(sizeMemoryTranslate_t *), PIN_PARG_END()); RTN_ReplaceSignature(freeRtn, AFUNPTR(replaceMemoryMapFun), IARG_PROTOTYPE, proto_free, IARG_CONTEXT, IARG_ORIG_FUNCPTR, IARG_FUNCARG_ENTRYPOINT_VALUE, 0, IARG_END); } }
E faça com que nossa função interceptada fique assim:
memoryTranslate * getMemoryMap(sizeMemoryTranslate_t * size){ ... return memoryMap; }
Qual é o trabalho mais não trivial, resta fazer o cliente para o OpenOCD, no cliente PinTool que eu não queria implementá-lo, então criei um aplicativo separado com o qual nosso cliente PinTool se comunica através do nome fifo.
Assim, o esquema de interfaces e comunicações é o seguinte:

Um fluxo de trabalho simplificado no exemplo de interceptação do endereço 0x123:

Vamos dar uma olhada no que acontece aqui:
o cliente PinTool é iniciado, inicializa nossos interceptores, inicia o programa
O programa inicia, ele precisa endereçar os endereços dos registradores em alguma matriz de threads, a função getMemoryMap é chamada, que nosso PinTool intercepta. Por exemplo, um dos registradores mudou para o endereço 0x123, nós o rastrearemos
Cliente PinTool salva os valores de endereços não associados
Transfira o controle de volta para o nosso programa
Além disso, em algum lugar há uma gravação em nosso endereço rastreado 0x123. A função StoreReg2Addr controla isso
E envia a solicitação de gravação para o cliente OpenOCD
O cliente retorna a resposta que é analisada. Se tudo estiver bem, o controle do programa retornará
Além disso, em algum lugar do programa, a leitura ocorre no endereço rastreado 0x123.
O loadAddr2Reg rastreia isso e envia uma solicitação OpenOCD ao cliente.
O cliente OpenOCD processa e retorna uma resposta
Se estiver tudo bem, mas o valor do registro MK for retornado ao programa
O programa continua.
Por enquanto é tudo, os códigos-fonte e os exemplos completos estarão nas seguintes partes.