2009-03-25 18 views
15

Para aplicaciones integradas, a menudo es necesario acceder a ubicaciones de memoria fija para registros periféricos. La manera estándar que he encontrado para hacer esto es algo como lo siguiente:Variable de dirección fija en C

// 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, pero lo que no entiendo es cómo se asigna el espacio para foo_reg (es decir, lo que mantiene el enlazador poniendo otra variable en 0x100?). ¿Se puede reservar el espacio en el nivel C, o tiene que haber una opción de enlazador 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 cuestión:

Mis interfaces de procesador 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 debo señalar 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 desde el código C. Me agrupar los registros en una estructura, a continuación, colocar la estructura en el lugar adecuado:

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

Registers regs _at_ 0x100; 

regs.reg1 = 0; 

creación de una estructura realidad 'Registros' se reserva el espacio en los ojos del compilador/enlazador de.

Ahora, usando las herramientas GNU, obviamente no tengo el en la extensión. Uso del 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 ni administración de memoria avanzada. Básicamente:

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

Respuesta

12

Su enlazador y compilador no lo saben (sin que usted lo cuente, por supuesto). Depende del diseñador de la ABI de su plataforma especificar que no asignen objetos a esas direcciones. Entonces, a veces (la plataforma en la que trabajé tenía eso) un rango en el espacio de direcciones virtuales que se asigna directamente a direcciones físicas y otro rango que puede usarse por procesos de espacio de usuario para aumentar la pila o asignar memoria de montón

Puede utilizar la opción defsym con GNU ld para asignar un símbolo a una dirección fija:

--defsym symbol=expression 

O si la expresión es más complicada que la simple aritmética, utilizar un script enlazador personalizado. Ese es el lugar donde puede definir las regiones de la memoria y decirle al vinculador qué regiones deberían asignarse a qué secciones/objetos. Ver 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 ABI y luego escriben scripts de enlazador y backends de ensamblador/compilador que cumplen con los requisitos de su plataforma.

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

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

En realidad, el enlazador sí lo hace, a través de una secuencia de comandos del 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. – strager

+0

de hecho, en la intención de decir sin hacer nada, no hay posibilidad de que lo sepa :) –

+0

¿'defsym' realmente asigna algo a una dirección determinada? AFAIK es esencialmente un linker equivalente a '# define', ni siquiera se puede especificar el tamaño del objeto con él, y no impedirá que el enlazador coloque otra variable en la misma dirección. –

1

Normalmente, estas direcciones quedan fuera del alcance de su proceso. Por lo tanto, su enlazador no se atrevería a poner cosas allí.

+0

Mi procesador se conecta a un FPGA a través de un conjunto de registros mapeados en el espacio de datos normal del procesador. Así que debo señalar esos registros y bloquear el espacio de direcciones asociado. –

+0

¿Realmente se ha encontrado con una situación en la que hay una superposición? El código de objeto contiene principalmente direcciones reubicables, y se asignan al espacio de proceso del usuario en tiempo de ejecución. – dirkgently

1

Si la ubicación de la memoria tiene un significado especial en su arquitectura, el compilador debe saber eso y no poner ninguna variable allí. Eso sería similar al espacio asignado IO en la mayoría de las arquitecturas. No tiene conocimiento de que lo está usando para almacenar valores, simplemente sabe que las variables normales no deberían ir allí. Muchos compiladores incorporados admiten extensiones de idioma 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ás tratando de hacer es declarar un int en la ubicación de memoria deseada, y luego simplemente 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 tipo que una macro.

3

Cuando el sistema operativo incorporado carga la aplicación en la memoria, la cargará normalmente 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 + código tamaño + 4 ... suponiendo 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 vinculador no colocará el código en ubicaciones de memoria específicas, funciona en "relativo a donde mi código de programa es 'espacio de memoria.

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

¡Salud!

+0

