tag standard library lib abs c embedded

standard - Variable de dirección fija en C



jstl prefix (10)

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é impide que el enlazador ponga 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 ubicarse en 0x100? Estoy usando las herramientas de GNU (gcc, ld, etc.), así 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 se conecta a un FPGA a través de un conjunto de registros mapeados en el espacio de datos regular (donde viven las variables) del procesador. Así que necesito apuntar a esos registros y bloquear el espacio de direcciones asociado. En el pasado, he usado un compilador que tenía una extensión para ubicar variables de código C. Agruparía los registros en una estructura, luego colocaría la estructura en la ubicación apropiada:

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 extensión at . Usando el método de 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(); } }


A menudo, para el software integrado, puede definir dentro del archivo del vinculador un área de RAM para las variables asignadas al vinculador, y un área separada para las variables en ubicaciones absolutas, que el vinculador no tocará.

Si no lo hace, podría causar un error en el vinculador, ya que debería detectar que está intentando colocar una variable en una ubicación que ya está siendo utilizada por una variable con dirección absoluta.


Cuando el sistema operativo incorporado carga la aplicación en la memoria, generalmente la carga en una 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 esta es una variable global. Si es una variable local, se encuentra en la pila. Cuando hace referencia a 0x100, hace referencia al espacio de la 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 "espacio relativo a donde está mi código de programa".

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

¡Aclamaciones!


Esto depende un poco de qué sistema operativo está utilizando. Supongo que está utilizando algo como DOS o vxWorks. En general, el sistema tendrá áreas certian del espacio de memoria reservado para el hardware, y los compiladores para esa plataforma siempre serán lo suficientemente inteligentes como para evitar esas áreas para sus propias asignaciones. De lo contrario, estaría escribiendo continuamente basura aleatoria en el disco o impresoras de línea cuando pretendía acceder a las variables.

En caso de que algo más lo confunda, también debo señalar que #define es una directiva de preprocesador. Ningún código se genera para eso. Simplemente le dice al compilador que reemplace textualmente cualquier foo_reg que vea en su archivo fuente con *(int *)0x100 . No es diferente de solo escribir *(int *)0x100 en ti mismo en todos los lugares donde tenías foo_reg , aparte de que puede parecer más limpio.

Lo que probablemente haría en su lugar (en un compilador de C moderno) es:

// access register ''foo_reg'', which is located at address 0x100 const int* foo_reg = (int *)0x100; *foo_reg = 1; // write to foo_regint x = *foo_reg; // read from foo_reg


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


Obtener la cadena de herramientas de GCC para darle 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 C run time. Este es un módulo de ensamblaje (a menudo denominado 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 C ++ con objetos globales, etc. Las personalizaciones típicas incluyen la necesidad de configurar su hardware para abordar realmente la RAM (posiblemente incluyendo la configuración del controlador DRAM) para que haya un lugar para colocar datos y apilarlos. Algunas CPU deben tener estas cosas en una secuencia específica: por ejemplo, el ColdFire MCF5307 tiene una selección de chip que responde a cada dirección después del arranque, que finalmente debe configurarse para cubrir solo el área del mapa de memoria planificado 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 está en varias direcciones. ROM en 0x00000000, RAM en 0x10000000, registros de dispositivo en 0xD0000000, etc. En algunos procesadores, el equipo de hardware solo pudo conectar una selección de chip desde la CPU a un dispositivo, y dejar que usted decida qué disparadores de dirección seleccionan el pin. .

  3. GNU ld es compatible con un lenguaje de script de vinculador 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á la secuencia de comandos del vinculador, ya que gcc suministra un stock que se ajusta 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 tiene 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 lo harás. Pero aún necesita una imagen binaria sin procesar (a menudo en un formato HEX adecuado para un programador de PROM de alguna forma), por lo que deberá usar la utilidad objcopy de binutil para transformar la salida del enlazador a un formato adecuado.

Por lo tanto, para responder a la pregunta real que hizo ...

Utiliza una secuencia de comandos 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 involucran colocar los segmentos de texto, datos, pila bss y montón en intervalos de direcciones que evitan los registros de hardware. También hay mecanismos disponibles que pueden asegurar que ld arroje un error si sobrellena su ROM o RAM, y también debería usarlos.

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

Aunque es posible usar el atributo de la section de GCC para definir una instancia de una struct no inicializada que se encuentra 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 utiliza esa técnica, la secuencia de comandos del vinculador sería la responsable de asignar FPGA_REGS a su dirección correcta.

En cualquier caso, necesitará obtener una buena comprensión de 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, las prioridades de interrupción, los modos de supervisor contra usuario (o suena 0 a 3 en las variantes x86) y similares.


Para ampliar la respuesta de litb, también puede usar la --just-symbols= {symbolfile} para definir varios símbolos, en caso de que tenga más de un par de dispositivos asignados en memoria. El archivo de símbolos debe estar en el formato

symbolname1 = address; symbolname2 = address; ...

(Los espacios alrededor del signo igual parecen ser requeridos.)


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 mapeado IO en la mayoría de las arquitecturas. No tiene conocimiento de que lo esté utilizando para almacenar valores, simplemente sabe que las variables normales no deberían ir allí. Muchos compiladores integrados admiten extensiones de lenguaje que le permiten declarar variables y funciones en ubicaciones específicas, generalmente utilizando #pragma . Además, en general, la forma en que he visto a la gente implementar el tipo de mapeo de memoria que está tratando de hacer es declarar un int en la ubicación de memoria deseada, y luego tratarla como una variable global. Alternativamente, puede declarar un puntero a un int e inicializarlo a esa dirección. Ambos proporcionan más seguridad de tipo que una macro.


Su enlazador maneja la colocación de datos y variables. Conoce su sistema de destino a través de un script de enlace. La secuencia de comandos del vinculador define regiones en un diseño de memoria como .text (para datos y código constantes) 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 encargado del script del vinculador asegurarse de que las secciones utilizables por el vinculador no anulen sus direcciones IO.


Tu enlazador y compilador no saben sobre eso (sin que le digas nada, por supuesto). Depende del diseñador de la ABI de su plataforma especificar que no asignan objetos a esas direcciones.

Entonces, a veces hay (la plataforma en la que trabajé tenía) un rango en el espacio de direcciones virtuales que se asigna directamente a direcciones físicas y otro rango que los procesos de espacio de usuario pueden usar para aumentar la pila o asignar memoria de almacenamiento dinámico.

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

--defsym symbol=expression

O si la expresión es más complicada que la aritmética simple, use un script de enlace personalizado. Ese es el lugar donde puede definir las regiones de la memoria y decirle al vinculador qué regiones deben darse a qué secciones / objetos. Vea here 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 los scripts del enlazador y los back-ends del ensamblador / compilador que cumplen los requisitos de su plataforma.

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

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


Un enlazador usualmente usaría un script de vinculador para determinar dónde se asignarían las variables. Esto se llama la sección de "datos" y, por supuesto, debería 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 vinculador en GCC here .