Fixed address variable in C

Per le applicazioni integrate, è spesso necessario accedere alle posizioni di memoria fissa per i registri delle periferiche. Il modo standard che ho trovato per fare questo è qualcosa di simile al seguente:

// 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 

Capisco come funziona, ma quello che non capisco è come viene allocato lo spazio per foo_reg (cioè cosa impedisce al linker di inserire un’altra variabile a 0x100?). Lo spazio può essere prenotato a livello di C o deve essere presente un’opzione di linker che specifica che nulla deve trovarsi a 0x100. Sto usando gli strumenti GNU (gcc, ld, ecc.), Quindi sono per lo più interessato alle specifiche di tale toolset al momento.

Alcune informazioni aggiuntive sulla mia architettura per chiarire la domanda:

Il mio processore si interfaccia con un FPGA tramite un set di registri mappati nello spazio dati regolare (dove vivono le variabili) del processore. Quindi ho bisogno di indicare quei registri e bloccare lo spazio degli indirizzi associato. In passato, ho usato un compilatore che aveva un’estensione per localizzare le variabili dal codice C. Vorrei raggruppare i registri in una struttura, quindi posizionare la struttura nella posizione appropriata:

 typedef struct { BYTE reg1; BYTE reg2; ... } Registers; Registers regs _at_ 0x100; regs.reg1 = 0; 

In realtà la creazione di una struttura ‘Registri’ riserva lo spazio negli occhi del compilatore / linker.

Ora, usando gli strumenti GNU, ovviamente non ho l’estensione. Utilizzando il metodo del puntatore:

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

Questa è una semplice applicazione senza OS e nessuna gestione avanzata della memoria. Essenzialmente:

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

Il tuo linker e il compilatore non ne sono a conoscenza (senza che tu dica nulla, ovviamente). Spetta al progettista dell’ABI della tua piattaforma specificare che non assegnano oggetti a tali indirizzi.

Quindi, a volte c’è (la piattaforma su cui ho lavorato) un intervallo nello spazio degli indirizzi virtuali che è mappato direttamente agli indirizzi fisici e un altro intervallo che può essere usato dai processi dello spazio utente per far crescere lo stack o allocare memoria heap.

È ansible utilizzare l’opzione defsym con GNU ld per allocare un simbolo ad un indirizzo fisso:

 --defsym symbol=expression 

O se l’espressione è più complicata della semplice aritmetica, usa uno script linker personalizzato. Questo è il posto in cui è ansible definire le regioni di memoria e indicare al linker quali regioni dovrebbero essere date a quali sezioni / oggetti. Vedi qui per una spiegazione. Anche se questo di solito è esattamente il compito dello scrittore della catena di strumenti che usi. Prendono le specifiche dell’ABI e quindi scrivono script di linker e back-end assemblatore / compilatore che soddisfano i requisiti della piattaforma.

Per inciso, GCC ha una section attributo che è ansible utilizzare per posizionare la struttura in una sezione specifica. Potresti quindi dire al linker di posizionare quella sezione nella regione in cui vivono i tuoi registri.

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

Un linker utilizza in genere uno script linker per determinare dove verranno allocate le variabili. Questa è chiamata la sezione “dati” e, naturalmente, dovrebbe indicare una posizione RAM. Pertanto è imansible assegnare una variabile a un indirizzo non in RAM.

Puoi leggere ulteriori informazioni sugli script linker in GCC qui .

Il tuo linker gestisce il posizionamento di dati e variabili. Conosce il tuo sistema di destinazione attraverso uno script linker. Lo script linker definisce le regioni in un layout di memoria come .text (per dati costanti e codice) e .bss (per le variabili globali e l’heap) e crea anche una correlazione tra un indirizzo virtuale e fisico (se necessario) . È compito del manutentore dello script linker assicurarsi che le sezioni utilizzabili dal linker non sovrascrivano i tuoi indirizzi IO.

