¿Cómo podemos compartir una función entre la aplicación principal y el gestor de arranque?

Estoy usando STM32F4 (ARM-Cortex M4). Quiero compartir una función entre la aplicación principal y el cargador de arranque sin redefinir esa función, por ejemplo, imagine que escribí una función de impresión en la aplicación principal y ahora también quiero usarla en el cargador de arranque. La primera forma es copiar esa función en el cargador de arranque y luego usarla, pero no quiero esto. Quiero definir esta función 1 vez en la memoria y luego, cuando lo necesite, invocarla.

editar: tengo una idea y escribí este código para saltar entre dos programas (aplicación y gestor de arranque). Defino esta función en mi gestor de arranque:

void Shared_Func(void)
{
while(1)
    {
     HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
     HAL_Delay(700);
    }
 }  
 typedef void (*ptr_shared_func_t)(void);
 ptr_shared_func_t ptr_shared_func;

y en principal:

ptr_shared_func = Shared_Func;
ptr_shared_func();

si ejecuto este código, funciona y no hay problema. Luego, en el modo de depuración, encuentro la dirección de Shared_Func y, en lugar de usar el nombre de la función, escribo:

unsigned int *add_shared_fun = (unsigned int *)0x08001055;// correct address is   0x08001054 ->for thumb2 change to 0x08001055
ptr_shared_func = (void(*)(void))(add_shared_fun);//Shared_Func;

¡En este caso, el código no funciona! Mi objetivo final para usar esta solución fue que después de verificar este tipo de salto a la función, definiré un puntero en una dirección especial y luego guardaré la dirección de Shared_Func () en él, luego en el programa de la aplicación usaré esta dirección almacenada en ponter, salto a Shared_Func ()!

Ahora mi pregunta es ¿cuál es la mejor solución y cuál es el problema en mi código?

De la misma forma que compartes cualquier función. Con bibliotecas y archivos de cabecera.
@DKNguyen, creo que OP está hablando de 2 proyectos, un gestor de arranque y una aplicación. Bootloader es independiente y no reconoce la aplicación. Supongo que OP quiere que esta (s) función (es) no ocupe espacio en la memoria flash dos veces. Si simplemente usa esta función como cualquier otra biblioteca incluyéndola en el proyecto a través de #include "some_header.h", habrá dos copias de la misma función en la memoria flash.
Un enfoque típico sería definir una interfaz para consultar la estructura que contiene punteros a las funciones. Pero normalmente es un cargador de arranque que expone funciones para compartir con (posiblemente múltiples) aplicaciones que fueron lanzadas por ese cargador de arranque, ya que el cargador de arranque no cambia tan a menudo como las aplicaciones.
como dijo @Tagli, estoy hablando de dos proyectos separados que tienen 2 archivos .hex separados, por lo tanto, si usamos #include "*.h", entonces copiamos el doble de esa parte del código en la memoria. ¡Aquí el problema es la separación de la aplicación y el gestor de arranque!
@Vlad, ¿puedes explicar más? o usar un código de ejemplo?
@HwSwDesigner Un código que proporciona funciones también aloja la estructura con punteros a esas funciones. En el caso más simple, un puntero a esa estructura se almacena en una ubicación predefinida, y esa ubicación se da a conocer a las aplicaciones definiéndola en algún archivo .h, junto con la definición de la estructura y los prototipos de funciones.
Probablemente no sea una buena idea llamar a las funciones principales del programa desde el gestor de arranque. Un error en el programa principal podría hacer que el gestor de arranque no funcione.
@HwSwDesigner no, incluir un encabezado dos veces nunca usa memoria en el binario final; ¡así no es como nada de eso funciona! El encabezado solo debe decirle al compilador cómo se ven las funciones utilizadas en su código fuente.

Respuestas (3)

Esto está sucediendo constantemente en su computadora. Los archivos .dll y .so son básicamente código que no está compilado en su programa, pero contiene funciones que llama su programa. Cualquiera que sea la dirección a la que quiera ir, realmente no importa, la aplicación de llamada del cargador de arranque, la aplicación que llama al cargador de arranque, puede decidir sobre la cordura y los peligros de eso, pero es muy posible de cualquier manera.

Lo que no ves cuando

#include <stdio.h>
int main ( void ) { printf("Hello World!\n"); return 0; }

gcc hello.c -o hello

Es, bueno toneladas de cosas, pero en este contexto. Lo más probable es que la biblioteca C que se vincula no contenga la función printf ni las miles de otras funciones en las que se basa printf. Lo que contiene es algo que está integrado en el diseño y la implementación de la biblioteca que va y encuentra la biblioteca C y parchea los punteros de función para que esto funcione.

