Variable de dirección fija en C

10 minutos de lectura

Para aplicaciones integradas, a menudo es necesario acceder a ubicaciones de memoria fijas para registros periféricos. La forma estándar que he encontrado para hacer esto es algo como lo siguiente:

// access register 'foo_reg', which is located at address 0x100
#define foo_reg *(int *)0x100

foo_reg = 1;      // write to foo_reg
int x = foo_reg;  // read from foo_reg

Entiendo cómo funciona eso, pero lo que no entiendo es cómo se asigna el espacio para foo_reg (es decir, ¿qué evita que el enlazador coloque otra variable en 0x100?). ¿Se puede reservar el espacio en el nivel C o tiene que haber una opción de enlace que especifique que nada debe estar ubicado en 0x100? Estoy usando las herramientas GNU (gcc, ld, etc.), por lo que estoy más interesado en los detalles de ese conjunto de herramientas en este momento.

Alguna información adicional sobre mi arquitectura para aclarar la pregunta:

Mi procesador interactúa con un FPGA a través de un conjunto de registros asignados al espacio de datos regular (donde viven las variables) del procesador. Entonces necesito señalar esos registros y bloquear el espacio de direcciones asociado. En el pasado, usé un compilador que tenía una extensión para ubicar variables del código C. Agruparía los registros en una estructura, luego colocaría la estructura en la ubicación adecuada:

typedef struct
{ 
   BYTE reg1;
   BYTE reg2;
   ...
} Registers;

Registers regs _at_ 0x100;

regs.reg1 = 0;

En realidad, la creación de una estructura de ‘Registros’ reserva el espacio a los ojos del compilador/enlazador.

Ahora, usando las herramientas GNU, obviamente no tengo la en extensión. Usando el método del puntero:

#define reg1 *(BYTE*)0x100;
#define reg2 *(BYTE*)0x101;
reg1 = 0

// or
#define regs *(Registers*)0x100
regs->reg1 = 0;

Esta es una aplicación simple sin sistema operativo y sin administración avanzada de memoria. Esencialmente:

void main()
{
    while(1){
        do_stuff();
    }
}

avatar de usuario
Johannes Schaub – litb

Su enlazador y compilador no saben nada de eso (sin que usted le diga nada, por supuesto). Depende del diseñador de la ABI de su plataforma especificar que no asignan objetos en esas direcciones.

Entonces, a veces (la plataforma en la que trabajé tenía eso) hay un rango en el espacio de direcciones virtuales que se asigna directamente a las direcciones físicas y otro rango que los procesos del espacio de usuario pueden usar para hacer crecer la pila o para asignar memoria en montón.

Puede usar la opción defsym con GNU ld para asignar algún símbolo en una dirección fija:

--defsym symbol=expression

O si la expresión es más complicada que la simple aritmética, use un script de enlace personalizado. Ese es el lugar donde puede definir regiones de memoria y decirle al enlazador qué regiones deben asignarse a qué secciones/objetos. Ver aquí para una explicación. Aunque ese suele ser exactamente el trabajo del escritor de la cadena de herramientas que utiliza. Toman la especificación de la ABI y luego escriben secuencias de comandos de vinculación y back-end de ensamblador/compilador que cumplen con los requisitos de su plataforma.

Por cierto, GCC tiene un atributo section que puede usar para colocar su estructura en una sección específica. Luego podría decirle al enlazador que coloque esa sección en la región donde viven sus registros.

Registers regs __attribute__((section("REGS")));

  • En realidad, el enlazador lo hace, a través de un script de enlazador que define las regiones de memoria disponibles para su uso. Define (por ejemplo) regiones .text y .bss para datos constantes y datos volátiles (RAM), respectivamente.

    – extraño

    25 de marzo de 2009 a las 18:00

  • de hecho 🙂 quería decir sin hacer nada, no hay posibilidad de que sepa sobre eso 🙂

    – Johannes Schaub – litb

    25 de marzo de 2009 a las 18:02

  • Lo hace defsym realmente asignar nada en absoluto en una dirección determinada? AFAIK es esencialmente un enlazador equivalente a #defineni siquiera puede especificar el tamaño del objeto con él, y no evitará que el vinculador coloque otra variable en la misma dirección.

    – Dmitri Grigoriev

    23 de junio de 2015 a las 13:26


  • también es posible decir en el script del enlazador “coloque este(s) símbolo(s) en esa área, o esa dirección”, pero no recuerdo cómo.

    – kyb

    24/10/2017 a las 14:00

Un enlazador normalmente usaría un script de enlazador para determinar dónde se asignarían las variables. Esto se llama la sección de “datos” y, por supuesto, debe apuntar a una ubicación de RAM. Por lo tanto, es imposible que una variable se asigne a una dirección que no esté en la RAM.

Puede leer más sobre los scripts de vinculación en GCC aquí.

avatar de usuario
extraño

Su enlazador maneja la colocación de datos y variables. Conoce su sistema de destino a través de un script de enlace. El script del enlazador define regiones en un diseño de memoria tal como .text (para datos constantes y código) y .bss (para sus variables globales y el montón), y también crea una correlación entre una dirección virtual y física (si es necesario). Es el trabajo del mantenedor de la secuencia de comandos del enlazador asegurarse de que las secciones utilizables por el enlazador no anulen sus direcciones IO.

