So beenden Sie das Schreiben von Firmware für Mikrocontroller und beginnen zu leben


Hallo, mein Name ist Eugene und ich bin es leid, Firmware für Mikrocontroller zu schreiben. Wie ist das passiert und was soll man damit machen? Lassen Sie es uns herausfinden.


Nachdem Sie in C ++, Java, Python usw. mit großer Programmierung gearbeitet haben, möchten Sie nicht mehr zu den kleinen Mikrocontrollern mit starkem Bauch zurückkehren. Zu ihren mageren Werkzeugen und Bibliotheken. Aber manchmal gibt es nichts zu tun, die Aufgaben von Echtzeit und Autonomie lassen keine Wahl. Es gibt jedoch einige Arten von Aufgaben, die in diesem Bereich nur zu lösen sind.


Zum Beispiel ist das Testen von Geräten, etwas Langweiligeres und Langweiligeres in der eingebetteten Programmierung, kaum vorstellbar. Im Allgemeinen sowie praktische Werkzeuge dafür. Sie schreiben ... Sie blinken ... Sie blinken ... eine LED (manchmal meldet sich UART an). Alle Stifte ohne spezielle Testwerkzeuge.


Es ist auch bedrückend, dass es für unsere kleinen Mikrocontroller keine instrumentellen Tests gibt. Alles ist nur über die Firmware und über den Debugger zu testen.


Das Studium der Arbeit mit neuen Geräten und Peripheriegeräten erfordert viel Aufwand und Zeit. Ein Fehler und das Programm müssen jedes Mal neu kompiliert und erneut ausgeführt werden.


Für solche Experimente ist so etwas wie REPL besser geeignet, so dass Sie diese, zumindest trivialen, Dinge einfach und schmerzlos tun können:


\.


Wie man dazu kommt, ist diese Artikelserie gewidmet.


Und diesmal stieß ich auf ein Projekt, bei dem es notwendig war, ein ziemlich kompliziertes Gerät mit vielen aller Arten von Sensoren und anderen mir unbekannten Chips zu testen, bei dem viele Peripheriegeräte von MK und eine Reihe verschiedener Schnittstellen verwendet wurden. Der besondere Spaß war, dass ich nicht die Firmware-Quellcodes für das Board hatte, sodass alle Tests von Grund auf neu geschrieben werden mussten, ohne die Betriebszeit aus dem Quellcode zu verwenden.


Das Projekt versprach einen guten Toastmeister und die Wettbewerbe sind ungefähr zwei Monate lang interessant (und höchstwahrscheinlich länger).


Okay, hier werden wir nicht weinen. Man muss entweder wieder in die Wildnis von C und endloser Firmware eintauchen oder sich weigern oder sich etwas einfallen lassen, um diese Lektion zu erleichtern. Am Ende sind Faulheit und Neugier der Motor des Fortschritts.


Als ich OpenOCD das letzte Mal verstand, stieß ich auf einen so interessanten Punkt in der Dokumentation wie


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

Interessant ... Und es ist möglich, Peripherieregister mit ihnen zu lesen und zu schreiben? .. es stellt sich heraus, dass dies möglich ist, und außerdem können diese Befehle remote über den TCL-Server ausgeführt werden, der beim Start von openOCD gestartet wird.


Hier ist ein Beispiel für eine blinkende LED für 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; 

und eine ähnliche Folge von openOCD-Befehlen


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

Und jetzt, wenn Sie über das Ewige nachdenken und Firmware für MK in Betracht ziehen ... dann besteht der Hauptzweck dieser Programme darin, in die Chipregister zu schreiben; Firmware, die nur etwas tut und nur mit dem Prozessorkern funktioniert, hat keinen praktischen Nutzen!


Hinweis

