Comment arrêter d'écrire le firmware des microcontrôleurs et commencer à vivre


Bonjour, je m'appelle Eugene et je suis fatigué d'écrire un firmware pour les microcontrôleurs. Comment est-ce arrivé et que faire avec cela, essayons de comprendre.


Après avoir travaillé dans une grande programmation C ++, Java, Python, etc., vous n'avez pas envie de revenir aux microcontrôleurs petits et à ventre de pot. À leurs maigres outils et bibliothèques. Mais parfois, il n'y a rien à faire, les tâches en temps réel et l'autonomie ne laissent pas de choix. Mais il existe certains types de tâches qui se déchaînent dans ce domaine à résoudre.


Par exemple, il est difficile d'imaginer des équipements de test, quelque chose de plus ennuyeux et des leçons ennuyeuses dans la programmation intégrée. En général, ainsi que des outils pratiques pour cela. Vous écrivez ... Vous flashez ... vous clignotez ... une LED (parfois connectée sur UART). Tous les stylos, sans outils de test spécialisés.


Il est également déprimant qu'il n'y ait pas de tests instrumentaux pour nos petits microcontrôleurs. Tout se fait uniquement via le firmware et via le débogueur à tester.


Et l'étude de l'utilisation de nouveaux appareils et périphériques nécessite beaucoup d'efforts et de temps. Une erreur et le programme doit être recompilé et réexécuté à chaque fois.


Pour de telles expériences, quelque chose comme REPL est plus approprié, de sorte que vous pouvez faire simplement et sans douleur ces choses, au moins triviales,:


\


Comment y arriver, cette série d'articles est consacrée.


Et cette fois, je suis tombé sur un projet où il était nécessaire de tester un appareil assez compliqué, avec beaucoup de toutes sortes de capteurs et d'autres puces que je ne connaissais pas auparavant, qui utilisait de nombreux périphériques de MK et un tas d'interfaces différentes. Le plaisir spécial était que je n'avais pas les codes sources du firmware pour la carte, donc tous les tests devraient être écrits à partir de zéro, sans utiliser le temps de fonctionnement du code source.


Le projet promettait un bon toastmaster et les compétitions sont intéressantes pendant environ deux mois (et probablement plus).


D'accord, ici nous n'allons pas pleurer. Il faut soit replonger dans les déserts du C et du firmware sans fin, soit refuser ou proposer quelque chose afin de faciliter cette leçon. Au final, la paresse et la curiosité sont le moteur du progrès.


La dernière fois, quand j'ai compris OpenOCD, je suis tombé sur un point aussi intéressant dans la documentation que


http://openocd.org/doc/html/General-Commands.html 15.4 Memory access commands mdw, mdh, mdb —         mww, mwh, mwb —        

Intéressant ... Et il est possible de lire et d'écrire des registres périphériques en les utilisant? .. cela s'avère possible, et en plus, ces commandes peuvent être exécutées à distance via le serveur TCL, qui démarre lorsque openOCD démarre.


Voici un exemple de voyant clignotant pour stm32f103C8T6


 // Step 1: Enable the clock to PORT B RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull) GPIOC->CRH = GPIO_CRH_MODE13_0 | GPIO_CRH_MODE13_1; // Step 3: Set PB0 high GPIOC->BSRR = GPIO_BSRR_BS13; // Step 4: Reset PB0 low GPIOC->BSRR = GPIO_BSRR_BR13; 

et une séquence similaire de commandes openOCD


 mww 0x40021018 0x10 mww 0x40011004 0x300000 mww 0x40011010 0x2000 mww 0x40011010 0x20000000 

Et maintenant, si vous pensez à l'éternel et considérez le firmware pour MK ... alors le but principal de ces programmes est d'écrire sur les registres de puces; le firmware qui fera juste quelque chose et ne fonctionnera qu'avec le cœur du processeur n'a aucune utilité pratique!


Remarque

