Dieser Artikel fasst einige Interna von avr-gcc zusammen.
Inhaltsverzeichnis
Ablauf der Codegenerierung
Die Code-Erzeugung durch avr-gcc geschieht in mehreren, voneinander unabhängigen Schritten. Diese Schritte sind für den Anwender nicht immer erkennbar, und es auch nicht unbedingt notwendig, sie zu kennen. Für ein besseres Verständnis der Code-Generierung und zur Einordnung von Fehlermeldungen und Warnungen ist eine Kenntnis aber durchaus hilfreich.
Übersichts-Grafik
Schritte der Codegenerierung
Ohne die Angabe spezieller Optionen legt avr-gcc die Zwischenformate nur als temporäre Dateien an, und nach Beenden des Compilers werden diese wieder gelöscht. Dadurch fällt die Aufgliederung in Unterschritte nicht auf. In diesem Falle müssen Assembler und Linker/Locator auch nicht extra aufgerufen werden, sondern die Aufrufe werden durch avr-gcc verwaltet.
- Precompileren
- Alle Preprozessor-Direktiven werden aufgelöst. Dazu gehören Direktiven wie
#include <avr/io.h> #include "meinzeug.h" #define MAKRONAME ERSATZTEXT #if !defined(__AVR__) #error einen Fehler ausgeben und abbrechen #else /* Alles klar, wir koennen loslegen mit C-Code fuer AVR */ #endif MAKRONAME
- Precompilieren besteht also nur aus reinem Textersatz: Auflösen von Makros, kopieren von anderen Dateien in die Quelle, etc.
- Compilieren
- In diesem Schritt geschieht der eigentliche Compilier-Vorgang: avr-gcc übersetzt die reine, precompilierte C-Quelle (*.i): Die Quelle wird auf Syntax-Fehler geprüft, es werden Optimierungen gemacht, und das übersetzte C-Programm als Assembler-Datei in (*.s) gespeichert.
- Assemblieren
- Der Assembler (avr-as) übersetzt den Assembler-Code (*.s) in das AVR-eigene Objektformat elf32-avr (*.o). Das Objekt enthält schon Maschinen-Code. Zusätzlich gibt es aber noch Lücken, die erst später gefüllt werden und Debug-Informationen und ganz viel anderes Zeug.
- Linken und Lokatieren
- Der Linker (avr-ld) bindet die angegebenen Objekte (*.o) zusammen und löst externe Referenzen auf. Der Linker entscheidet anhand der Beschreibung im Linker-Script, in welchen Speicheradressen und Sections die Daten landen: er lokatiert (von location, locate (en)). Module aus Bibliotheken (*.a) werden hinzugebunden (z.B. printf) und die elf32-avr Ausgabedatei (üblicherweise *.elf) erzeugt.
- Umwandeln ins gewünschte Objekt-Format
- Ohne Angabe spezieller Optionen erzeugen Linker und Assembler ihre Ausgabe im Objektformat elf32-avr. Wird ein anderes Objektformat wie Intel-HEX (*.hex), binary (*.bin) oder srec (*.srec) benötigt, kann avr-objcopy dazu verwendet werden, um diese zu erstellen. Der Inhalt einzelner Sections kann gezielt umkopiert oder ausgeblendet werden, so daß Dateien erstellt werden können, die nur den Inhalt des Flashs (Section .text) oder des EEPROMs (Section .eeprom) repräsentieren. Durch das Umwandeln in ein anderes Objektformat gehen üblicherweise Informationen wie Debug-Informationen verloren.
- Es ist auch möglich, den Linker mit der Optione --oformat=... zu starten, damit er direkt das gewünschte Ausgabeformat erzeugt. Diese Option lässt man von avr-gcc an den Linker weiterreichen, etwa wenn man direkt eine Intel-HEX-Datei erstellen will und keine elf-Datei braucht, wie sie z.b. beim Debuggen benötigt wird:
> avr-gcc ... -Wl,--oformat=ihex
Dabei ist dafür zu sorgen, daß keine Debug-Informationen etc. in der hex-Datei landen! Generell ist es vorzuziehen, die hex-Datei aus einer elf-Datei zu erzeugen.
Allgemeine Charakteristika von avr-gcc
- → avr-gcc: Application Binary Interface (ABI) im GCC Wiki
- Groß- und Kleinschreibung
- C unterscheidet generell zwischen Groß- und Kleinschreibung, sowohl bei Variablen- und Funktionsnamen, bei Sprungmarken als auch bei Makros, und je nach Betriebssystem auch bei Pfad- und Dateinamen/Dateierweiterungen.
- Größe des Typs int
- Der Standard-Typ int ist 16 Bit groß
- Größe von Pointern
- Ein Pointer (Zeiger) ist 16 Bit groß
- Endianess
- avr-gcc implementiert Datentypen als little-endian, d.h. bei Datentypen, die mehrere Bytes groß sind, wird das niederwertigste Byte an der niedrigsten Adresse gespeichert. Dies gilt auch für Adressen und deren Ablage auf dem Stack sowie die Ablage von Werten, die mehrere Register belegen.
- size_t
- size_t ist unsigned und immer 16 Bit groß, unabhängig davon , ob mit -mint8 übersetzt wird oder nicht.
Binäre Konstanten
Einige Versionen von avr-gcc ermöglichen die Verwendung binärer Konstanten für Ganzzahl-Werte:
unsigned char value = 0b00000010;
Davon sollte man absehen, denn zum einen hat man schnell eine 0 zu wenig oder zu viel getippselt, es ist kein Standard-C und man hat die leserlichere Alternative
unsigned char value = (1<<1);
Registerverwendung
- → avr-gcc: Register Layout im GCC Wiki
avr-gcc verwendet die Prozessor-Register GPRs auf eine definierte Art und Weise. Wie die Register verwendet werden, muss man wissen, wenn man Assembler-Funktionen schreibt, die mit durch avr-gcc übersetztem C-Code zusammenpassen sollen. Der "reinen" C-Programmierer muss sich keine Gedanken um die Register-Verwendung machen.
- R0
- ein temporäres Register, in dem man rumwutzen darf. Der Compiler verwendet dieses Register als Scratch-Register.
- T-Flag
- Das T-Flag im SREG wird analog wie R0 verwendet
- R1
- enthält den Wert 0. Wird der Wert zerstört, zum Beispiel durch einen MUL-Befehl, muss das Register wieder auf 0 gesetzt werden.
- R1 R17, R28, R29
- allgemeine Register, die durch einen Funktionsaufruf nicht verändert bzw. wieder auf den ursprünglichen Wert restauriert werden
- R0, R18 R27, R30, R31
- können durch Funktionsaufrufe verändert werden, ohne restauriert zu werden
- Framepointer
- R28 R29 (Y-Reg) enthält den Framepointer, sofern benötigt
- Argument-Register
- Die Register R8 bis R25 finden Verwendung zur Übergabe von Funktionsparametern. Für den Ort, an dem das Argument übergeben wird, gilt:
- Die Größe in Bytes wird zur nächsten geraden Zahl aufgerundet, falls die Argumentgröße ungerade ist.
- Ist in den verbleibenden Übergaberegistern kein Platz mehr oder ist das Argument namenlos (varargs), wird es im Stack oder im Speicher übergeben und die Adresse als implizites erstes Argument.
- Der Registerort fängt mit 26 an.
- Von dem Registerort wird die berechete Größe abgezogen und (falls Platz) das Argument in diesen Registern übergeben. Ist das erste Argument z.B. ein long, dann erfolgt die Übergabe in den Registern R22, R23, R24 und R25 (LSB zuerst). Danach wird die gerundete Größe des Arguments vom Registerort abgezogen, dieser also auf 22 gesetzt. Ist das nächste Argument ein char, wird dessen Größe auf 2 aufgerundet und vom Ort abgezogen. Dieses Argument wird also in R20 übergeben und der Ort auf 20 gesetzt, etc.
- Return-Register
- Ein Return-Wert wird in den gleichen Registern zurückgegeben, die auch für ein gleichgrosses erstes Funktionsargument genommen würden. Liefert eine Funktion ein long, dann erfolgt die Rückgabe also in den Registern R22-R25 (LSB zuerst). Bei einem short sind es die Register R24 und R25.
Builtins
Zur bedingten Codeerzeugung und zur Erkennung, welcher Compiler sich an der Quelle zu schaffen macht, sind folgende Builtins hilfreich.
Siehe auch: Maschinenspezifische Optionen
Builtin Defines
GCC
- __GNUC__
- X wenn GCC-Version X.Y.Z
- __GNUC_MINOR__
- Y wenn GCC-Version X.Y.Z
- __GNUC_PATCHLEVEL__
- Z wenn GCC-Version X.Y.Z
- __VERSION__
- "X.Y.Z" wenn GCC-Version X.Y.Z
- __GXX_ABI_VERSION
- Version der ABI (Application Binary Interface)
- __STDC__
- Ist 1, wenn Standard-C übersetzt wird
- __OPTIMIZE__
- Optimierung ist aktiviert
- __NO_INLINE__
- Ohne Schalter -finline resp. -finline-all-functions etc.
- __ASSEMBLER__
- Definiert, falls GCC die Eingabe als Assembler-Code betrachtet und nicht compiliert. Weiterleitung an den Assembler.
- __cplusplus
- Es wird C++ übersetzt (Quell-Endung *.cpp, *.c++ oder Option -x c++).
- __FILE__
- Löst auf zum Dateinamen der Quelldatei, in der das __FILE__ steht.
- __LINE__
- Löst auf zur Zeilennummer der Quelldatei, in der das __LINE__ steht.
- __DATE__
- Löst auf zum Datum (precompile-date)
- __TIME__
- Löst auf zur Zeit (precompile-time)
avr-gcc
- __AVR
- Definiert für Target avr, d.h. avr-gcc ist am Werk
- __AVR__
- dito
- __AVR_ARCH__
- codiert den AVR-Kern, für den Code erzeugt wird (Classic, Mega, ...).
- __AVR_XXXX__
- Gesetzt, wenn -mmcu=xxxx.
Builtin Variablen
- __func__
- Eine magische Variable, die den aktuellen Funktionsnamen enthält. Gerade so, als hätte man ihn selbst mit
static const char __func__[] = "main";
- definiert.
Speicherverwaltung
Sections
Sections sind mit Fächern vergleichbar, in die Daten, Code, Debug-Informationen usw. einsortiert werden. Zur Section .text gehört z.B. der ausführbare Code, welcher letztendlich im Flash landet. Wo genau das ist, braucht man nicht zu wissen und es spielt auch keine Rolle, wo eine bestimmte Funktion landet.
Für 'normalen' Code und 'normale' Daten braucht man sich nicht um die Sections zu kümmern, sie werden von avr-gcc automatisch richtig zugeordnet. Für spezielle Anwendungen kann es aber notwendig sein, die Ablage in eine andere Section zu machen; etwa wenn man Daten im EEPROM lesen/schreiben will. Wie das genau gemacht wird, steht im Abschnitt "Attribute" und es gibt Beispiele in den Abschnitten "Datenablage am Beispiel Strings" und "Zufall".
Section | Ablage | Betrifft | Beschreibung |
---|---|---|---|
.text | Flash | Code | normaler Programm-Code |
.data | SRAM | Daten | wird vom Startup-Code initialisiert |
.rodata | SRAM | Daten | für AVR analog zu .data, enthält readonly-Daten und wird im RAM abgelegt, nicht im Flash |
.bss | SRAM | Daten | wird vom Startup-Code zu 0 initialisiert |
.progmem.data | Flash | Daten | Daten im Flash, die dort mit PROGMEM bzw. __attribute__((progmem)) abgelegt wurden und mit pgm_read_*-Funktionen gelesen werden können |
.eeprom | EEPROM | Daten | Daten im EEPROM, die dort z.B. mit EEMEM hingelegt werden |
.noinit | SRAM | Daten | wird nicht initialisiert |
.initn | Flash | Code | wird vor main ausgeführt, n = 0...9 |
.finin | Flash | Code | wird nach main ausgeführt, n = 9...0 |
.vectors | Flash | Code | Vektor-Tabelle: Tabelle mit Sprüngen zur jeweiligen ISR |
.bootloader | Flash | Code | für den Bootloader |
.stab* .debug* |
— | Debug-Info | wird nicht geladen |
Der Anfang einer Section kann auch dem Linker mitgegeben werden, etwa wenn wie üblich avr-gcc als Treiber für den Linker verwendet wird:
avr-gcc ... -Wl,--section-start=.eeprom=0x810001
Damit beginnt Section .eeprom nicht an der (virtuellen) Adresse 0x810000, sondern ein Byte später. Manche AVRs haben einen Silicon-Bug, der bei Verwendung der EEPROM-Adresse 0 zu Fehlern führt. Mit der obigen Linker-Option wird diese Adresse nicht mehr verwendet.
Dynamische Speicherallokierung
Zur dynamischen Speicherallokierung stehen Standard-Funktionen wie malloc zur Verfügung. Damit kann man zur Laufzeit Speicher anfordern und wenn man ihn nicht mehr benötigt, wieder freigeben. Funktionen wie malloc und calloc sind jedoch recht aufwändig. Die allokierten Speicherstücke werden intern in einer verketteten Liste verwaltet, und das verbraucht wertvollen Platz im Flash und im SRAM sowie Laufzeit.
Resourcen-schonendere Möglichkeiten, zur Laufzeit an Speicher zu kommen, bieteten __builtin_alloca und dynamische Arrays. Der Speicher, der damit belegt wird, wird nicht auf dem Heap angelegt, sondern im Frame der Funktion. Das ist wesentlich effektiver als die Standard-Methoden, denn es muss nur ein Wert zum Framepointer addiert werden. Den so erhaltenen Speicher braucht man auch nicht freizugeben. Das geschieht automatisch beim Verlassen der Funktion in deren Epilog, indem der Wert wieder vom Framepointer subtrahiert wird.
Von der Verwendung ist der mittels __builtin_alloca und dynamischer Arrays erhaltene Speicher also wie eine lokale Variable, mitsamt den bekannten Regeln für den Gültigkeitsbereich.
Insbesondere darf dieser Speicher nicht mit return an die darüberliegende Funktion zurückgegeben werden, weil er dann nicht mehr gültig ist und ein Zugriff darauf zu einem Laufzeitfehler führt!
Der Speicherbereich ist dort gültig, wo auch die Adresse einer 'normalen' lokalen Variablen gültig wäre, wenn diese an der gleichen Stelle definiert würde.
Das Programm/der Algorithmus muss daher beim Beschreiten dieses Wegs darauf angepasst sein.
Verwendung:
void function (size_t num_data) { /* data_t hat man irgendwo selber definiert, oder es ist ein elementarer Typ */ data_t * const p = (data_t * const) __builtin_alloca (num_data * sizeof (data_t)); /* Mach was mit p[0] ... p[num_data-1] */ ... }
oder mittels eines dynamischen Arrays:
void function (size_t num_data) { /* data_t hat man irgendwo selber definiert, oder es ist ein elementarer Typ */ data_t p[num_data]; /* Mach was mit p[0] ... p[num_data-1] */ ... }
Attribute
Mit Attributen kann man die Codeerzeugung beeinflussen. Es gibt verschiedene Attribute, die auf Daten, Typen, und/oder Funktionen anwendbar sind.
Syntax:
__attribute__ ((<name>)) __attribute__ ((<name1>, <name2>, ...)) __attribute__ ((<name> ("<wert>")))
Nützliche Attribute von GCC
Attribut | Funktionen | Daten | Typen | Beschreibung |
---|---|---|---|---|
section ("<name>") | (x) | (x) | (x) | Lokatiert nach Section <name> |
noreturn | x | Die Funktion wird nie zurückkehren. Ermöglicht weitere Optimierungen. | ||
inline | x | Funktion wird geinlinet falls möglich | ||
always_inline | x | Funktion wird geinlinet | ||
noinline | x | Funktion wird keinesfalls geinlinet | ||
const | x | Diese Funktion hängt nur von ihren Parametrn ab und hat keine Nebenwirkungen, ausser den Rückgabewert zu liefern. Dieses Wissen ermöglicht Optimierungen. | ||
pure | x | Diese Funktion hängt nur von ihren Parametern und globalen Variablen ab und hat keine Nebenwirkungen, ausser den Rückgabewert zu liefern. Dieses Wissen ermöglicht Optimierungen. | ||
deprecated | x | x | Hängt dieses Attribut an einem Prototypen, so bekommt man eine Warnung, wenn das dazugehörige Objekt angefasst wird. Sehr praktisch, um alle Verwendungen eines Datums/einer Funktion anzeigen zu lassen. | |
packed | x | x | Datenablage in Strukturen erfolgt dicht, also ohne eventuelle Füllbytes | |
unused | x | Bei Funktionsparametern, die nicht gebraucht werden. Vermeidet entsprechende Warnungen bei Funktionen die ein bestimmtes Interface implementieren (müssen). | ||
constructor | x | Die Funktion wird vor main automatisch aufgerufen, um Initialisierungen zu erledigen. Die Reihenfolge der Aufrufe mehrerer so attributierter Funktionen ist nicht spezifiziert. Die Funktion darf keine Parameter haben. Der Aufruf erfolgt nach dem Initialiseren der globalen und statischen Variablen. |
Attribute von avr-gcc
Attribut | Funktionen | Daten | Typen | Beschreibung |
---|---|---|---|---|
progmem | x | Lokatiert read-only Daten ins Flash | ||
naked | x | Funktion wird ohne Prolog/Epilog erzeugt | ||
interrupt | x | Hier nur wegen der Vollständigkeit erwähnt | ||
signal | x | Hier nur wegen der Vollständigkeit erwähnt. Verwendet AVR Libc in ISR(·) | ||
OS_task | x | Hier nur wegen der Vollständigkeit erwähnt. Verwendet AVR Libc in ISR(·,ISR_NOBLOCK) | ||
OS_main | x | Ab Version 4.4. Die Funktion sichert keine Register, hat im Gegensatz zu einem naked Funktion jedoch eine return-Instruktion und initialisiert einen Framepointer falls benötigt. | ||
OS_task | x | Ab Version 4.4. Wie OS_main, jedoch Interrupt-sicher: Falls der Stackpointer verändert werden muss, dann geschiegt das unter IRQ-Abschaltung, was etwas größeren Code wie bei OS_main bedeuten kann. |
- Beispiele
void __attribute__ ((constructor)) foo() { /* Code */ }
Frühe Codeausführung vor main()
Mitunter ist es notwendig, Code unmittelbar nach dem Reset auszuführen, noch bevor man in main() mit der eigentlichen Programmausführung beginnt. Das kann zB. zur Bedienung eines Watchdog-Timers erforderlich sein.
Nach einen Reset und vor Aufruf von main werden Initialisierungen ausgeführt wie
- Setzen des Stackpointers
- Vorbelegung globaler Datenobjekte: Daten ohne Initializer werden zu 0 initialisert (Section .bss). Für Daten mit Initializer (Section .data) werden die Werte aus dem Flash ins SRAM kopiert.
- Initialisierung von Registern wie R1, in dem bei avr-gcc immer die Konstante 0 gehalten wird.
init-Sections
Im Linker-Script werden Sections von .init0 bis .init9 definiert, die nacheinander abgearbeitet werden. Erst danach wird main betreten. Um Code früh auszuführen, legt man die Funktion in eine dieser Sections:
void code_init3(void) __attribute__ ((naked, used, section (".init3"))); /* !!! never call this function !!! */ void code_init3 (void) { /* Code */ }
Der Code in den .initn-Sections wird sequenziell ausgeführt, daher darf in diese Sections nur der nackte Funktions-Code, ohne Prolog und ohne Epilog (also auch ohne ret-Instruktion!). Da diese nackten Funktionen kein ret haben, dürfen sie nicht explizit aufgerufen werden! Ihr Aufruf erfolgt implizit dadurch, daß ihr Code in einer init-Section steht.
Zu beachten ist dabei
- Eine so definierte Funktion darf keinesfalls aufgerufen werden!
- Zuweisungen wie i=0; ergeben vor .init3 inkorrekten Code, da vor Ende von .init2 Register R1 noch nicht mit 0 besetzt ist, avr-gcc aber davon ausgeht, daß es eben diesen Wert enthält.
- Lokale Variablen müssen in Registern liegen, denn vor Ende von .init2 ist der Stackpointer noch nicht initialisiert. Zudem ist die Funktion naked, hat also insbesondere keinen Prolog, der den Framepointer (Y-Register) setzen könnte, falls er benötigt wird.
- Gegebenenfalls ist daher die Verwendung von inline-Assembler angezeigt oder die Implementierung in einem eigenen Assembler-Modul, das dazu gelinkt wird. Der erzeugte Code ist im List-File zu überprüfen.
- Werden mehrere Funktionen in die selbe init-Section gelegt, ist die Reihenfolge ihrer Ausführung nicht spezifiziert und i.A. nicht die gleiche wie in der Quelle.
Unbenutzte init-Sections haben die Nummern 0, 1, 3 und 5 bis 8. Die verbleibenden werden vom Startup-Code verwendet:
- .init2
- Initialisieren von R1 mit 0 und Setzen des Stackpointers
- .init4
- Kopieren der Daten vom Flash ins SRAM (.data, .rodata) und Löschen von .bss
- .init6
- Ausführung von globalen Konstruktoren
- .init9
- Aufruf von main
Ein Programmbeispiel für Code in einer init-Section ist in "Speicherverbrauch bestimmen mit avr-gcc".
Konstruktoren
In GNU-C ist es möglich, auch in C Konstruktoren zu definieren. Diese werden automatisch vor main ausgeführt:
void myinit(void) __attribute__ ((constructor, used)); void myinit (void) { /* Code */ }
Im Vergleich zu init-Sections ist diese Methode einfacher, da keine Einschränkungen wie bei den init-Sections bestehen: Es sind lediglich parameterlose void-Funktionen zu definieren. Konstruktoren müssen voneinander unabhängig sein, denn bei mehreren Konstruktoren ist deren Aufrufreihenfolge nicht festgelegt.