Aparentemente, está en un sistema baremetal, por lo que tiene que hacer todo esto usted mismo, al igual que lo hacen las personas que crean bibliotecas C, excepto que saben que están sentados sobre un sistema operativo conocido e implementan su solución basada en eso.

Hay juegos de enlazadores que puedes jugar de tal forma que construyes ambos binarios al mismo tiempo y el enlazador hace todo el trabajo, básicamente estás construyendo un binario pero dividiéndolo en dos partes, y SÓLO funcionará si tienes los binarios. desde el mismo enlace combinado, mezcle y combine, y obtendrá una falla masiva. Una característica de este enfoque es, por ejemplo, que puede crear varios binarios, podría tener un gestor de arranque y varias aplicaciones que ese gestor de arranque podría cargar y hacer que las aplicaciones llamen a funciones en el gestor de arranque sin problemas, todo el trabajo está en el script del enlazador y el extracción de varios binarios, no es necesario escribir ningún código en el gestor de arranque ni en las propias aplicaciones. Esta es una solución muy especializada y generalmente va a fallar.

La respuesta correcta es usar punteros de función donde un binario llama al otro. Cómo y cuándo hacer esto está determinado por su diseño/solución.

Podrías tener

int hello ( int );
int fun ( int x )
{
   return(hello(x+1));
}

para que escribas ese código como normal pero

int *_hello ( int x );
int hello ( int x )
{
    if(_hello==NULL)
    {
        _hello=((volatile int *)0x00001100));
    }
    return(_hello(x));
}

O en algún lugar tienes un

void init_remotes ( void )
{
    _hello=point at fixed/known address for hello;
    _there=point at fixed/known address for there;
    _world=point at fixed/known address for world;
}

No tengo ningún uso para los punteros de función, por lo que mi sintaxis está rota, pero eso no debería importar, es posible que ya lo sepa o pase el minuto buscándolo en Google.

Las estructuras también son una solución (perezosa), pero comprenda que nunca debe usar estructuras en dominios de compilación, lo que genera problemas, fallas y mantenimiento regular.

Las estructuras que no se cruzan se usan a menudo para cosas como esta, no siempre una biblioteca remota/compartida, sino en general una forma de abstraer algo. Digamos que tengo un dispositivo de almacenamiento y quiero tener una función de archivo de lectura y escritura, podría implementar eso con estructuras de funciones de almacenamiento->leer (algo). almacenamiento->escribir (algo). Y dependiendo de si es flash, una memoria USB o un disco duro, las funciones de lectura/escritura pueden apuntar a flash_read(), usb_read(), hd_read(). En algún momento antes de usar cualquiera de las funciones, los elementos en la estructura de almacenamiento apuntan a la función específica de medios si usb luego almacenamiento-> leer = usb_read algo así con la sintaxis correcta.

Ahora para lo que realmente estás preguntando. TÚ tienes que hacer el trabajo aquí, TÚ tienes que diseñar la conexión, en cualquier dirección que vayas. Si, por ejemplo, el cargador de arranque contiene las funciones comunes/compartidas, dos enfoques simples.

Una es que el cargador de arranque a menudo se encuentra en un espacio de direcciones conocido, digamos 0x00000000. Entonces, podría diseñarlo para que las direcciones de función estén en un desplazamiento conocido en el binario del cargador de arranque. Puedes llegar a poner marcadores y versiones

[0x00001000] 0x12345678  some marker to help prevent crashing in case the pointers are not here
[0x00001004] 0x00000104  a version number so that your application may know what functions are there or what version of them 
[0x00001008] 0x0000200C  address to the function hello();
[0x0000100C] 0x00003120  address to the function world();

Y la aplicación, en algún momento después de que se inicie y antes de que llame a estas funciones remotas/compartidas, debe apuntar el puntero de función de la aplicación al puntero de función remota/compartida. Entonces simplemente funciona.

No es difícil en el lado del gestor de arranque en este caso que las herramientas hagan el trabajo de completar las direcciones por usted.

Podrías en la aplicación, por ejemplo, tener

.globl hello
hello:
  ldr r3,=0x00001008
  ldr r3,[r3]
  bx r3

Esta es una solución cruda (no el asm, sino las direcciones codificadas) que funcionará. Una solución más sencilla es pasar algo a la aplicación mediante el gestor de arranque (así es básicamente como Linux obtiene sus parámetros, un registro apunta a una lista vinculada o un árbol de dispositivos de información que analiza Linux). Entonces, elija un registro, r2 puede ser definido por usted ya que posee el diseño de todo esto, la dirección (dentro del cargador de arranque, por supuesto) a la tabla de puntero de función en algún formulario diseñado por usted.

