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
- 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 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
- R1
- enthält immer den Wert 0
- 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 Groß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 Memory ü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.
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 ein 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, u.a. aus .progmem |
.bss | SRAM | Daten | wird vom Startup-Code zu 0 initialisiert |
.progmem | Flash | Daten | wird vom Startup-Code nach .data kopiert |
.eeprom | EEPROM | Daten | Daten im EEPROM |
.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 |
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.
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 Parametrn 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 dazugrhö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 | Variable wird nicht verwendet, z.B. 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 | x | x | Lokatiert 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 |
Beispiele:
#if ! defined (EEMEM) #define EEMEM __attribute__ ((section (".eeprom"))) #endif /* EEMEM */ const char EE_HALLO_WELT[] EEPROM = "Hallo Welt"; const int EE_wert EEMEM = 0x1234; void __attribute__ ((constructor)) foo() { /* Code */ }