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