Y por feo e indeseable que sea, a menudo encontrará algo como esto, disculpe los errores en la sintaxis.

typedef struct
{
   unsigned int marker;
   unsigned int version;
   int *_hello (); //LOL I cant remember how to do this, it is probably another typedef
   int *_world;
} my_library;

my_library ml = 
{
   0x12345678,
   0x0104,
   hello,
   world
};

int hello ( int x )
{
  return(5+x);
}
int world ( int x )
{
  return(7*x);
}

//loader calling app after loading into ram
branch_to_application(LOAD_ADDRESS,something,ml);

.globl branch_to_application
branch_to_application:
   bx r0

luego usa la estructura correspondiente en la aplicación, conecta r2 a la estructura en la aplicación de alguna manera (pasa r2 a tu punto de entrada C, hazlo en el arranque, etc.)

y luego

something=ml.hello(3);

Otra solución similar es pasar la estructura al cargador de arranque, por ejemplo, una dirección pasada en un registro a la aplicación puede ser la función a la que pasas tu estructura, luego el cargador de arranque completa esa estructura. O puedes hacerlo en un ascii de moda, esa dirección podría ser una función en el cargador de arranque y la llama una vez para cada función a la que desea que la dirección ... lo que sea.

Tenga en cuenta que estoy seguro de que puede descubrir cómo hacerlo sin estructuras y reducir el riesgo si no eliminar ese riesgo, pero obtener los mismos resultados.

Le dejaré ordenar la sintaxis ya que no es relevante para esta respuesta. Ocurre mágicamente en un sistema operativo con C u otros lenguajes al llamar a esas bibliotecas porque las herramientas, la biblioteca, el sistema operativo, están diseñados para resolver este problema de bibliotecas compartidas (una llamada binaria funciona en otra binaria). Para bare metal, usted tiene que hacer el trabajo usted mismo, las herramientas no conocen su "sistema operativo", por lo que de alguna manera debe hacérselo saber. Que como con una biblioteca C, alguien se sienta y diseña una solución para conectar un binario a las funciones en otro. Y finalmente se reduce a que en algún momento antes de usar la función remota/compartida, debe señalarla, y/o en la versión auxiliar de la función en su código, busca la dirección de la función y luego se bifurca. .

Ahora a su pregunta, explique cómo el cargador de arranque ejecutará algo en una aplicación. Normalmente, colocaría la impresión en el cargador de arranque y la aplicación, una vez ejecutada, la llamará. Si desea que el cargador de arranque llame al código en la aplicación, funcionará, pero es muy extraño. Eso significa que el gestor de arranque no puede imprimir hasta que carga la aplicación, hace el trabajo de encontrar el puntero a la función y luego llamarla. Al revés, se obtiene un cargador de arranque más grande, sí, pero aplicaciones más pequeñas y menos espacio flash consumido, ya que solo necesita una función de impresión en un lugar (el cargador de arranque) y solo necesita stubs en las aplicaciones o una estructura o lo que sea.

En general, esto tiene ventajas y desventajas, las desventajas son que es posible que tenga un producto que haya desarrollado el cargador de arranque con el tiempo y que los clientes tengan una versión del producto con un cargador de arranque de 5 años, y que tenga una nueva aplicación para ese producto y necesita funcionar de alguna manera con el gestor de arranque de 5 años, el gestor de arranque de 4 años, el gestor de arranque de 3,5 años, el gestor de arranque de 3 años... te haces una idea. ¿Es tan importante ahorrar flash y ram? ¿Cuánto estás ahorrando? Después de dos o tres años te diste cuenta de que deberías haber tenido más funciones compartidas ahora todos los nuevos binarios tienen que llevar las nuevas funciones quemando ram y flash. ¿Actualiza el gestor de arranque en el campo, lo convierte en una elección del cliente (y se queda atascado teniendo que admitir todas las versiones lanzadas anteriormente para cada nueva aplicación). ¿Cómo se asegura de no bloquear el producto cuando realiza la actualización de campo? Tal vez si escribe un código más limpio y optimiza mejor, puede ahorrar cientos o miles de bytes, y tal vez eso sea suficiente en todos los binarios que no tienen. para compartir bibliotecas. Tal vez la biblioteca compartida sea en sí misma una aplicación cargable, el gestor de arranque ha incorporado solo lo que necesita, pero carga la biblioteca compartida desde el flash a algún lugar, luego carga la aplicación en algún lugar y le da a la aplicación un puntero a la biblioteca compartida en el lanzamiento . Y cuando actualiza/instala el nuevo conjunto de aplicaciones, actualiza la biblioteca compartida. Tal vez la biblioteca compartida sea en sí misma una aplicación cargable, el gestor de arranque ha incorporado solo lo que necesita, pero carga la biblioteca compartida desde el flash a algún lugar, luego carga la aplicación en algún lugar y le da a la aplicación un puntero a la biblioteca compartida en el lanzamiento . Y cuando actualiza/instala el nuevo conjunto de aplicaciones, actualiza la biblioteca compartida. Tal vez la biblioteca compartida sea en sí misma una aplicación cargable, el gestor de arranque ha incorporado solo lo que necesita, pero carga la biblioteca compartida desde el flash a algún lugar, luego carga la aplicación en algún lugar y le da a la aplicación un puntero a la biblioteca compartida en el lanzamiento . Y cuando actualiza/instala el nuevo conjunto de aplicaciones, actualiza la biblioteca compartida.

