Validación de direcciones de memoria en un Cortex-M0 / M3 / M4 / M7

Hola Habr!

Con respecto al aflojamiento del r√©gimen el otro d√≠a, la indignaci√≥n en los comentarios de una publicaci√≥n vecina de que los art√≠culos sobre microcontroladores est√°n parpadeando por el LED, as√≠ como la muerte prematura de mi blog est√°ndar, que soy demasiado vago para restaurar, transferir√© aqu√≠ material √ļtil sobre una peque√Īa y lamentable luz. Un truco de prensa para trabajar con n√ļcleos Cortex-M: verificar la validez de las direcciones aleatorias.


Una de las funciones muy √ļtiles y al mismo tiempo, por alguna raz√≥n, listas para usar que no se describen en ning√ļn lugar en los microcontroladores Cortex-M (todo) es la capacidad de verificar la exactitud de la direcci√≥n en la memoria. Con √©l, puede determinar el tama√Īo de flash, RAM y EEPROM, determinar la presencia en un procesador particular de perif√©ricos y registros espec√≠ficos, vencer los procesos ca√≠dos mientras se mantiene la salud general del sistema operativo, etc.

En el modo normal, cuando llega a una dirección inexistente en Cortex-M3 / M4 / M7, se emite una excepción BusFault y, en ausencia de su controlador, se intensifica a HardFault. En Cortex-M0 no hay excepciones "detalladas" (MemFault, BusFault, UsageFault), y cualquier falla se escala inmediatamente a HardFault.

En general, no puede ignorar HardFault: puede ser el resultado de una falla de hardware, por ejemplo, y el comportamiento posterior del dispositivo se volver√° impredecible. Pero en el caso particular, esto puede y debe hacerse.

Cortex-M3 y Cortex-M4: el BusFault no cumplido


En Cortex-M3 y superior, verificar la validez de la dirección es bastante simple: debe prohibir todas las excepciones (excepto, obviamente, no enmascarables) a través del registro FAULTMASK, deshabilitar el procesamiento de BusFault específicamente y luego meter la dirección que se está verificando y ver si la bandera BFARVALID en el registro BFAR , es decir, registro de dirección de falla del bus. Si lo ha tomado, acaba de tener un BusFault, es decir La dirección es incorrecta.

El código se ve así, todas las definiciones y funciones provienen del CMSIS estándar (no de proveedor), por lo que debería funcionar en cualquier M3, M4 o M7:

bool cpu_check_address(volatile const char *address) { /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */ static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; /* Clear BFARVALID flag by writing 1 to it */ SCB->CFSR |= BFARVALID_MASK; /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */ uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; /* probe address in question */ *address; /* Check BFARVALID flag */ if ((SCB->CFSR & BFARVALID_MASK) != 0) { /* Bus Fault occured reading the address */ is_valid = false; } /* Reenable BusFault by clearing BFHFNMIGN */ SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; } 

Cortex-M0 y Cortex-M0 +


Con Cortex-M0 y Cortex-M0 + todo es más complicado, como dije anteriormente, no tienen BusFault y todos los registros correspondientes, y las excepciones se escalan de inmediato a HardFault. Por lo tanto, solo hay una salida: hacer que el controlador HardFault pueda comprender que la excepción fue causada deliberadamente y volver a la función que la llamó, pasando una bandera que indica que HardFault estaba allí.