Quando il sistema operativo incorporato carica l’applicazione in memoria, la carica in genere in una posizione specifica, diciamo 0x5000. Tutta la memoria locale che stai usando sarà relativa a quell’indirizzo, cioè int x sarà da qualche parte come 0x5000 + dimensione del codice + 4 … assumendo che questa sia una variabile globale. Se si tratta di una variabile locale, si trova in pila. Quando si fa riferimento a 0x100, si fa riferimento allo spazio di memoria del sistema, allo stesso spazio che il sistema operativo è responsabile della gestione e probabilmente a un luogo specifico monitorato.

Il linker non colloca il codice in posizioni di memoria specifiche, funziona in ‘relativo a dove il mio codice di programma è’ spazio di memoria.

Questo si rompe un po ‘quando entri nella memoria virtuale, ma per i sistemi embedded, questo tende a essere vero.

Saluti!

Ottenere la toolchain GCC per darti un’immagine adatta per l’uso direttamente sull’hardware senza un SO per caricarlo è ansible, ma comporta un paio di passaggi che normalmente non sono necessari per i programmi normali.

  1. Sarà quasi certamente necessario personalizzare il modulo di avvio del tempo di esecuzione C. Questo è un modulo assembly (spesso chiamato qualcosa come crt0.s ) che è responsabile di inizializzare i dati inizializzati, cancellare il BSS, chiamare i costruttori per gli oggetti globali se sono inclusi i moduli C ++ con oggetti globali, ecc. Le personalizzazioni tipiche includono la necessità di hardware per indirizzare effettivamente la RAM (possibilmente includendo anche l’impostazione del controller DRAM) in modo che ci sia un posto dove mettere i dati e impilare. Alcune CPU devono eseguire queste operazioni in una sequenza specifica: ad es. ColdFire MCF5307 ha una selezione di chip che risponde a ogni indirizzo dopo l’avvio, che alla fine deve essere configurato per coprire solo l’area della mappa di memoria pianificata per il chip collegato.

  2. Il tuo team hardware (o te con un altro cappello acceso, forse) dovrebbe avere una mappa di memoria che documenta ciò che è a vari indirizzi. ROM a 0x00000000, RAM a 0x10000000, registri di dispositivo su 0xD0000000, ecc. In alcuni processori, il team dell’hardware poteva solo colbind un chip select dalla CPU a un dispositivo e lasciare a te decidere quali trigger di indirizzo selezionare pin .

  3. GNU ld supporta un linguaggio di script linker molto flessibile che consente di collocare le varie sezioni dell’immagine eseguibile in spazi di indirizzi specifici. Per la normale programmazione, non si vede mai lo script linker dal momento che uno stock è fornito da gcc che è sintonizzato sulle ipotesi del sistema operativo per una normale applicazione.

  4. L’output del linker è in un formato rilocabile che è destinato a essere caricato nella RAM da un sistema operativo. Probabilmente ha correzioni di riposizionamento che devono essere completate e possono anche caricare dynamicmente alcune librerie. In un sistema ROM, il caricamento dinamico è (di solito) non supportato, quindi non lo farai. Ma hai ancora bisogno di un’immagine binaria grezza (spesso in un formato HEX adatto per un programmatore PROM di qualche forma), quindi dovrai usare l’utilità objcopy di binutil per trasformare l’output del linker in un formato adatto.

Quindi, per rispondere alla domanda vera che hai chiesto …

Si utilizza uno script linker per specificare gli indirizzi di destinazione di ciascuna sezione dell’immagine del programma. In questo script, hai a disposizione diverse opzioni per gestire i registri dei dispositivi, ma tutti implicano l’inserimento di segmenti di testo, dati, stack bss e heap in intervalli di indirizzi che evitano i registri hardware. Ci sono anche meccanismi disponibili che possono assicurarsi che ld genera un errore se si riempie troppo la ROM o la RAM e si dovrebbero usare anche quelli.