Cuando el sistema operativo incorporado carga la aplicación en la memoria, generalmente la cargará en alguna ubicación específica, digamos 0x5000. Toda la memoria local que esté utilizando será relativa a esa dirección, es decir, int x estará en algún lugar como 0x5000+tamaño de código+4… asumiendo que se trata de una variable global. Si es una variable local, está ubicada en la pila. Cuando hace referencia a 0x100, está haciendo referencia al espacio de memoria del sistema, el mismo espacio que el sistema operativo es responsable de administrar, y probablemente un lugar muy específico que supervisa.

El enlazador no colocará el código en ubicaciones de memoria específicas, funciona en el espacio de memoria ‘relativo a donde está el código de mi programa’.

Esto se descompone un poco cuando ingresa a la memoria virtual, pero para los sistemas integrados, esto tiende a ser cierto.

¡Salud!

Obtener la cadena de herramientas de GCC para brindarle una imagen adecuada para usar directamente en el hardware sin un sistema operativo para cargarla es posible, pero implica un par de pasos que normalmente no son necesarios para los programas normales.

  1. Es casi seguro que necesitará personalizar el módulo de inicio de tiempo de ejecución de C. Este es un módulo de ensamblaje (a menudo llamado algo así como crt0.s) que es responsable de inicializar los datos inicializados, borrar el BSS, llamar a los constructores para objetos globales si se incluyen módulos de C++ con objetos globales, etc. controlador también) para que haya un lugar para colocar datos y apilar. Algunas CPU necesitan hacer estas cosas en una secuencia específica: por ejemplo, ColdFire MCF5307 tiene una selección de chip que responde a cada dirección después del arranque, que eventualmente debe configurarse para cubrir solo el área del mapa de memoria planeado para el chip adjunto.

  2. Su equipo de hardware (o usted con otro sombrero, posiblemente) debe tener un mapa de memoria que documente lo que hay en varias direcciones. ROM en 0x00000000, RAM en 0x10000000, registros de dispositivo en 0xD0000000, etc. En algunos procesadores, el equipo de hardware podría haber conectado solo un chip seleccionado de la CPU a un dispositivo, y dejar que usted decida qué dirección activa ese pin de selección .

  3. GNU ld admite un lenguaje de secuencias de comandos de vinculación muy flexible que permite que las diversas secciones de la imagen ejecutable se coloquen en espacios de direcciones específicos. Para la programación normal, nunca verá el script del enlazador, ya que gcc proporciona uno estándar que está ajustado a las suposiciones de su sistema operativo para una aplicación normal.

  4. La salida del enlazador está en un formato reubicable que está destinado a ser cargado en la RAM por un sistema operativo. Probablemente tenga correcciones de reubicación que deben completarse e incluso puede cargar dinámicamente algunas bibliotecas. En un sistema ROM, la carga dinámica (generalmente) no es compatible, por lo que no hará eso. Pero aún necesita una imagen binaria sin procesar (a menudo en un formato HEX adecuado para un programador PROM de alguna forma), por lo que deberá usar la utilidad objcopy de binutil para transformar la salida del enlazador a un formato adecuado.

Entonces, para responder a la pregunta real que hiciste…

Utiliza un script de enlace para especificar las direcciones de destino de cada sección de la imagen de su programa. En esa secuencia de comandos, tiene varias opciones para manejar los registros de dispositivos, pero todas implican colocar el texto, los datos, la pila bss y los segmentos del montón en rangos de direcciones que evitan los registros de hardware. También hay mecanismos disponibles que pueden asegurarse de que ld arroje un error si sobrellena su ROM o RAM, y debería usarlos también.

En realidad, obtener las direcciones del dispositivo en su código C se puede hacer con #define como en su ejemplo, o declarando un símbolo directamente en el script del enlazador que se resuelve en la dirección base de los registros, con una coincidencia extern declaración en un archivo de encabezado C.

Aunque es posible usar GCC’s section atributo para definir una instancia de un no inicializado struct como estar ubicado en una sección específica (como FPGA_REGS), he descubierto que no funciona bien en sistemas reales. Puede crear problemas de mantenimiento y se convierte en una forma costosa de describir el mapa de registro completo de los dispositivos en chip. Si usa esa técnica, la secuencia de comandos del enlazador sería responsable de mapear FPGA_REGS a su dirección correcta.

En cualquier caso, necesitará obtener una buena comprensión de los conceptos de archivos de objetos como “secciones” (específicamente las secciones de texto, datos y bss como mínimo), y es posible que deba buscar detalles que cierren la brecha entre el hardware y software como la tabla de vectores de interrupción, prioridades de interrupción, modos de supervisor frente a usuario (o anillos 0 a 3 en variantes x86) y similares.

avatar de usuario
dirkgently

Por lo general, estas direcciones están fuera del alcance de su proceso. Entonces, su enlazador no se atrevería a poner cosas allí.

avatar de usuario
armador

Si la ubicación de la memoria tiene un significado especial en su arquitectura, el compilador debe saberlo y no colocar ninguna variable allí. Eso sería similar al espacio asignado de IO en la mayoría de las arquitecturas. No tiene conocimiento de que lo está utilizando para almacenar valores, solo sabe que las variables normales no deberían ir allí. Muchos compiladores incorporados admiten extensiones de lenguaje que le permiten declarar variables y funciones en ubicaciones específicas, generalmente usando #pragma. Además, en general, la forma en que he visto a las personas implementar el tipo de asignación de memoria que está tratando de hacer es declarar un int en la ubicación de memoria deseada, luego tratarlo como una variable global. Alternativamente, puede declarar un puntero a un int e inicializarlo en esa dirección. Ambos proporcionan más seguridad de tipos que una macro.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad