Embedded-Programmierung

Stapel nach Maß

11. Juni 2012, 11:57 Uhr | Von Anders Lundgren und Lotta Frimanson

Für ein Embedded System sind der Stapelspeicher (Stack) und der dynamische Speicher (Heap) von fundamentaler Bedeutung. Ein sorgfältiges Aufsetzen ist die Voraussetzung für die Stabilität und Zuverlässigkeit des Systems. Der Programmierer muss Stack und Heap statisch zuordnen, doch ist die Berechnung der Stack-Größe bekanntermaßen selbst bei kleinsten Embedded Systemen kompliziert. Wird die Stack-Nutzung unterschätzt, so kann das zu schweren Laufzeitfehlern führen, die sich nur schwer finden lassen. Wird sie überschätzt, bedeutet das eine unnötige Verschwendung von Speicherressourcen.

Diesen Artikel anhören

Ob Desktop- oder Embedded Systeme: Einige Fehler und Erwägungen beim Design von Stack und Heap sind dieselben, andere aber sind komplett verschieden. Ein Beispiel für den Unterschied der beiden Umgebungen ist der verfügbare Speicher: Windows und Linux benötigen standardmäßig zwischen 1 MByte und 8 MByte, aber es kann auch sehr viel mehr sein. Die Größe des dynamischen Speichers ist lediglich durch den verfügbaren physischen Speicher und/oder die Datenblockgröße beschränkt.

Embedded Systeme dagegen sind sehr begrenzt hinsichtlich ihrer Speicherressourcen, vor allem wenn es um RAM-Speicher geht. Stack und Heap müssen in dieser eingeschränkten Speicherumgebung unbedingt minimiert werden. Allen kleinen Embedded Systemen gemein ist, dass diese über keinen virtuellen Speicher verfügen. Die Zuweisung von Stack, Heap und übergreifenden Daten (etwa Variablen, TCP/IP- und USB-Puffer, etc.) ist statisch und erfolgt während des Übersetzens der Applikation.

Grenzen erweitern

passend zum Thema

Bild 1: Stack-Overflow
Bild 1: Stack-Overflow
© IAR Systems

Seine Grenzen zu erweitern mag im täglichen Leben bereichernd sein, kann aber auch zu Problemen führen. Beim Programmieren die Grenzen von zugewiesenen Daten zu überschreiten wird aber mit Sicherheit Schwierigkeiten auslösen. Mit viel Glück werden diese sofort oder im Systemtest sichtbar, aber sie können sich auch erst zeigen, wenn es zu spät ist, und das Produkt an tausende Kunden ausgeliefert oder in abgelegenen Gegenden im Einsatz ist.

Ein Speicherüberlauf (Overflow) kann in allen drei Speicherbereichen auftreten: im globalen Speicher ebenso wie in Stack oder Heap (Bild 1). Array- oder Pointer-Bezüge zu überschreiben kann Zugriffe außerhalb des Speicherbereichs verursachen, der für das betreffende Objekt vorgesehen war. Manche Array-Zugriffe lassen sich durch eine statische Analyse validieren, zum Beispiel durch den Compiler selbst oder eine MISRA-C-Überprüfung:

int array[32];
array[35] = 0x1234;

Ist der Array-Index eine Variable, kann die statische Analyse nicht mehr alle Konflikte finden. Auch Indexzeiger lassen sich nur schwer von einer statischen Analyse nachverfolgen:

int* p = malloc(32 * sizeof(int));
p += 35;
*p = 0x1234;

Für Desktop-Systeme sind bereits seit Langem Laufzeitmethoden verfügbar, um Objekt-Overflow-Fehler zu identifizieren, unter anderem »Purify«, »Insure++« oder »Valgrind«. Diese Tools instrumentieren den Anwendungscode, um Speicherzugriffe im Betrieb zu validieren. Der Preis hierfür ist jedoch eine erheblich langsamere Ausführung und ein größerer Code, sodass dies keine praktikable Methode für kleine Embedded Systeme darstellt.

Stack und Heap

Der Stapelspeicher, genannt Stack, ist der Speicherbereich, in dem ein Programm seine Daten ablegt, zum Beispiel lokale Variablen, Rücksprungadressen, Funktionsargumente, temp

Normalerweise wächst der Stack im Speicher nach unten, und wenn der für den Stack vorgesehene Speicher nicht groß genug ist, schreibt der ausführende Code in den Bereich darunter, sodass ein Overflow entsteht. Der überschriebene Speicher ist meistens der Bereich, in dem globale und statische Variablen abgelegt werden. Ein unterdimensionierter Stack kann also schwere Laufzeitfehler verursachen, wenn Variablen überschrieben, Verweise verfälscht und Rücksprungadressen korrumpiert werden.

