Cómo dejar de escribir firmware para microcontroladores y comenzar a vivir


Hola, mi nombre es Eugene y estoy cansado de escribir firmware para microcontroladores. Cómo sucedió esto y qué hacer con él, vamos a resolverlo.


Después de trabajar en programación grande C ++, Java, Python, etc., no tiene ganas de volver a los microcontroladores pequeños y panzudos. A sus escasas herramientas y bibliotecas. Pero a veces no hay nada que hacer, las tareas en tiempo real y la autonomía no dejan elección. Pero hay algunos tipos de tareas que simplemente se vuelven locos en esta área para resolver.


Por ejemplo, pruebas de equipos, algo más aburrido y lecciones aburridas en programación integrada, difícilmente se pueden imaginar. En general, así como herramientas convenientes para esto. Usted escribe ... parpadea ... parpadea ... un LED (a veces se conecta a UART). Todos los bolígrafos, sin herramientas de prueba especializadas.


También es deprimente que no haya pruebas instrumentales para nuestros pequeños microcontroladores. Todo es solo a través del firmware y a través del depurador para probar.


Y el estudio de trabajar con nuevos dispositivos y periféricos requiere mucho esfuerzo y tiempo. Un error y el programa se debe volver a compilar y volver a ejecutar cada vez.


Para tales experimentos, algo como REPL es más adecuado, para que pueda hacer estas cosas, al menos triviales, de manera simple y sin dolor:


\


Cómo llegar a esto, esta serie de artículos está dedicada.


Y esta vez me encontré con un proyecto en el que era necesario probar un dispositivo bastante complicado, con muchos tipos de sensores y otros chips desconocidos para mí anteriormente, que usaban muchos periféricos MK y un montón de interfaces diferentes. La diversión especial fue que no tenía los códigos fuente de firmware para la placa, por lo que todas las pruebas tendrían que escribirse desde cero, sin usar el tiempo de operación del código fuente.


El proyecto prometió un buen maestro de brindis y las competiciones son interesantes durante dos meses más o menos (y muy probablemente más).


Vale, aquí no vamos a llorar. Uno debe sumergirse nuevamente en la naturaleza de C y el firmware infinito, o rechazar o inventar algo para facilitar esta lección. Al final, la pereza y la curiosidad son el motor del progreso.


La última vez, cuando entendí OpenOCD, encontré un punto tan interesante en la documentación como


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

Interesante ... Y es posible leer y escribir registros periféricos con ellos ... resulta posible, y además, estos comandos se pueden ejecutar de forma remota a través del servidor TCL, que se inicia cuando se inicia openOCD.


Aquí hay un ejemplo de un LED parpadeante para 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; 

y una secuencia similar de comandos openOCD


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

Y ahora, si piensa en lo eterno y considera el firmware para MK ... entonces el propósito principal de estos programas es escribir en los registros del chip; ¡el firmware que solo hará algo y funcionará solo con el núcleo del procesador no tiene un uso práctico!


Nota