Esto se hace puramente en ensamblador. En el siguiente ejemplo, el registro R5 se establece en 1 y se escriben dos "n√ļmeros m√°gicos" en los registros R1 y R2. Si HardFault ocurre despu√©s de intentar cargar el valor en la direcci√≥n que se est√° verificando, entonces debe verificar los valores de R1 y R2, y si encuentran los n√ļmeros necesarios, establezca R5 en cero. El valor de R5 se transfiere al c√≥digo syshech a trav√©s de una variable especial que est√° r√≠gidamente vinculada a este registro, la direcci√≥n que se ensamblar√° en el ensamblador est√° impl√≠cita, solo sabemos que en arm-none-eabi el primer par√°metro de la funci√≥n se coloca en R0.

 bool cpu_check_address(volatile const char *address) { /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */ (void)address; /* R5 will be set to 0 by HardFault handler */ /* to indicate HardFault has occured */ register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" /* set default R5 value */ "ldr r1, =0xDEADF00D \n" /* set magic number */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic to be sure */ "ldrb r3, [r0] \n" /* probe address */ ); return result; } 

El código del controlador HardFault en su forma más simple se ve así:

 __attribute__((naked)) void hard_fault_default(void) { /* Get stack pointer where exception stack frame lies */ __asm__ volatile ( /* decide if we need MSP or PSP stack */ "movs r0, #4 \n" /* r0 = 0x4 */ "mov r2, lr \n" /* r2 = lr */ "tst r2, r0 \n" /* if(lr & 0x4) */ "bne use_psp \n" /* { */ "mrs r0, msp \n" /* r0 = msp */ "b out \n" /* } */ " use_psp: \n" /* else { */ "mrs r0, psp \n" /* r0 = psp */ " out: \n" /* } */ /* catch intended HardFaults on Cortex-M0 to probe memory addresses */ "ldr r1, [r0, #0x04] \n" /* read R1 from the stack */ "ldr r2, =0xDEADF00D \n" /* magic number to be found */ "cmp r1, r2 \n" /* compare with the magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x08] \n" /* read R2 from the stack */ "ldr r2, =0xCAFEBABE \n" /* 2nd magic number to be found */ "cmp r1, r2 \n" /* compare with 2nd magic number */ "bne regular_handler \n" /* no magic -> handle as usual */ "ldr r1, [r0, #0x18] \n" /* read PC from the stack */ "add r1, r1, #2 \n" /* move to the next instruction */ "str r1, [r0, #0x18] \n" /* modify PC in the stack */ "ldr r5, =0 \n" /* set R5 to indicate HardFault */ "bx lr \n" /* exit the exception handler */ " regular_handler: \n" /* here comes the rest of the fucking owl */ ) 

En el momento de abandonar el controlador de excepciones, Cortex arroja los registros, que se garantiza que est√°n da√Īados por el controlador (R0-R3, R12, LR, PC ...), en la pila. El primer fragmento, que ya se encuentra en la mayor√≠a de los controladores HardFault listos para usar, a excepci√≥n de aquellos escritos bajo metal puro, determina qu√© pila: cuando se trabaja en el sistema operativo, puede ser MSP o PSP, y tienen direcciones diferentes. En proyectos de metal desnudo, la pila MSP (puntero de pila principal) generalmente se instala a priori, sin verificar, porque el PSP (puntero de pila de proceso) no puede estar all√≠ debido a la falta de procesos.

Despu√©s de determinar la pila deseada y poner su direcci√≥n en R0, leemos los valores R1 (desplazamiento 0x04) y R2 (desplazamiento 0x08), la comparamos con palabras m√°gicas, si ambas coinciden: leemos el valor de PC (desplazamiento 0x18) de la pila, sumamos 2 (2 bytes: el tama√Īo de la instrucci√≥n en Cortex-M *) y gu√°rdelo de nuevo en la pila. Si esto no se hace, cuando regresemos del controlador, nos encontraremos con la misma instrucci√≥n que realmente caus√≥ la excepci√≥n, y siempre correremos en c√≠rculos. El ap√©ndice 2 nos lleva a la siguiente instrucci√≥n en el momento del regreso.

* Upd. En los comentarios, surgi√≥ la pregunta sobre el tama√Īo de las instrucciones en el Cortex-M, har√© la respuesta correcta aqu√≠: en este caso, el bloqueo provoca la instrucci√≥n LDRB, que est√° disponible en la arquitectura ARMv7-M en dos versiones: 16 bits y 32 bits. La segunda opci√≥n se seleccionar√° si se cumple al menos una de las condiciones:

  • el autor indic√≥ expl√≠citamente la instrucci√≥n LDRB.W en lugar de LDRB (nosotros no)
  • se utilizan registros por encima de R7 (para nosotros - R0 y R3)
  • Se especifica un desplazamiento mayor de 31 bytes (no tenemos un desplazamiento)


En todos los demás casos (es decir, cuando los operandos se ajustan al formato de la versión de 16 bits de la instrucción), el ensamblador debe seleccionar la versión de 16 bits.

Por lo tanto, en nuestro caso, siempre habrá una instrucción de 2 bytes que debe ser superada, pero si edita el código con fuerza, las opciones son posibles.

Luego, escriba 0 en R5, que sirve como indicador de entrar en HardFault. Los registros después de R3 no se guardan en la pila antes de los registros especiales, y al salir del controlador, no se restauran de ninguna manera, por lo que es nuestra conciencia estropearlos o no estropearlos. En este caso, cambiamos R5 de 1 a 0 a propósito.

El regreso del manejador de interrupciones se realiza exactamente de una manera. Al ingresar al controlador, se escribe un valor especial en el registro LR llamado EXC_RETURN, que es necesario escribir en la PC para salir del controlador, y no solo escribirlo, sino hacerlo con un comando POP o BX (es decir, "mov pc, lr, por ejemplo, no funciona , aunque la primera vez te parezca que funciona). BX LR parece un intento de ir a una dirección sin sentido (en LR habrá algo como 0xFFFFFFF1 que no tiene nada que ver con la dirección real del procedimiento al que debemos regresar), pero en realidad el procesador ve este valor en la PC (donde irá automáticamente), restaurará los registros de la pila y continuará ejecutando nuestro procedimiento, con el siguiente procedimiento después de llamar a HardFault debido al hecho de que aumentamos manualmente la PC en esta pila en 2.

Puede leer sobre todas las compensaciones y comandos claramente dónde , por supuesto.

Bueno, o si los n√ļmeros m√°gicos no son visibles, entonces todo ir√° a regular_handler, despu√©s de lo cual sigue el procedimiento habitual de procesamiento de HardFault: como regla, esta es una funci√≥n que imprime valores de registro en la consola, decide qu√© hacer a continuaci√≥n con el procesador, etc.

Determinaci√≥n del tama√Īo de RAM


Usar todo esto es simple y directo. ¬ŅQueremos escribir un firmware que funcione en varios microcontroladores con diferentes cantidades de RAM, mientras que cada vez use RAM en un programa completo?

Sí fácil:

 static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); } 

Entonces, se necesita maxsize aqu√≠, que a la m√°xima cantidad posible de RAM entre √©l y el siguiente bloque de direcciones puede que no haya espacio en el que cpu_check_address se rompa. En este ejemplo, es 80 KB. Tampoco tiene sentido sondear todas las direcciones: solo mire la hoja de datos para ver cu√°l es el paso m√≠nimo posible entre los dos modelos de controlador y config√ļrelo como bloque.

Transición programática al gestor de arranque ubicado en el medio de la nada


A veces puedes hacer trucos a√ļn m√°s complicados; por ejemplo, imagina que quieres saltar program√°ticamente al cargador de arranque de f√°brica STM32 para cambiar al modo de actualizaci√≥n de firmware a trav√©s de UART o USB, sin molestarte en escribir tu cargador de arranque.

El gestor de arranque STM32 se encuentra en el área llamada Memoria del sistema, a la que debe ir, pero hay un problema: esta área tiene diferentes direcciones no solo en diferentes series de procesadores, sino en diferentes modelos de la misma serie (se puede encontrar una placa épica en AN2606 en páginas 22 a 26). Cuando agrega la funcionalidad correspondiente a la plataforma en general, y no solo a un producto específico, desea versatilidad.

En los archivos CMSIS, tambi√©n falta la direcci√≥n de inicio de la Memoria del sistema. No es posible determinarlo por ID de cargador de arranque, porque Este es un problema de huevo y gallina: la identificaci√≥n del cargador de arranque se encuentra en el √ļltimo byte de la memoria del sistema, lo que nos lleva de nuevo a la cuesti√≥n de la direcci√≥n.

Sin embargo, si miramos la tarjeta de memoria STM32, veremos algo como esto:


En este caso, estamos interesados ‚Äč‚Äčen el entorno de Memoria del sistema; por ejemplo, en la parte superior hay un √°rea que alguna vez fue programable (no en todos los STM32) y bytes de Opci√≥n (en total). Esta estructura se observa no solo en diferentes modelos, sino en diferentes l√≠neas STM32, con la √ļnica diferencia en la presencia de OTP y la presencia de una brecha en las direcciones entre la memoria del sistema y las opciones.

Pero para nosotros en este caso, lo más importante es que la dirección de inicio de los Bytes de opción está en los encabezados regulares de CMSIS; allí se llama OB_BASE.

Más simple. Escribimos la función para buscar la primera dirección válida o no válida desde la especificada:

 char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; } 