Bien sûr, vous pouvez considérer la crypte (=


Beaucoup se souviendront davantage de l'utilisation des interruptions. Mais ils ne sont pas toujours nécessaires, et dans mon cas, vous pouvez vous en passer.


Et donc, la vie s'améliore. Dans la source openOCD, vous pouvez même trouver un exemple intéressant d' utilisation de cette interface.


Très bon blanc sur python.


Il est tout à fait possible de convertir des adresses de registre à partir de fichiers d'en-tête et de commencer à écrire dans un langage de script casher. Vous pouvez déjà préparer du champagne, mais cela ne m'a pas semblé suffisant, car je veux utiliser la bibliothèque de périphériques standard ou le nouveau HAL pour travailler avec des périphériques au lieu de s'embarrasser de registres.


Portage de bibliothèques sur python ... dans un cauchemar, nous le ferons. Vous devez donc utiliser ces bibliothèques en C ou ... C ++. Et chez les pros, vous pouvez remplacer presque tous les opérateurs ... pour leurs classes.


Et les adresses de base dans les fichiers d'en-tête, remplacent par des objets de leurs classes.


Par exemple, dans le fichier stm32f10x.h


 #define PERIPH_BB_BASE ((uint32_t)0x42000000) /*!< Peripheral base address in the bit-band region */ 

Remplacez par


 class InterceptAddr; InterceptAddr addr; #define PERIPH_BB_BASE (addr) /*!< Peripheral base address in the bit-band region */ 

Mais les jeux avec des pointeurs dans la bibliothèque coupent cette idée dans l'œuf ...


Voici un exemple de fichier stm32f10x_i2c.c:


 FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG) { __IO uint32_t i2creg = 0, i2cxbase = 0; …. /* Get the I2Cx peripheral base address */ i2cxbase = (uint32_t)I2Cx; …. 

Il est donc nécessaire d'intercepter les adresses pour les adresses différemment. Comment faire cela vaut probablement le coup d'œil à Valgrind, ce n'est pas pour rien qu'il a un vérificateur de mémoire. Eh bien, il devrait vraiment savoir comment intercepter des adresses.


Pour l'avenir, je dirai qu'il vaut mieux ne pas y regarder ... J'ai presque réussi à intercepter les appels aux adresses. Pour presque tous les cas sauf ceci


 Int * p = ... *p = 0x123; 

Il est possible d'intercepter l'adresse, mais il n'était plus possible d'intercepter les données enregistrées. Seul le nom du registre interne dans lequel se trouve cette valeur, mais qui ne peut pas être atteint à partir de memcheck.


En fait, Valgrind m'a surpris, à l'intérieur de l'ancien monstre libVEX est utilisé, sur lequel je n'ai trouvé aucune information sur Internet. C'est bien qu'une petite documentation ait été trouvée dans les fichiers d'en-tête.


Ensuite, il y avait d'autres outils DBI.


Frida, Dynamic RIO, un peu plus, et enfin obtenu Pintool.


PinTool avait une assez bonne documentation et des exemples. Même si je n'en avais pas encore assez, j'ai dû faire des expériences avec certaines choses. L'outil s'est avéré très puissant, il ne fait que bouleverser le code fermé et la restriction uniquement à la plate-forme Intel (bien qu'à l'avenir cela puisse être contourné)


Nous devons donc intercepter l'écriture et la lecture à des adresses spécifiques. Voyons quelles instructions sont responsables de ce https://godbolt.org/z/nJS9ci .


Pour x64, ce sera un MOV pour les deux opérations.


Et pour x86, ce sera MOV pour l'écriture et MOVZ pour la lecture.


Remarque: il est préférable de ne pas activer l'optimisation, sinon d'autres instructions peuvent apparaître.


En-tête de spoiler
 INS_AddInstrumentFunction(EmulateLoad, 0); INS_AddInstrumentFunction(EmulateStore, 0); ..... static VOID EmulateLoad(INS ins, VOID *v) { // Find the instructions that move a value from memory to a register if ((INS_Opcode(ins) == XED_ICLASS_MOV || INS_Opcode(ins) == XED_ICLASS_MOVZX) && INS_IsMemoryRead(ins) && INS_OperandIsReg(ins, 0) && INS_OperandIsMemory(ins, 1)) { INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(loadAddr2Reg), IARG_MEMORYREAD_EA, IARG_MEMORYREAD_SIZE, IARG_RETURN_REGS, INS_OperandReg(ins, 0), IARG_END); // Delete the instruction INS_Delete(ins); } } static VOID EmulateStore(INS ins, VOID *v) { if (INS_Opcode(ins) == XED_ICLASS_MOV && INS_IsMemoryWrite(ins) && INS_OperandIsMemory(ins, 0)) { if (INS_hasKnownMemorySize(ins)) { if (INS_OperandIsReg(ins, 1)) { INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(multiMemAccessStore), IARG_MULTI_MEMORYACCESS_EA, IARG_REG_VALUE, INS_OperandReg(ins, 1), IARG_END); } else if (INS_OperandIsImmediate(ins, 1)) { INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)multiMemAccessStore, IARG_MULTI_MEMORYACCESS_EA, IARG_UINT64, INS_OperandImmediate(ins, 1), IARG_END); } } else { if (INS_OperandIsReg(ins, 1)) { INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr), IARG_MEMORYWRITE_EA, IARG_REG_VALUE, INS_OperandReg(ins, 1), IARG_MEMORYWRITE_SIZE, IARG_END); } else if (INS_OperandIsImmediate(ins, 1)) { INS_InsertCall(ins, IPOINT_BEFORE, AFUNPTR(storeReg2Addr), IARG_MEMORYWRITE_EA, IARG_UINT64, INS_OperandImmediate(ins, 1), IARG_UINT32, IARG_MEMORYWRITE_SIZE, IARG_END); } } } } 

Dans le cas de la lecture de l'adresse, nous appelons la fonction loadAddr2Reg et supprimons l'instruction d'origine. Sur cette base, loadAddr2Reg devrait nous renvoyer la valeur nécessaire.


Avec un enregistrement, c'est de plus en plus difficile ... les arguments peuvent être de différents types et peuvent également être transmis de différentes manières, vous devez donc appeler différentes fonctions avant la commande. Sur une plate-forme 32 bits, multiMemAccessStore et 64 storeReg2Addr seront appelés. Et ici, nous ne supprimons pas l'instruction de la chaîne de montage. Il n'y a aucun problème pour le retirer, mais dans certains cas, il n'est pas possible d'imiter son action. Le programme se bloque parfois pour sigfault pour une raison quelconque. Pour nous, ce n'est pas critique, que ce soit écrit pour lui-même, l'essentiel est qu'il y ait la possibilité d'intercepter des arguments.


Ensuite, nous devons voir quelles adresses nous devons intercepter, consultez la carte mémoire de notre puce stm32f103C8T6:


image
Nous sommes intéressés par les adresses avec SRAM et PERIPH_BASE, c'est-à-dire de 0x20000000 à 0x20000000 + 128 * 1024 et de 0x40000000 à 0x40030000. Eh bien, ou plutôt, pas tout à fait, comme nous nous souvenons des instructions d'enregistrement, nous n'avons pas pu les supprimer. Par conséquent, l'enregistrement à ces adresses tombera en sigfault. De plus, il est peu probable que ces adresses contiennent des données de notre programme, et non cette puce en a une autre. Par conséquent, nous devons définitivement les réparer quelque part. Disons sur une sorte de tableau.


Nous créons des tableaux de la taille requise, puis substituons leurs pointeurs dans les définitions d'adresse de base.


Dans notre programme, dans les titres à la place


 #define SRAM_BASE ((uint32_t)0x20000000) /*!< SRAM base address in the alias region */ #define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */ 

Faire


  #define SRAM_BASE ((AddrType)pAddrSRAM) #define PERIPH_BASE ((AddrType)pAddrPERIPH) 

et où pAddrSRAM et pAddrPERIPH sont des pointeurs vers des tableaux pré-alloués.


Maintenant, notre client PinTool doit en quelque sorte transmettre comment nous avons réparé les adresses nécessaires.
La chose la plus simple qui m'a semblé faire était d'intercepter une fonction qui renvoie une structure de tableau de ce format:


 typedef struct { addr_t start_addr; //      addr_t end_addr; //   addr_t reference_addr; //   } memoryTranslate; 

Par exemple, pour notre puce, elle sera tellement remplie


 map->start_addr = (addr_t)pAddrSRAM; map->end_addr = 96*1024; map->reference_addr = (addr_t)0x20000000U; 

Il n'est pas difficile d'intercepter la fonction et d'en tirer les valeurs requises:


 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); } } 

