aus RN-Wissen, der freien Wissensdatenbank
Wechseln zu: Navigation, Suche



Dieser Artikel fasst einige Interna von avr-gcc zusammen.

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

Zusammenspiel zwischen avr-gcc und binutils

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".

Tabelle: Bedeutung der Sections bei avr-gcc
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

RAM-Layout für ein AVR mit 1kByte SRAM

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

Tabelle: Attribute von GCC (Auszug)
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

Tabelle: Attribute von avr-gcc
Attribut Funktionen Daten Typen Beschreibung
progmem x Lokatiert read-only Daten ins Flash. progmem in typedef wird nicht unterstützt!
naked x Funktion wird ohne Prolog/Epilog erzeugt
interrupt x Hier nur wegen der Vollständigkeit erwähnt. Verwendet AVR Libc in ISR(·,ISR_NOBLOCK)
signal x Hier nur wegen der Vollständigkeit erwähnt. Verwendet AVR Libc in ISR(·)
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 (void)
{
   /* 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.