Realmente ottenere gli indirizzi del dispositivo nel tuo codice C può essere fatto con #define come nell’esempio, o dichiarando un simbolo direttamente nello script del linker che viene risolto nell’indirizzo di base dei registri, con una dichiarazione extern corrispondente in un’intestazione C file.

Sebbene sia ansible utilizzare l’attributo della section di GCC per definire un’istanza di una struct non inizializzata che si trova in una sezione specifica (come FPGA_REGS ), ho trovato che non funziona bene nei sistemi reali. Può creare problemi di manutenzione e diventa un modo costoso per descrivere la mappa del registro completo dei dispositivi su chip. Se si utilizza questa tecnica, lo script del linker sarebbe quindi responsabile della mapping di FPGA_REGS al suo indirizzo corretto.

In ogni caso, sarà necessario avere una buona comprensione dei concetti di file object come “sezioni” (in particolare le sezioni di testo, dati e bss come minimo) e potrebbe essere necessario scovare i dettagli che colmano il divario tra l’hardware e software come la tabella vettoriale di interrupt, priorità di interruzione, modalità supervisore vs. utente (o anelli da 0 a 3 su varianti x86) e simili.

In genere questi indirizzi sono fuori dalla portata del tuo processo. Quindi, il tuo linker non oserebbe mettere lì le cose.

Se la posizione della memoria ha un significato speciale sulla tua architettura, il compilatore dovrebbe saperlo e non inserire alcuna variabile lì. Sarebbe simile allo spazio mappato IO sulla maggior parte delle architetture. Non sa che lo stai usando per archiviare valori, semplicemente sa che le variabili normali non dovrebbero andare lì. Molti compilatori incorporati supportano estensioni linguistiche che consentono di dichiarare variabili e funzioni in posizioni specifiche, in genere utilizzando #pragma . Inoltre, generalmente il modo in cui ho visto le persone implementare il tipo di mapping della memoria che stai cercando di fare è dichiarare un int alla posizione di memoria desiderata, quindi considerarlo come una variabile globale. In alternativa, è ansible dichiarare un puntatore a un int e inizializzarlo su quell’indirizzo. Entrambi offrono più sicurezza di tipo rispetto a una macro.

Per espandere la risposta di litb, puoi anche usare l’ --just-symbols= {symbolfile} per definire diversi simboli, nel caso tu abbia più di un paio di dispositivi mappati in memoria. Il file di simboli deve essere nel formato

 symbolname1 = address; symbolname2 = address; ... 

(Gli spazi attorno al segno di uguale sembrano essere obbligatori).

Spesso, per il software incorporato, è ansible definire all’interno del file linker un’area di RAM per le variabili assegnate al linker e un’area separata per le variabili in posizioni assolute, che il linker non toccherà.

Non riuscendo a farlo dovrebbe causare un errore del linker, poiché dovrebbe rilevare che sta tentando di posizionare una variabile in una posizione già utilizzata da una variabile con indirizzo assoluto.

Questo dipende un po ‘dal sistema operativo in uso. Suppongo che tu stia usando qualcosa come DOS o vxWorks. Generalmente il sistema avrà aree certian dello spazio di memoria riservato per l’hardware, ei compilatori per quella piattaforma saranno sempre abbastanza intelligenti da evitare quelle aree per le proprie allocazioni. Altrimenti si scriverebbe continuamente dati casuali su stampanti disco o linea quando si intendeva accedere alle variabili.

Nel caso in cui qualcun altro ti stesse confondendo, dovrei anche sottolineare che #define è una direttiva per il preprocessore. Nessun codice viene generato per questo. Semplicemente dice al compilatore di sostituire testualmente qualsiasi foo_reg che vede nel tuo file sorgente con *(int *)0x100 . Non è diverso dal digitare *(int *)0x100 in te stesso ovunque tu abbia foo_reg , a parte il fatto che potrebbe sembrare più pulito.

Quello che probabilmente farei invece (in un moderno compilatore C) è:

 // 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