Y mire hacia abajo desde los bytes de Opción, primero el final de la memoria del sistema o la OTP adyacente, y luego el comienzo de la memoria del sistema en dos pasadas:

 /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; 

Y sin mucha dificultad, organizamos esto en una función que encuentra el comienzo de la memoria del sistema y salta sobre ella, es decir, inicia el gestor de arranque:

 static void jump_to_bootloader(void) __attribute__ ((noreturn)); /* Sets up and jumps to the bootloader */ static void jump_to_bootloader(void) { /* System memory is the valid area next _below_ Option bytes */ char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; /* Here we have System memory top address */ c = cpu_find_next_valid_address(a, b, true); /* Here we have System memory bottom address */ c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); /* Reset the stack pointer */ __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); } 

Depende del modelo de procesador específico ... nada depende. La lógica no funcionará en los modelos que tienen un agujero entre OTP y la memoria del sistema, pero no he verificado si hay alguno. Trabajará activamente con OTP - verificar.

Otros trucos se aplican solo al procedimiento habitual para llamar al gestor de arranque desde su c√≥digo; no olvide restablecer el puntero de la pila y llame al procedimiento para abandonar el gestor de arranque antes de inicializar perif√©ricos en el procesador, velocidades de reloj, etc .: debido a su minimalismo, el gestor de arranque puede obstruirse Inicialice la periferia y espere que est√© en el estado predeterminado. Una buena opci√≥n para llamar al gestor de arranque desde un lugar arbitrario en su programa es escribir en el RTC Backup Register o simplemente en una direcci√≥n conocida en la memoria de un n√ļmero m√°gico, reiniciar el programa y verificar en las primeras etapas de inicializaci√≥n de este n√ļmero.

PD Dado que todas las direcciones en la tarjeta de memoria del procesador est√°n alineadas en el peor de los casos por 4, el procedimiento anterior se acelerar√° en gran medida por la idea de recorrerlas en incrementos de 4 bytes en lugar de uno.

Aviso importante


NB: tenga en cuenta que en un controlador espec√≠fico la validez de una direcci√≥n espec√≠fica no indica necesariamente la presencia real de la funcionalidad que se puede encontrar en esa direcci√≥n. Por ejemplo, la direcci√≥n del registro que controla alg√ļn bloque perif√©rico opcional puede ser v√°lida, aunque el bloque en s√≠ est√° ausente en este modelo. Desde el punto de vista del fabricante, los trucos m√°s interesantes son posibles, generalmente enraizados en el uso de los mismos cristales para diferentes modelos de procesadores. Sin embargo, en la mayor√≠a de los casos, estos procedimientos funcionan y resultan muy √ļtiles.

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


All Articles