Forum: Compiler & IDEs GCC -fstack-usage


von Christopher J. (christopher_j23)


Lesenswert?

Servus miteinander,

Um den benötigten Stack abschätzen zu können habe ich mich mal mit der 
GCC-Option -fstack-usage beschäftigt. Der GCC generiert dann zu jeder 
.o-Datei eine .su-Datei, in der zu jeder Funktion die Stackbenutzung 
angegeben wird.

z.B. bekommt man für ein leicht abgeändertes Hello-World
1
#include "stdio.h"
2
3
int magic_number(void)
4
{
5
        int k,l,m,ret;
6
        k = 4;
7
        l = 10;
8
        m = 2;
9
10
        ret = k * l + m;
11
        return ret;
12
}
13
14
int main(void)
15
{
16
        int i;
17
        i = magic_number();
18
        printf("The answer to everything is %d\n",i);
19
        return 0;
20
}

mit
1
arm-none-eabi-gcc -O0 -fstack-usage -c main.c

eine main.su mit dem Inhalt
1
main.c:3:5:magic_number 24      static
2
main.c:14:5:main        16      static

Mal abgesehen davon, dass der Code so natürlich sinnfrei ist, kann man 
schon erkennen, dass der Wert für main.c offensichtlich nicht den 
Stackverbrauch der darin aufgerufenen Funktionen beinhaltet, da der Wert 
für die - aus main() aufgerufene - Funktion magic_number() schon höher 
ist. Eigentlich ist das auch logisch, da magic_number() theoretisch auch 
in einer anderen .c-Datei oder auch in einer Library liegen könnte und 
lediglich die Deklaration mittels Header in main.c bekannt wäre, was in 
diesem Beispiel für printf() auch der Fall ist.

Die Frage wäre nun ob es einen Weg gibt, die Stack-Benutzung insgesamt 
nach oben abschätzen zu können, also gewissermaßen eine 
"Hochwassermarke" zu bekommen. Interrupts und rekursive Funktionen 
können dabei ruhig außen vor bleiben.