Alle diese Fehler lassen sich nur schwer finden. Auf der anderen Seite bedeutet eine Überdimensionierung des Stacks eine Verschwendung von Speicherressourcen. Es gibt einige Methoden, mit denen sich die benötigte Stack-Größe verlässlich berechnen lässt und mit denen sich Stack-bezogene Probleme identifizieren lassen. Im Heap findet sich der dynamische Speicher eines Systems.

Dynamischer Speicher und Heap können in kleinen Embedded Systemen oftmals als optional angesehen werden. Der dynamische Speicher erlaubt es verschiedenen Teilen des Programmes, sich Speicher zu teilen. Benötigt ein Modul den ihm zugewiesenen Speicher nicht mehr, gibt es diesen einfach an die Speicherverwaltung zurück, sodass ihn wieder andere Module nutzen können. Im Heap werden unter anderem gespeichert:

  • temporäre Datenobjekte,
  • C++ neu/löschen,
  • C++-STL-Container,
  • C++-Ausnahmen.

In großen Systemen ist die Berechnung der Heap-Größe aufgrund des dynamischen Verhaltens der Anwendung schwierig bis schlichtweg unmöglich. Außerdem gibt es nur wenige Tools im Embedded-Bereich, mit denen sich die Heap-Nutzung messen lässt. Ein intakter dynamischer Speicher ist von größter Bedeutung. Die allokierten Daten sind normalerweise mit kritischen Daten der Speicherverwaltung vermischt.

Eine schlechte Nutzung des zugewiesenen Speicherplatzes bedeutet unter Umständen nicht nur eine Beschädigung der dynamischen Daten, sondern kann auch die gesamte Speicherverwaltung beeinträchtigen und höchstwahrscheinlich zu einem Anwendungsabsturz führen. Zu berücksichtigen ist weiterhin, dass die Echtzeit-Performance des Heaps nicht deterministisch ist. Die Zeit der Speicherzuweisung hängt unter anderem vom vorherigen Gebrauch und der angeforderten Speicherplatzgröße ab.

Für Embedded-Entwickler, denen es vor allem auf Zyklen ankommt, stellt das eine besondere Herausforderung dar. Grundsätzlich wird empfohlen, die Verwendung von dynamischem Speicher bei kleinen Embedded Systemen zu minimieren.

Zuverlässiges Stack-Design

Es gibt viele Faktoren, welche die Berechnung der maximalen Stack-Nutzung erschweren. Viele Anwendungen sind komplex und ereignisgesteuert, bestehen aus Hunderten von Funktionen und zahllosen Interrupts. Wahrscheinlich sind sogar Interrupt-Funktionen eingebaut, die jederzeit ausgelöst werden können, und wenn diese ineinander greifen können, wird die Situation immer schwerer zu erfassen. Es gibt also nicht nur einen »natürlichen«, einfach zu verfolgenden Ausführungsverlauf.

Denkbar sind indirekte Aufrufe über Funktionszeiger, bei denen es viele mögliche Zielfunktionen gibt. Auch Rekursionen und unkommentierte Assembler-Routinen verursachen Probleme bei der Berechnung der maximalen Stack-Nutzung. Viele Mikrocontroller verfügen über multiple Stacks, etwa einen System-Stack und einen Nutzer-Stack. Multiple Stacks werden auch bei Embedded-Echtzeitbetriebssystemen wie »µC/OS« oder »ThreadX« eingesetzt, in denen jede Task ihren eigenen Stack-Bereich erhält. Auch die Nutzung von Laufzeitbibliotheken und Software anderer Anbieter können die Berechnung erschweren, wenn der Quellcode für die Bibliothek oder das Echtzeitbetriebssystem nicht verfügbar ist.

Weiterhin ist zu berücksichtigen, dass sich Änderungen des Codes oder des Schedulings der Anwendung stark auf die Stack-Nutzung auswirken können. Verschiedene Compiler und Optimierungslevel erzeugen unterschiedlichen Code, der wiederum unterschiedlich viel Stapelspeicher beansprucht. Alles in allem ist es wichtig, stets die maximalen Stack-Anforderungen im Auge zu behalten.

Die Festlegung der Stack-Größe ist ein Faktor, der beim Design der Applikation zu berücksichtigen ist, und daher benötigt der Entwickler eine verlässliche Methode, um den maximalen Stack-Bedarf zu bestimmen. Auch wenn das gesamte verfügbare RAM dem Stack-Gebrauch zur Verfügung gestellt wird, ist immer noch nicht gesichert, dass dieser auch ausreicht.

Eine denkbare Option ist es, das System unter Umständen mit dem schlechtmöglichsten Stack-Verhalten zu testen. Während dieser Tests ist ein Verfahren nötig, das feststellen kann, wie viel Stack genutzt wurde. Das lässt sich grundsätzlich auf zwei Arten bewerkstelligen: durch einen Ausdruck des aktuellen Stack-Gebrauchs oder indem sichergestellt wird, dass Spuren des Stack-Gebrauchs im Speicher zurückbleiben, die sich nach Abschluss des Testlaufs nachvollziehen lassen.