Et faites ressembler notre fonction interceptée à ceci:


 memoryTranslate * getMemoryMap(sizeMemoryTranslate_t * size){ ... return memoryMap; } 

Quel est le travail le plus simple qui soit, il reste à rendre le client à OpenOCD, dans le client PinTool, je ne voulais pas l'implémenter, j'ai donc fait une application distincte avec laquelle notre client PinTool communique via nommé fifo.


Ainsi, le schéma des interfaces et des communications est le suivant:
image
Un workflow simplifié sur l'exemple d'interception de l'adresse 0x123:


image
Voyons ce qui se passe ici:


le client PinTool est lancé, il initialise nos intercepteurs, démarre le programme
Le programme démarre, il doit adresser les adresses des registres sur un tableau de threads, la fonction getMemoryMap est appelée, que notre PinTool intercepte. Pour un exemple, l'un des registres a basculé à l'adresse 0x123, nous allons le suivre
Le client PinTool enregistre les valeurs des adresses dissociées
Transférer le contrôle à notre programme
De plus, quelque part il y a un enregistrement à notre adresse suivie 0x123. La fonction StoreReg2Addr garde cette trace
Et envoie la demande d'écriture au client OpenOCD
Le client renvoie la réponse, qui est analysée. Si tout va bien, le contrôle du programme revient
De plus, quelque part dans le programme, la lecture a lieu à l'adresse suivie 0x123.
loadAddr2Reg effectue le suivi et envoie une demande OpenOCD au client.
Le client OpenOCD le traite et renvoie une réponse
Si tout va bien, mais la valeur du registre MK est retournée au programme
Le programme continue.
C'est tout pour l'instant, des codes source complets et des exemples seront dans les parties suivantes.

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


All Articles