Obwohl Sie natürlich die Krypta (=


Viele werden sich mehr an die Arbeit mit Interrupts erinnern. Aber sie sind nicht immer erforderlich, und in meinem Fall können Sie auf sie verzichten.


Und so wird das Leben besser. In der openOCD-Quelle finden Sie sogar ein interessantes Beispiel für die Verwendung dieser Schnittstelle.


Sehr guter Rohling auf Python.


Es ist durchaus möglich, Registeradressen aus Header-Dateien zu konvertieren und mit dem Schreiben in einer koscheren Skriptsprache zu beginnen. Sie können bereits Champagner zubereiten, aber es schien mir nicht genug zu sein, da ich die Standard Peripherals Library oder die neue HAL verwenden möchte, um mit Peripheriegeräten zu arbeiten, anstatt mich mit Registern zu beschäftigen.


Portieren von Bibliotheken nach Python ... in einem Albtraum werden wir es tun. Sie müssen diese Bibliotheken also in C oder ... C ++ verwenden. Und bei den Profis können Sie fast alle Operatoren überschreiben ... für ihre Klassen.


Und die Basisadressen in den Header-Dateien werden durch Objekte ihrer Klassen ersetzt.


Zum Beispiel in der Datei stm32f10x.h


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

Ersetzen durch


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

Aber Spiele mit Zeigern in der Bibliothek hacken diese Idee im Keim ...


Hier ist ein Beispiel für eine stm32f10x_i2c.c-Datei:


 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; …. 

Es ist also notwendig, Adressen zu Adressen abzufangen, die irgendwie anders sind. Wie das geht, ist wahrscheinlich einen Blick auf Valgrind wert, nicht umsonst, dass er einen Memchecker hat. Nun, er sollte wirklich wissen, wie man Adressen abfängt.


Mit Blick auf die Zukunft werde ich sagen, dass es besser ist, nicht dorthin zu schauen ... Ich habe es fast geschafft, Anrufe an Adressen abzufangen. In fast allen Fällen außer diesem


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

Es ist möglich, eine Adresse abzufangen, aber es war nicht mehr möglich, aufgezeichnete Daten abzufangen. Nur der Name des internen Registers, in dem dieser Wert liegt, der aber nicht über memcheck erreichbar ist.


Tatsächlich überraschte mich Valgrind, in dem alten Monster wird libVEX verwendet, über das ich im Internet keine Informationen gefunden habe. Es ist gut, dass in den Header-Dateien eine kleine Dokumentation gefunden wurde.


Dann gab es noch andere DBI-Tools.


Frida, Dynamic RIO, noch mehr und schließlich Pintool.


PinTool hatte einige ziemlich gute Dokumentationen und Beispiele. Obwohl ich immer noch nicht genug davon hatte, musste ich mit einigen Dingen experimentieren. Das Tool erwies sich als sehr leistungsfähig, es stört nur den geschlossenen Code und die Beschränkung nur auf die Intel-Plattform (obwohl dies in Zukunft umgangen werden kann).


Wir müssen also das Schreiben und Lesen an bestimmten Adressen abfangen. Mal sehen, welche Anweisungen für diese https://godbolt.org/z/nJS9ci verantwortlich sind .


Für x64 ist dies ein MOV für beide Operationen.


Und für x86 ist es MOV zum Schreiben und MOVZ zum Lesen.


Hinweis: Es ist am besten, die Optimierung nicht zu aktivieren, da sonst möglicherweise andere Anweisungen angezeigt werden.


Spoiler Überschrift
 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); } } } } 

Beim Lesen von der Adresse rufen wir die Funktion loadAddr2Reg auf und löschen die ursprüngliche Anweisung. Auf dieser Grundlage sollte loadAddr2Reg den erforderlichen Wert an uns zurückgeben.


Mit einem Datensatz wird es immer schwieriger ... Die Argumente können unterschiedlicher Art sein und auch auf unterschiedliche Weise übertragen werden, sodass Sie vor dem Befehl verschiedene Funktionen aufrufen müssen. Auf einer 32-Bit-Plattform werden multiMemAccessStore und auf 64 storeReg2Addr aufgerufen. Und hier löschen wir die Anweisung nicht vom Fließband. Es gibt keine Probleme, es zu entfernen, aber in einigen Fällen ist es nicht möglich, seine Aktion nachzuahmen. Das Programm stürzt aus irgendeinem Grund manchmal in Sigfault ab. Für uns ist dies nicht kritisch, lassen Sie es sich selbst schreiben, die Hauptsache ist, dass es die Möglichkeit gibt, Argumente abzufangen.