Esto solo es cierto para [código independiente de posición] (https://en.wikipedia.org/wiki/Position-independent_code) que puede o no puede usarse en un sistema incorporado particular. La mayoría de tales sistemas ni siquiera tienen el sistema operativo, el código solo se encuentra en la ROM y no hay ninguna razón para hacerlo independiente de la posición. –

9

Un vinculador 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 asignar una variable a una dirección que no esté en la RAM.

Puede leer más sobre los scripts del enlazador en GCC here.

5

Su vinculador maneja la ubicación de datos y variables. Conoce su sistema de destino a través de un script del enlazador. El script del enlazador define regiones en memory layout 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 física y virtual (si es necesaria). El mantenedor del script del enlazador debe asegurarse de que las secciones utilizables por el vinculador no anulen sus direcciones IO.

0

Esto depende un poco del sistema operativo que está utilizando. Supongo que estás usando 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 serán siempre sean lo suficientemente inteligentes como para evitar esas áreas para sus propias asignaciones. De lo contrario, estarías continuamente escribiendo basura al azar en el disco o impresoras de línea cuando quisieras acceder a las variables.

En caso de que algo más te haya confundido, también debo señalar que #define es una directiva de preprocesador. No se genera código para eso. Simplemente le dice al compilador que reemplace textualmente cualquier foo_reg que ve en su archivo fuente con *(int *)0x100. No es diferente a simplemente escribir *(int *)0x100 en ti mismo en todos los lugares donde tengas foo_reg, aparte de que puede parecer más limpio.

Lo que probablemente haría en su lugar (en un compilador de C moderna) 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 
1

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

symbolname1 = address; 
symbolname2 = address; 
... 

(Los espacios alrededor del signo igual, parecen ser necesarios.)

3

Conseguir la cadena de herramientas GCC para darle una imagen adecuada para su uso directamente en el hardware sin un sistema operativo para cargarlo es posible, pero implica un par de pasos que normalmente no son necesarios para los programas normales.

  1. Es casi seguro que tendrá que personalizar el módulo de inicio de tiempo de ejecución de C. Este es un módulo de ensamblaje (a menudo llamado crt0.s) que es responsable de inicializar los datos inicializados, borrar BSS, llamar 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 en realidad se dirige a la memoria RAM (posiblemente incluyendo la configuración del controlador DRAM también) para que haya un lugar donde poner datos y apilar. Algunas CPU necesitan hacer estas cosas en una secuencia específica: p. 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 puesto, 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 haber conectado una selección de chip de la CPU a un dispositivo y dejar que usted decida qué dirección activa esa selección .

  3. GNU ld admite un lenguaje de script de enlazador 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 enlazador, ya que una fuente es suministrada por gcc que está sintonizada con las suposiciones de su sistema operativo para una aplicación normal.

  4. La salida del vinculador 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 cargar dinámicamente algunas bibliotecas. En un sistema ROM, la carga dinámica (generalmente) no es compatible, por lo que no lo hará. Pero aún necesita una imagen binaria en bruto (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 vinculador a un formato adecuado.

lo tanto, para responder a la pregunta real que pedirá ...

Se utiliza un guión de enlazado 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 tratar con registros de dispositivos, pero todas ellas implican colocar los segmentos de texto, datos, pila bss y montón en rangos de direcciones que evitan los registros de hardware. También hay mecanismos disponibles que pueden asegurar que ld arroje un error si usted llena en exceso su ROM o RAM, y debe usarlos también.

conseguir En realidad las direcciones de los dispositivos en el código C se puede hacer con #define como en el ejemplo, o declarando un símbolo directamente en el guión enlazador que se resuelve a la dirección base de los registros, con un extern declaración coincidente en un archivo de encabezado C

Aunque es posible utilizar el atributo de GCC section para definir una instancia de un sin inicializar struct como siendo situado en una sección específica (tales como FPGA_REGS), he encontrado que no funcione 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 el chip. Si usa esa técnica, el script 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 archivo de objeto como "secciones" (específicamente las secciones de texto, datos y bss como mínimo), y puede que tenga que buscar los detalles de ese puente la brecha entre el hardware y el software, como la tabla de vectores de interrupción, las prioridades de interrupción, los modos supervisor versus usuario (o los anillos 0 a 3 en las variantes x86) y similares.

1

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

Si no se hace esto, se debe producir un error del enlazador, ya que debe detectar que está tratando de colocar una variable en una ubicación que ya está siendo utilizada por una variable con una dirección absoluta.

Cuestiones relacionadas