En mi opinión, todo esto solo es realmente relevante si tiene múltiples aplicaciones posibles por cargador de arranque. Si solo tiene un cargador de arranque y una aplicación y los cargadores de arranque están ahí para que el usuario pueda actualizar la aplicación en el campo, entonces solo necesita lo suficiente para hacerlo y la aplicación tiene todo lo que necesita, sí, es posible que tenga una superposición desperdiciada.

Si este es un caso de cargador de arranque primario y redundante junto con una aplicación primaria y redundante, de modo que pueda realizar actualizaciones de campo de cualquiera con menos riesgo de bloquear el producto. Ya estás quemando mucho flash, ¿cuánto estás ahorrando realmente?

En pocas palabras, debido a que esto es (se supone que es) metal desnudo en un mcu, debe diseñar el mecanismo usted mismo. Descubra las direcciones de función en el otro binario y apúntelas en el binario que se está ejecutando actualmente. O lo hace en un código auxiliar/envoltorio para cada función (muy derrochador) o crea una inicialización ideal única, antes de usar cualquier función remota, que tiene una tabla de direcciones en el lado remoto que llena la tabla/lista de punteros de función en el lado cercano. Entonces solo usas estas funciones. Tu nivel de paranoia/miedo/experiencia determinará qué tan simple o complicado quieres hacer esto. Obviamente, comience de manera simple con una sola función como experimento, complique a partir de ahí.

Otro punto a mencionar es que uno debe asignar permanentemente regiones separadas de almacenamiento estático para el cargador de arranque y el programa principal (o diseñar el cargador de arranque para que ninguna de las funciones compartidas requiera almacenamiento estático).
Gracias por responder a mi pregunta amigos.

Tuve que hacer esto una vez para ahorrar espacio flash/ROM. Había algunas funciones de controlador de hardware y CRC que podían compartirse entre el gestor de arranque y la aplicación. La respuesta básica es: cree un puntero de función, asigne el puntero de función a la dirección adecuada de la función que desea llamar, llame a la función a través del puntero de función. Puede ser creativo sobre cómo pasar la información de la dirección de la función a cada programa. La forma rápida y sucia: codifique las direcciones mirando la dirección de la función desde un archivo de mapa/lista generado por su compilador. Una forma más sofisticada: cree una "tabla de búsqueda de direcciones de funciones" que siempre se coloque en una dirección específica en la memoria (necesita usar directivas de archivos de vinculación para que esto suceda). De este modo, puede actualizar el cargador de arranque o la aplicación (lo que significa que las direcciones de las funciones pueden cambiar) y aún así ambos permanecen sincronizados. Probablemente necesitará acceder a alguna sintaxis específica del compilador para generar la tabla de búsqueda de direcciones de funciones.

¿Puedes usar un código de ejemplo?

Escribo este código para resolver el problema y lo probé. IDE: keil uvision v5.22

En el programa del cargador de arranque:

  //Shared_Func is a sample function for sharing between two app
   void Shared_Func(void)
  {
    while(1)
    {
        //do somethings
    }
}

typedef void (*ptr_shared_func_t)(void);//define new function pointer type
const ptr_shared_func_t ptr_const_shared_fun __attribute__((at(USER_APP_BASE_ADDRESS -4))) = Shared_Func;
//save address of Shared_Func in special address. To avoid non-interference between application code and address of shared function ,i defined that at (USER_APP_BASE_ADDRESS -4).

En el programa de aplicación:

typedef void (*ptr_shared_fun_t)(void);
ptr_shared_fun_t ptr_shared_fun;

en la función principal o en cualquier lugar que queramos llamar a Shared_Func:

unsigned int *add_shared_fun = (unsigned int *)(USER_APP_BASE_ADDRESS-4);
ptr_shared_fun = (void(*)(void))(*add_shared_fun);
ptr_shared_fun();//call SharedFunc with function pointer