Als nächstes müssen wir sehen, welche Adressen wir abfangen müssen. Schauen Sie sich die Speicherzuordnung für unseren stm32f103C8T6-Chip an:


Bild
Wir sind an Adressen mit SRAM und PERIPH_BASE interessiert, d. H. Von 0x20000000 bis 0x20000000 + 128 * 1024 und von 0x40000000 bis 0x40030000. Nun, oder besser gesagt, nicht ganz, da wir uns an die Aufnahmeanweisung erinnern, konnten wir sie nicht löschen. Daher fällt der Datensatz an diesen Adressen in Sigfault aus. Darüber hinaus ist es unwahrscheinlich, dass diese Adressen Daten aus unserem Programm enthalten, nicht dass dieser Chip einen anderen hat. Deshalb müssen wir sie definitiv irgendwo reparieren. Sagen wir auf einer Art Array.


Wir erstellen Arrays mit der erforderlichen Größe und ersetzen dann ihre Zeiger in den definierten Basisadressen.


In unserem Programm stattdessen in den Schlagzeilen


 #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 */ 

Tun Sie


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

und wobei pAddrSRAM und pAddrPERIPH Zeiger auf vorab zugewiesene Arrays sind.


Jetzt muss unser PinTool-Client irgendwie vermitteln, wie wir die erforderlichen Adressen repariert haben.
Das Einfachste, was mir so vorkam, war, eine Funktion abzufangen, die eine Array-Struktur aus diesem Format zurückgibt:


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

Zum Beispiel wird unser Chip so gefüllt sein


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

Es ist nicht schwierig, die Funktion abzufangen und die erforderlichen Werte daraus zu ziehen:


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

Und lassen Sie unsere abgefangene Funktion so aussehen:


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

Was die nicht trivialste Arbeit ist, bleibt, den Client für OpenOCD zu machen. Im PinTool-Client wollte ich ihn nicht implementieren, also habe ich ihn zu einer separaten Anwendung gemacht, mit der unser PinTool-Client über den Namen fifo kommuniziert.


Somit ist das Schema der Schnittstellen und der Kommunikation wie folgt:
Bild
Ein vereinfachter Workflow am Beispiel des Abfangens der Adresse 0x123:


Bild
Werfen wir einen Blick darauf, was hier passiert:


Der PinTool-Client wird gestartet, initialisiert unsere Interceptors und startet das Programm
Das Programm startet, es muss die Adressen der Register auf einem Thread-Array adressieren, die Funktion getMemoryMap wird aufgerufen, die unser PinTool abfängt. Zum Beispiel hat eines der Register auf die Adresse 0x123 gespiegelt, wir werden es verfolgen
Der PinTool-Client speichert die Werte von nicht zugeordneten Adressen
Übertragen Sie die Kontrolle zurück in unser Programm
Außerdem gibt es irgendwo eine Aufzeichnung unter unserer verfolgten Adresse 0x123. Die StoreReg2Addr-Funktion verfolgt dies
Und sendet die Schreibanforderung an den OpenOCD-Client
Der Client gibt die analysierte Antwort zurück. Wenn alles in Ordnung ist, kehrt die Programmsteuerung zurück
Weiterhin erfolgt irgendwo im Programm das Lesen an der verfolgten Adresse 0x123.
loadAddr2Reg verfolgt dies und sendet eine OpenOCD-Anfrage an den Client.
Der OpenOCD-Client verarbeitet es und gibt eine Antwort zurück
Wenn alles in Ordnung ist, aber der Wert aus dem MK-Register an das Programm zurückgegeben wird
Das Programm wird fortgesetzt.
Das ist alles für den Moment. Vollständige Quellcodes und Beispiele finden Sie in den folgenden Abschnitten.

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


All Articles