Gefunden habe ich dazu bisher lediglich einen Blog-Artikel 
(https://mcuoneclipse.com/2015/08/21/gnu-static-stack-usage-analysis), 
der wiederum auf ein Perl-Script verweist:
https://www.dlbeer.co.nz/oss/avstack.html

Das Script wertet sowohl die .su-Dateien, als auch die Object-Files 
selber aus. Dabei gibt es allerdings das Problem, dass für 
Assembler-Sourcen keine .su-Datei existiert, da dort -fstack-usage 
offenbar nicht funktioniert.

Das gleiche Problem gibt es auch mit link-time optimization. D.h. sobald 
-flto mit ins Spiel kommt, ist die main.su einfach leer. Mir ist zwar 
klar, dass der Output, also die Object-Files mit -flto ganz anders ist, 
als ohne und das der Compiler Optimierungen erst zur link-time vornimmt, 
weshalb Aussagen über die Stackbenutzung quasi erst nach dem Linken 
möglich sind aber wie bekomme ich denn dann diese Information?

Ein weiteres Perl-Script auf das ich gestoßen bin stammt ursprünglich 
aus der Busy-Box bzw. dem Linux-Kernel:
https://github.com/bboozzoo/stm32tools/blob/master/checkstack.pl

Das gute ist, dass es direkt auf .elf-Ebene operiert, also egal ist, ob 
das Binary aus C, C++ oder ASM stammt. Allerdings gibt das Script auch 
nur den Stack für die einzelnen Funktionen und nicht deren 
Unterfunktionen an. Interessanterweise sind die Zahlen komplett anders 
(nämlich kleiner) als jene aus -fstack-usage und denen traue ich im 
Prinzip eher als dem Perl-Script.

Als Alternative gibt es ja noch das "messen" mittels Pattern, so wie 
etwa bei FreeRTOS 
(https://freertos.org/uxTaskGetStackHighWaterMark.html) allerdings wäre 
mir eine statische Analyse prinzipiell lieber. Das scheint aber 
irgendwie ein sehr schwieriges Unterfangen zu sein, wenn man alles mit 
berücksichtigen möchte.

Falls jemand von euch eine Idee hat, wäre ich dafür sehr dankbar.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Christopher J. schrieb:
> Die Frage wäre nun ob es einen Weg gibt, die Stack-Benutzung insgesamt
> nach oben abschätzen zu können, also gewissermaßen eine
> "Hochwassermarke" zu bekommen. Interrupts und rekursive Funktionen
> können dabei ruhig außen vor bleiben.

Das geht per statischer Analyse, und man braucht den dynamischen 
Call-Graph.

Schikanan wie indirekte Funktionsaufrufe, LongJump / Exceptions, 
rekursive Funktionen, Stack-Objekte dynamischer Größe, Sub-Worstcase 
Akkumulation kommen hinzu.

Es gibt professionelle Anbieter solcher Tools zur statischen und / oder 
abstrakten Analyse, Empfehlung spreche ich hier aber keine aus.

> Dabei gibt es allerdings das Problem, dass für Assembler-Sourcen
> keine .su-Datei existiert, da dort -fstack-usage offenbar nicht
> funktioniert.

Wie auch? Assembler heißt: Selber machen. In diesem Falle also .su 
klöppeln und immer schön aufpassen, dass es auch das asm abbildet. Oder 
falls das Asm einfach[tm] genug ist, ein Tool verwenden.

> Das gleiche Problem gibt es auch mit link-time optimization. D.h. sobald
> -flto mit ins Spiel kommt, ist die main.su einfach leer.

Vermutlich verwendest du slim Objects. Und die Info im main.su mit fat 
Objects würde auch garnix nutzen, weil der Binärcode darin Makulatur 
ist: verwendet wird dann ja eh nur noch der gestreamte Byte-Code.

> Mir ist zwar klar, dass der Output, also die Object-Files mit
> -flto ganz anders ist, als ohne und das der Compiler Optimierungen
> erst zur link-time vornimmt, weshalb Aussagen über die Stackbenutzung
> quasi erst nach dem Linken möglich sind aber wie bekomme ich denn
> dann diese Information?

Die Info steht dann nicht in Dateien, deren Namen von den Modulen 
abgeleitet ist, sondern in den ltrans Objects.  Mit
1
-flto -save-temps -o foo.elf
sehe ich .su in foo.elf.ltrans<n>.ltrans.su

Ohne -save-temps sollte das eigentlich auch gehen und nicht in ad-hoc 
benannten Dateien landen.  Evtl. ist hier ein Bug-Report angebracht.

Allerdings stellt sich die Frage, ob du überhaupt LTO verwenden willst. 
Der Compiler wirft den kompletten (statischen) Call-Tree in einen großen 
Topf, rührt kräftig rum, und voilà: Der Call-Tree wird so geinlinet und 
geclont und dann zerschnippelt und auf verschiedene ltrans aufgeteilt, 
dass diese möglichst gleiche Compile- und Link-Last tragen.

Du musst also erst diesen neuen Call-Tree aufbauen (lassen).

> Als Alternative gibt es ja noch das "messen" mittels Pattern, so wie
> etwa bei FreeRTOS

"Messen" liefert prinzipbedingt immer nur eine Untergrenze also einen 
Abschätzung nach unter, nicht aber die gewünschte Abschätzung nach oben. 
In einzelnen Fällen können (das Maximum über) Untergrenze tatsächlich 
eine (dann optimale) Abschätzungen nach oben darstellen, aber dass dies 
tatsächlich und für alle Eingaben zutrifft, ist dann gesondert 
nachzuweisen.

> Das scheint aber irgendwie ein sehr schwieriges Unterfangen zu
> sein, wenn man alles mit berücksichtigen möchte.

Wie gesagt: es gibt Anbieter, die damit ihre Brötchen verdienen.  Wenn 
das Problem trivial wäre, gäbe es solche Anbieter nicht ;-)

Teilweise kommen auch deren Tools nicht aus dem Sumpf und brauchen 
Annotations in der Quelle. Wenn man also die Anforderung hat, dass eine 
Software zur Laufzeit bestimmte Resourcen-Obergrenzen (Stack, Laufzeit, 
Heap, Object-Pools, IRQ-Latenz, Respond-Times, Durchsatz, etc) 
garantiert nicht reißt, dann ist es nicht die schlechteste Idee, dies 
bereits ins Design der Software einzubeziehen und z.B. vtables vermeiden

von Christopher J. (christopher_j23)


Lesenswert?

Danke für die ausführliche Antwort. -save-temps ist tatsächlich die 
Lösung, wenn -flto gesetzt ist. Ich denke auch, dass es sich hierbei um 
einen Bug handelt.

Johann L. schrieb:
> Allerdings stellt sich die Frage, ob du überhaupt LTO verwenden willst.
> Der Compiler wirft den kompletten (statischen) Call-Tree in einen großen
> Topf, rührt kräftig rum, und voilà: Der Call-Tree wird so geinlinet und
> geclont und dann zerschnippelt und auf verschiedene ltrans aufgeteilt,
> dass diese möglichst gleiche Compile- und Link-Last tragen.
>
> Du musst also erst diesen neuen Call-Tree aufbauen (lassen).

Das war so auch mein Verständnis von link-time optimization. Mit den 
einzelnen Details bin ich aber nicht so firm. Grundsätzlich frage ich 
mich aber warum ich es nicht verwenden sollte, wenn es doch verfügbar 
ist und Einsparungen bei den Ressourcen bietet. Ich kann mir jedoch gut 
vorstellen, dass durch LTO der ein oder andere Bug zum Vorschein kommt, 
der sich ohne LTO nicht bemerkbar macht, ähnlich wie mit -O0 vs. -O2.

Johann L. schrieb:
> Vermutlich verwendest du slim Objects. Und die Info im main.su mit fat
> Objects würde auch garnix nutzen, weil der Binärcode darin Makulatur
> ist: verwendet wird dann ja eh nur noch der gestreamte Byte-Code.

Ja, es sind die "normalen" slim objects. Wenn ich das richtig verstanden 
habe, dann sind die "fat objects" lediglich dafür da, im Nachhinein 
entscheiden zu können ob man diese mit oder ohne LTO linkt, z.B. bei 
einer Library.

Johann L. schrieb:
> "Messen" liefert prinzipbedingt immer nur eine Untergrenze also einen
> Abschätzung nach unter, nicht aber die gewünschte Abschätzung nach oben.
> In einzelnen Fällen können (das Maximum über) Untergrenze tatsächlich
> eine (dann optimale) Abschätzungen nach oben darstellen, aber dass dies
> tatsächlich und für alle Eingaben zutrifft, ist dann gesondert
> nachzuweisen.

Genau das sind meine Bedenken bei dieser Methode, d.h. nur untere Grenze 
statt oberer Grenze. Wenn ich das richtig sehe, kann ein Nachweis, dass 
diese Untergrenze gleichzeitig eine Obergrenze darstellt nur dadurch 
erbracht werden, indem man nachweist, das innerhalb der Messung alle 
möglichen Pfade mit allen möglichen Eingaben beschritten wurden. Klingt 
nicht gerade nach einer einfachen Aufgabe.

Johann L. schrieb:
> Das geht per statischer Analyse, und man braucht den dynamischen
> Call-Graph.

Gibt es dafür auch frei verfügbare Tools? Ich hatte zunächst an clang 
bzw. libclang gedacht. Müsste ich mir mal genauer anschauen, was damit 
möglich ist.

Johann L. schrieb:
> Wie gesagt: es gibt Anbieter, die damit ihre Brötchen verdienen.  Wenn
> das Problem trivial wäre, gäbe es solche Anbieter nicht ;-)

Das hatte ich leider befürchtet.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.