Wie bereits angesprochen, lassen sich diese extremen Bedingungen in einem komplexen System aber nur sehr schwer auslösen. Ein grundsätzliches Problem mit dem Test eines ereignisgetriebenen Systems mit vielen Interrupts ist die hohe Wahrscheinlichkeit, dass einige Ausführungspfade niemals geprüft werden. Ein anderer Ansatz könnte sein, den maximal benötigten Stack theoretisch zu berechnen. Ein vollständiges System manuell zu berechnen ist natürlich unmöglich.

Für diese Berechnung wäre ein Tool nötig, welches das ganze System analysieren kann und entweder auf der Binärdatei- oder Quellcode-Ebene arbeiten muss. Ein binäres Tool arbeitet auf Maschinenbefehlsebene, um alle möglichen Bewegungen des Programmzählers über den Code hinweg zu überprüfen und so den Worst-Case-Ausführungspfad zu finden.

Ein statisches Analysetool auf Quellcode-Ebene wird dagegen alle betroffenen Compilationseinheiten auslesen. In beiden Fällen muss das Tool in der Lage sein, direkte und indirekte Funktionsaufrufe über Pointer in dem Modul zu bestimmen und ein konservatives Stack-Nutzungsprofil über das ganze System hinweg für alle Aufrufsketten zu ermitteln. Das Quellcode-Tool muss auch die Anforderungen des Compilers an den Stack kennen, wie zum Beispiel Alignments oder temporäre Daten des Compilers.

Beispielcode 1
Beispielcode 1
© IAR Systems

Ein solches Werkzeug selbst zu schreiben ist ein schwieriges Unterfangen, aber es gibt kommerzielle Alternativen, zum Beispiel eigenständige Tools für die statische Stack-Berechnung wie »PC-Lint«, oder Werkzeuge aus dem Angebot von Lösungsanbietern, etwa für das Echtzeitbetriebssystem »ThreadX« von Express Logic. Weitere Tools, die über die Informationen zur Berechnung der maximalen Stack-Anforderung verfügen, sind der Compiler und der Linker. Diese Funktion ist zum Beispiel in der »Embedded Workbench« für ARM von IAR verfügbar.

Berechnung über den Stack-Pointer

Ein Weg zur Berechnung der Stack-Tiefe nutzt die Adresse des aktuellen Stack-Pointers. Dazu verwendet man beispielsweise die Adresse eines Funktionsarguments oder einer lokalen Variable. Erfolgt dies zu Beginn von main() und dann für jede Funktion, die möglicherweise den größten Stack braucht, lässt sich daraus die Größe des Stacks berechnen, den die entsprechende Anwendung benötigt. Im Kasten »Beispielcode 1« findet sich ein Beispiel, bei dem davon ausgegangen wird, dass der Stack von hohen zu niedrigen Adressen wächst.

Mit dieser Methode lassen sich in kleinen und deterministischen Systemen gute Ergebnisse erzielen, aber in vielen Systemen ist es schwierig, die verschachtelten Funktionen mit der tiefsten Stack-Nutzung zu bestimmen und eine Worst-Case-Situation hervorzurufen. Es ist zu bedenken, dass die Ergebnisse dieser Berechnungsmethode nicht die Stack-Nutzung durch Interrupt-Funktionen berücksichtigen. Eine Variante dieser Methode ist es, den Stack-Pointer stichprobenartig mit einem hochfrequenten Timer-Interrupt auszulesen.

Die Interrupt-Frequenz sollte so hoch wie möglich angesetzt werden, ohne dabei das Echtzeitverhalten der Anwendung zu beeinflussen. Die Frequenz liegt typischerweise zwischen 10 kHz und 250 kHz. Der Vorteil dieser Vorgehensweise ist, dass man die Funktion mit der größten Stack-Nutzung nicht manuell finden muss. Es ist auch möglich, die Stack-Nutzung über die Interrupt-Funktionen zu ermitteln, wenn der verwendete Timer-Interrupt andere Interrupts unterbrechen darf. In jedem Fall ist Sorgfalt geboten, denn Interrupt-Funktionen können sehr kurz sein und daher vom Prüf-Interrupt verpasst werden.

void sampling_timer_interrupt_handler(void)
{
   char*
   currentStack;
   int a;
   currentStack =
   (char *)&a;
   if (currentStack <
   lowStack) lowStack
   = currentStack;
}


  1. Stapel nach Maß
  2. Stack-Sicherheitszone
  3. Zuverlässiges Heap-Design

Lesen Sie mehr zum Thema


Jetzt kostenfreie Newsletter bestellen!

Weitere Artikel zu IAR Systems GmbH