Aunque, por supuesto, puedes considerar la cripta (=


Muchos recordarán más sobre trabajar con interrupciones. Pero no siempre son obligatorios, y en mi caso, puedes prescindir de ellos.


Y así, la vida está mejorando. En la fuente openOCD, incluso puede encontrar un ejemplo interesante de uso de esta interfaz.


Muy bien en blanco en python.


Es bastante posible convertir direcciones de registro de archivos de encabezado y comenzar a escribir en un lenguaje de script kosher. Ya puedes preparar champán, pero me pareció que no era suficiente, porque quiero usar la Biblioteca de periféricos estándar o el nuevo HAL para trabajar con periféricos en lugar de preocuparme por los registros.


Portando bibliotecas a Python ... en alguna pesadilla lo haremos. Por lo tanto, debe usar estas bibliotecas en C o ... C ++. Y en los profesionales, puede anular a casi todos los operadores ... para sus clases.


Y las direcciones base en los archivos de encabezado, se reemplazan con objetos de sus clases.


Por ejemplo, en el archivo stm32f10x.h


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

Reemplazar con


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

Pero los juegos con punteros en la biblioteca cortan esta idea de raíz ...


Aquí hay un ejemplo de archivo 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; …. 

Por lo tanto, es necesario interceptar direcciones a direcciones de alguna manera diferente. Probablemente valga la pena echarle un vistazo a Valgrind, no en vano, tiene un memchecker. Bueno, realmente debería saber cómo interceptar direcciones.


Mirando hacia el futuro, diré que es mejor no mirar allí ... Casi logré interceptar llamadas a direcciones. Para casi todos los casos excepto este


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

Es posible interceptar la dirección, pero ya no fue posible interceptar los datos grabados. Solo el nombre del registro interno en el que se encuentra este valor, pero que no se puede alcanzar desde memcheck.


De hecho, Valgrind me sorprendió, dentro del antiguo monstruo se usa libVEX, sobre el cual no encontré ninguna información en Internet. Es bueno que se haya encontrado un poco de documentación en los archivos de encabezado.


Luego había otras herramientas DBI.


Frida, Dynamic RIO, un poco más, y finalmente obtuve Pintool.


PinTool tenía bastante buena documentación y ejemplos. Aunque todavía no tenía suficientes, tuve que hacer experimentos con algunas cosas. La herramienta resultó ser muy poderosa, solo altera el código cerrado y la restricción solo a la plataforma de inteligencia (aunque en el futuro esto se puede eludir)


Entonces, necesitamos interceptar escribir y leer en direcciones específicas. Veamos qué instrucciones son responsables de este https://godbolt.org/z/nJS9ci .


Para x64, este será un MOV para ambas operaciones.


Y para x86 será MOV para escribir y MOVZ para leer.


Nota: es mejor no habilitar la optimización, de lo contrario pueden aparecer otras instrucciones.


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

En el caso de leer desde la dirección, llamamos a la función loadAddr2Reg y eliminamos la instrucción original. En base a esto, loadAddr2Reg debería devolvernos el valor necesario.


Con un registro, es cada vez más difícil ... los argumentos pueden ser de diferentes tipos y también pueden transmitirse de diferentes maneras, por lo que debe llamar a diferentes funciones antes del comando. En una plataforma de 32 bits, se llamará a multiMemAccessStore y en 64 storeReg2Addr. Y aquí no eliminamos las instrucciones de la línea de ensamblaje. No hay problemas para eliminarlo, pero en algunos casos no es posible imitar su acción. El programa por alguna razón a veces se bloquea en sigfault. Para nosotros, esto no es crítico, dejémoslo escrito, lo principal es que existe la posibilidad de interceptar argumentos.


A continuación, necesitamos ver qué direcciones necesitamos interceptar, mira el Mapa de Memoria para nuestro chip stm32f103C8T6:


imagen
Estamos interesados ​​en direcciones con SRAM y PERIPH_BASE, es decir, de 0x20000000 a 0x20000000 + 128 * 1024 y de 0x40000000 a 0x40030000. Bueno, o mejor dicho, no del todo, ya que recordamos las instrucciones de grabación, no pudimos eliminarlas. Por lo tanto, el registro en estas direcciones se caerá en sigfault. Además, existe una probabilidad poco probable de que estas direcciones tengan datos de nuestro programa, no que este chip tenga otro. Por lo tanto, definitivamente necesitamos arreglarlos en alguna parte. Digamos en algún tipo de matriz.


Creamos matrices del tamaño requerido, y luego sustituimos sus punteros en la dirección base definida.


En nuestro programa, en los titulares


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

Hacer


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

y donde pAddrSRAM y pAddrPERIPH son punteros a matrices preasignadas.


Ahora, nuestro cliente PinTool necesita transmitir de alguna manera cómo reparamos las direcciones necesarias.
Lo más simple que me pareció cómo hacer esto fue interceptar una función que devuelve una estructura de matriz de este formato:


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

Por ejemplo, para nuestro chip estará tan lleno


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

No es difícil interceptar la función y tomar los valores requeridos de ella:


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

Y haga que nuestra función interceptada se vea así:


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

Cuál es el trabajo más trivial realizado, queda por hacer que el cliente abra OpenOCD, en el cliente PinTool no quería implementarlo, así que hice una aplicación separada con la cual nuestro cliente PinTool se comunica a través de named fifo.


Por lo tanto, el esquema de interfaces y comunicaciones es el siguiente:
imagen
Un flujo de trabajo simplificado en el ejemplo de interceptar la dirección 0x123:


imagen
Echemos un vistazo a lo que sucede aquí:


se inicia el cliente PinTool, inicializa nuestros interceptores, inicia el programa
El programa se inicia, necesita direccionar las direcciones de los registros en una matriz de subprocesos, se llama a la función getMemoryMap, que intercepta nuestra PinTool. Por ejemplo, uno de los registros ha cambiado a la dirección 0x123, lo rastrearemos
El cliente PinTool guarda los valores de las direcciones disociadas
Transfiera el control a nuestro programa
Además, en algún lugar hay una grabación en nuestra dirección rastreada 0x123. La función StoreReg2Addr realiza un seguimiento de esto
Y envía la solicitud de escritura al cliente OpenOCD
El cliente devuelve la respuesta, que se analiza. Si todo está bien, entonces el control del programa regresa
Además, en algún lugar del programa, la lectura ocurre en la dirección rastreada 0x123.
loadAddr2Reg realiza un seguimiento de esto y envía una solicitud OpenOCD al cliente.
El cliente OpenOCD lo procesa y devuelve una respuesta
Si todo está bien, pero el valor del registro MK se devuelve al programa
El programa continúa.
Eso es todo por ahora, los códigos fuente completos y ejemplos estarán en las siguientes partes.

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


All Articles