Forum: Mikrocontroller und Digitale Elektronik [ASM] Unbenutzte Symbole entfernen


von Christian (dragony)


Lesenswert?

Hallo zusammen,

ich erstelle derzeit ein reines Assembler Programm für den xmega und 
verwende dafür den gnu assembler in der avr-gcc toolchain.

Jetzt habe ich viele Hilfsfunktionen geschrieben, die ich aber nie alle 
brauche. Er haut mir aber immer alle Symbole in die elf-Binary rein. 
Natürlich habe ich schon mehrmals Google gefragt, aber alle 
Hilfestellungen beziehen sich wohl auf C-Code, jedenfalls wird mein Code 
durch die Parameterkette "-ffunction-sections -fdata-sections 
-Wl,--gc-sections -Os" nicht kleiner. Hier der gesamte Aufruf:

avr-gcc main.S -o main.elf -Wall -Wextra -std=gnu99 -mmcu=atxmega128a4u 
-I../include -ffunction-sections -fdata-sections -Wl,--gc-sections -Os

Ich habe die Befürchtung, dass bei reinem Assembler der Compiler einfach 
nicht schlau genug ist um rauszufinden, welche Symbole nicht gebraucht 
werden. Ich könnte ja mit einem gemeinem EIJMP eine mathematisch 
berechnete Adresse anspringen :p

Das wäre aber ein echter Nachteil der Assemblerprogrammierung, wenn ich 
hinterher von Hand alle Symbole rauskommentieren muss und das kann ja 
nicht wirklich so gedacht sein?

von asdfasd (Gast)


Lesenswert?

Dir ist aber schon klar, dass C-Compiler-Optionen keine Auswirkung auf 
den Assembler bzw Linker haben?  Bei ein "gcc foo.S ..." wird der 
C-Compiler nicht gestartet - nur der Preprozessor, Assembler und Linker.

Insb sind -ffunction-sections & co Optionen, die die Codegenerierung des 
C-Compilers steuern und habe keine Auswirkungen auf den Assembler. 
Damit --gc-sections (eine Linker-Option) funktioniert, musst du in 
deinem Assembler-Source selbst pro optionaler Funktion eine Sektion 
definieren.  Schau dir einfach mal an, was für Unterschiede der Compiler 
mit und ohne -ffunction-sections erzeugt (mittels "gcc -S" 
Assembler-Output erzeugen).

von asdfasd (Gast)


Lesenswert?

Ach so: die übliche Methode, die überall funktioniert und nicht 
irgendwelche erweiterten Features der GNU-Toolchain braucht, ist die, 
jede optionale Funktion/Funktionenblock in eine eigene Datei zu packen, 
all diese kleinen Dateien in ein .a-Library packen und dieses dann beim 
finalen Linken anzugeben.  Der Linker sucht sich dann nur die Files 
heraus, die benötigt werden.  Geht immer, auf allen Systemen.

von Bastler (Gast)


Lesenswert?

Solange sie in den FLASH des benutzten AVR passen (und die definierten 
Variablen keinen RAM-Engpaß verursachen), haben nicht benutzte 
"Routinen" doch keine negative Auswirkungen.
(C-)Compiler können solche Biblioteksfunktionen auf Anfrage selbst in 
verdauliche Häpchen zerlegen, aber die erzeugen ja, so ein hier 
berühmter Fisch, nur Code-Bloat. Ups, bei Handarbeit, sprich ASM scheint 
das ja auch der Fall zu sein. Unfassbar ;-)

von Christian B. (casandro)


Lesenswert?

a) Unbenutzten Code automatisch zu entfernen ist quasi unmöglich. 
Deshalb gehen da Compiler, wenn überhaupt, sehr vorsichtig vor. In der 
Praxis wird somit auch in C, und besonders in C++ fast nie was entfernt.

b) Symbole haben im Assembler nichts mit Code zu tun. Wenn Du eine 
Sprungmarke setzt, dann wird die im Code einfach durch die entsprechende 
Adresse ersetzt.

c) Dein Assembler macht genau das was Du ihm sagst. Er entfernt keine 
Befehle sondern setzt die nur 1:1 um.

von Bastler (Gast)


Lesenswert?

Unmögliches gibt es sogar zum kostenlosen Runterladen!
Zum Glück entschließt die Realität sich nie dazu, sich an manche 
"Wahrheiten" zu halten ;-)

von Georg (Gast)


Lesenswert?

Christian Berger schrieb:
> a) Unbenutzten Code automatisch zu entfernen ist quasi unmöglich.

Komisch, das konnte mein Pascal-Compiler unter CP/M schon vor 30 Jahren, 
und das absolut zuverlässig: der Linker mit der Option "Scan" linkt nur 
Routinen, die tatsächlich aufgerufen werden. Wenn man sich das logisch 
überlegt ist das auch garkein Problem, man verzichtet nur heute aus 
Bequemlichkeit darauf, weil eines der Mantras heutiger 
Softwareentwicklung ist, dass jedweglicher Speicher immer in beliebiger 
Grösse verfügbar ist.

Wenn ich z.B. die inch/mm-Umwandlung mit Multiplikationen ausführe und 
auch sonst nirgends was dividiere, verschwindet die Division automatisch 
aus der ausführbaren Datei. Ich habe das eigentlich auch immer für den 
Normalfall gehalten. Von Pascal, C oder Assembler ist das übrigens 
völlig unabhängig.

Georg

von Georg G. (df2au)


Lesenswert?

Georg schrieb:
> man verzichtet nur heute

Der alte Keil Compiler machte das auch. Indirekt aufgerufene Funktionen 
mussten explizit markiert werden, damit sie nicht weg optimiert wurden. 
Vermutlich ist das auch heute noch so.

von Axel S. (a-za-z0-9)


Lesenswert?

Georg schrieb:
> Wenn ich z.B. die inch/mm-Umwandlung mit Multiplikationen ausführe und
> auch sonst nirgends was dividiere, verschwindet die Division automatisch
> aus der ausführbaren Datei. Ich habe das eigentlich auch immer für den
> Normalfall gehalten. Von Pascal, C oder Assembler ist das übrigens
> völlig unabhängig.

Im Prinzip ja. Allerdings ist das eine Funktion des Linkers und für 
Assemblerprogramme wird der u.U. gar nicht aufgerufen. Aber selbst wenn 
- der Linker braucht hier Unterstützung. Man muß dann entweder jedes der 
einzeln linkbaren Häppchen als separates Objektcodefile in eine Library 
tun. So machen das Programmierumgebungen seit etlichen Dekaden.

Oder man geht noch einen Schritt weiter und nutzt das --gc-sections 
Feature des GNU Linkers. Dann muß man aber auch selber dafür sorgen, daß 
jede Funktion und die von ihr benutzten statischen Variablen in eine 
eigene passend benannte Section kommen. Der C/C++ Compiler macht das auf 
Anfrage automatisch. In Assembler muß man es händisch tun. Im Gegenzug 
bekommt man die Entscheidung, was am Ende in das Binary gelinkt wird, so 
fein aufgelöst wie es nur überhaupt geht.

Wie so oft zahlt es sich auch hier aus, wenn man mal hinter die Kulissen 
geschaut hat und weiß wie die Magie funktioniert. Leider wird allzu 
häufig das Gegenteil angepriesen. Man solle doch bitte die komfortablen 
IDE's benutzen, weil ohne wäre das doch alles viel zu kompliziert.

von Peter D. (peda)


Lesenswert?

Du mußt dich schon an die Syntax halten, die man unter C für 
Assemblerobjekte benutzt. Nur dann kann der Linker sie erkennen und 
wegoptimieren.
Um so ein Gerüst zu erhalten, kann man sich einfach in C eine 
Dummyfunktion schreiben und die nach Assembler compilieren lassen.

Ansonsten gilt für den Linker, alles was an Assembler dasteht, muß auch 
ins Hex-File.

In der Regel halten sich Assemblerprogrammierer aber nicht an 
Konventionen, sondern schreiben einfach drauflos und dann ist der Linker 
machtlos.

von Christian B. (casandro)


Lesenswert?

Georg schrieb:
> Christian Berger schrieb:
>> a) Unbenutzten Code automatisch zu entfernen ist quasi unmöglich.
>
> Komisch, das konnte mein Pascal-Compiler unter CP/M schon vor 30 Jahren,

Ja richtig, das ist auch Pascal, da ist vieles sehr viel einfacher. Das 
ist eine wohldefinierte Sprache, da kann der Compiler das machen. In C 
oder C++ geht das einfach nicht, weil Du ja keine Units hast sondern so 
eine Art "Modul" bei dem Du nur sehr schwer sagen kannst, ob Du nicht 
doch irgendwo einen Zeiger auf eine Funktion bekommst.

> Wenn ich z.B. die inch/mm-Umwandlung mit Multiplikationen ausführe und
> auch sonst nirgends was dividiere, verschwindet die Division automatisch
> aus der ausführbaren Datei. Ich habe das eigentlich auch immer für den
> Normalfall gehalten. Von Pascal, C oder Assembler ist das übrigens
> völlig unabhängig.

a) Assembler macht so was grundsätzlich nicht. Die ganze Idee hinter 
Assembler ist, dass der Assembler genau das macht was da steht.

b) Bestimmte Sprachen haben unterschiedliche Merkmale. In Pascal kannst 
Du zum Beispiel nur an die Stelle zurückspringen, von der eine Funktion 
oder Prozedur aufgerufen wurde. In C kannst Du an beliebige Stellen des 
Programms springen. Das macht das Problem deutlich komplexer.


Hier sind Übrigens Vorträge von Felix von Leitner. Der hat meines 
Wissens nach schon mal seine eigene Libc geschrieben und versteht somit 
grob was davon, was er macht:
http://www.fefe.de/source-code-optimization.pdf
http://www.fefe.de/c++/c%2b%2b-talk.pdf

von Georg (Gast)


Lesenswert?

Christian Berger schrieb:
> a) Assembler macht so was grundsätzlich nicht. Die ganze Idee hinter
> Assembler ist, dass der Assembler genau das macht was da steht.

Unsinn: der Assembler übersetzt wohl, was da steht, aber der Linker 
linkt nur das, was auch verwendet wird. Deine Kenntnisse müssen wohl 
älter sein als die 30 Jahre, die mein System alt ist - und mit Pascal 
hat das überhaupt nichts zu tun, du verstehst einfach nicht, wie Linken 
funktioniert. Es ist vollkommen wurscht, ob eine Routine in Pascal oder 
Assembler geschrieben ist, ich habe das immer nach Bedarf gemischt, und 
stell dir vor, der Linker von damals kommt damit zurecht. Irgenwelche 
Vorträge ändern daran nicht das geringste, sie sind höchstens falsch. 
Aber wahrscheinlich hast du sie einfach nicht verstanden.

Im übrigen ist das völlig trivial. Ich schreibe eine Routine, nenne sie 
Routine1, dann übergibt der Compiler oder Assembler (!!!) das 
unaufgelöste Symbol Routine1 an den Linker nur, wenn die Routine 
irgendwo aufgerufen wird, sonst eben nicht. Und der Linker sieht dann 
auch keinen Grund sie dazuzulinken.

Natürlich kann man absichtlich Blödsinn produzieren, z.B. in Assembler 
eine Routine als extern deklarieren, obwohl man sie nicht verwendet, 
aber warum sollte man sowas Dummes tun? Nur damit du recht behältst?

Georg

von Bastler (Gast)


Lesenswert?

Der Linker verbindet Teile. Dabei ist Grundvorausetzung für das 
Weglassen unbenützter Teile, daß es mehr als ein Teil gibt.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Ich glaube, wir sind uns hier alle einig, dass der Assambler (zumindest
der GNU-Assembler, um den es hier geht) keine Optimierungen vornimmt,
und dass der Linker prinzipiell zwar in der Lage ist, unbenutzte
Codeteile wegzulassen, dies aber nur dann tut, wenn er den Code
häppchenweise vorgesetzt bekommt.

Diese Häppchen sind entweder Module (d.h. separate Objektdateien) oder
Sections. Der Assemblerprogrammierer ist für die Aufteilung seines Codes
in Module oder Sections selber verantwortlich und erhält dabei so gut
wie keine Unterstützung durch den Assembler.

Genau diese Unterstützung ist aber wohl das, was sich der TE erhofft
hat, denn sein Code scheint aus einem einzigen Modul zu bestehen, das
aus der Quellcodedatei main.S mit per #include eingebundenen weiteren
Quellcodedateien gebildet wird. Da er vermutlich auch keine Aufteilung
in mehrere Code-Sections vornimmt, kommt das Ganze als ein einzelner
monolithischer Block beim Linker an, der deswegen nicht in der Lage ist,
unbenutzte Teile wegzulassen.

Auch in höheren Programmiersprachen ist die Modularisierung meist Sache
des Programmierers. Allerdings bieten hier einige Compiler Unterstützung
bei der Aufteilung in Sections. So stellt bspw. der GCC mit der Option
-ffunction-sections jede C-Funktion in eine eigene Section, um dem
Linker die Möglichkeit zu geben, unbenutzte Funktionen zu eliminieren.

So etwas ist aber in Assembler nicht möglich, da hier kein Zwang zur
Unterteilung des Programms in klar voneinander abgegrenzte
Unterprogramme besteht. Während man (und auch der Compiler) in einer
Hochsprache leicht entscheiden kann, welche Code-Teile zu einer Funktion
A gehören und welche zu einer Funktion B, ist dies in Assembler nicht so
ohne weiteres möglich, da dort nach Belieben kreuz und quer im Code hin-
und hergesprungen werden kann. Die Aufteilung in einzelne Sections muss
der Programmierer deswegen manuell vornehmen.

Es gibt also zwei Lösungen des Problems:

- Manuelle Modularisierung
- Manuelle Sektionierung

Genau diese beiden Alternativen hat der User asdfasd bereits ganz am
Anfang in seinen beiden Antworten genannt.

Ein optimierender Compiler hat zusätzlich noch die Möglichkeit der
Dead-Code-Elimination, bei der durch Kontrollflussanalyse Codeteile
ermittelt werden, die garantiert nie ausgeführt werden.

Mit Dead-Code-Elimination kann auch monolithischer Code optimiert
werden. Das Verfahren ist prinzipiell auch auf Assembler-Programme
anwendbar. M.W. gibt bzw. gab es optimierende Assembler, die diese
Optimierung implementiert haben. Der GNU-Assembler gehört aber nicht
dazu.

von Christian (dragony)


Lesenswert?

Hmmm mit welchem Weg fährt man denn erfahrungsgemäss besser? Sections 
oder Module? Vom Gefühl her würde ich jetzt auf Module tippen, aber mir 
fehlt da die Erfahrung.

Und wenn ich jetzt sagen wir den Code in 10+x Dateien aufgeteilt habe, 
wie gehe ich dann am besten weiter vor? Es sollte ja schon so sein, dass 
ich nicht für jede neue Datei das makefile ändern muss.

Leider habe ich bisher keine gut lesbare "best practice" Dokumentation 
zu diesem doch sehr spezifischen Problem gefunden. Die meisten nutzen 
halt IDEs und drücken F7 :p

von Yalu X. (yalu) (Moderator)


Lesenswert?

Der klassische Weg ist dieser:

asdfasd schrieb:
> Ach so: die übliche Methode, die überall funktioniert und nicht
> irgendwelche erweiterten Features der GNU-Toolchain braucht, ist die,
> jede optionale Funktion/Funktionenblock in eine eigene Datei zu packen,
> all diese kleinen Dateien in ein .a-Library packen und dieses dann beim
> finalen Linken anzugeben.  Der Linker sucht sich dann nur die Files
> heraus, die benötigt werden.  Geht immer, auf allen Systemen.

Diese Methode ist nur dann etwas ungeschickt, wenn du die verwendeten
Hilfsfunktionen oft änderst, da wegen jeder kleinen Änderung oft ein
Großteil der gesamten Bibliothek neu geschrieben werden muss. Aber im
Zeitalter schneller Festplatten ist das kein schwerwiegendes Problem.

Die Erstellung einer Bibliothek für die Hilfsfunktionen hat noch einen
weiteren Vorteil: Sind die Funktionen so generisch, dass du sie in
mehreren Projekten benutzen möchtest, legst für die Bibliothek ein
eigenes Projekt mit einem eigenem Makefile an. Die fertig gebaute
Bibliothek legst du bspw. als libauxfuncs.a an zentraler Stelle ab, wo
alle anderen Projekt Zugriff darauf haben.

Dann bindest du sie in jedes Projekt mit der Option -lauxfuncs ein.
Damit stehen eventuelle Verbesserungen und Erweiterungen der
Hilfsfunktionen sofort allen Projekten zur Verfügung. Du musst diese
dann nur neu bauen, brauchst aber keine Änderungen an den jeweiligen
Makefiles vorzunehmen.

Du hast damit etwas Ähnliches geschaffen wie die Standardbibliothek in
höheren Programmiersprachen. Da vom Linker immer nur die tatsächlich
benötigten Module in die Executables eingebaut werden, darf die
Bibliothek im Laufe der Zeit beliebig umfangreich werden, ohne dass dir
dadurch Nachteile entstehen.

: Bearbeitet durch Moderator
von Axel S. (a-za-z0-9)


Lesenswert?

Christian S. schrieb:
> Hmmm mit welchem Weg fährt man denn erfahrungsgemäss besser? Sections
> oder Module? Vom Gefühl her würde ich jetzt auf Module tippen, aber mir
> fehlt da die Erfahrung.

Module ist halt das, was schon immer funktioniert. Sections on demand 
linken ist ein Zusatzfeature des GNU Linkers. Das bringt tendenziell 
nochmal kleinere executables.

> Und wenn ich jetzt sagen wir den Code in 10+x Dateien aufgeteilt habe,
> wie gehe ich dann am besten weiter vor? Es sollte ja schon so sein, dass
> ich nicht für jede neue Datei das makefile ändern muss.

Warum nicht? Wenn du eine neue Datei anlegen kannst (eigentlich zwei, 
denn du willst die Funktionen ja sicher auch in einem Header-File 
deklarieren) dann kannst du ja im selben Zug auch das Makefile ändern. 
Für ganz Faule bietet GNU make aber auch Wildcards.

von asdfasd (Gast)


Lesenswert?

> Hmmm mit welchem Weg fährt man denn erfahrungsgemäss besser?

Ich würde immer Module nehmen.  Die Function-Sections sind hauptsächlich 
für automatisch generierten Code gedacht und sind mMn eher eine 
Notlösung (da erstmal alles immer generiert wird und im letzten Schritt 
dann ein Großteil wieder weggeschmissen wird).

> Und wenn ich jetzt sagen wir den Code in 10+x Dateien aufgeteilt habe,
> wie gehe ich dann am besten weiter vor? Es sollte ja schon so sein, dass
> ich nicht für jede neue Datei das makefile ändern muss.

Na ja, eine Datei in einem Makefile eintragen ist nicht so die Welt, 
aber wie Axel schrieb, mit gmake's Wildcards könnte ein Makefile so 
aussehen:
1
all: main
2
3
main: main.o lib.a
4
        cc -o main main.o lib.a
5
6
lib_SRCS:=$(wildcard lib-*.S)
7
lib.a: lib.a(${lib_SRCS:.S=.o})
8
9
clean:
10
        rm -f main main.o lib.a
11
12
.PHONY: all clean

main.S ist das Hauptprogramm und alle lib-xxx.S sind die Module.  Die 
"all" and "clean" targets nur zur Vollständigkeit.  Wenn einzelne Module 
allerdings selbst Build-Abhängigkeiten haben, kommst du nicht herum, das 
dem Makefile explizit mitzuteilen (z.B. lib-foo.S braucht ein 
foo-data.inc file, dann muss die Zeile "lib-foo.o: foo-data.inc" 
eingefügt werden).

Statt die Module lib-XXX.S zu nennen, kannst du sie auch in ein 
Unterverzeichnis packen: lib_SRCS:=$(wildcard lib/*.S).  Bei einigen 
Entwicklern ist dies sogar gängige Praxis, für jedes Unterverzeichnis 
ein Lib zu erstellen.  Die Unterverzeichnisse sind dann "stand alone", 
mit eigenem Makefile, die einfach nur ein subdir/lib.a erstellen.  Das 
Haupt-Makefile kümmert sich dann gar nicht mehr darum, welche .o-Files 
welches Unterverzeichnis bei welchen Optionen erzeugt - es linkt einfach 
das Hauptprogramm mit subdir1/lib.a subdir2/lib.a etc.

von Christian (dragony)


Lesenswert?

Vielen Dank für die sehr informativen Beiträge. Hat mir sehr geholfen. 
Ich werde mal ein paar der Ansätze ausprobieren und schauen, womit ich 
am besten fahre.

von Axel S. (a-za-z0-9)


Lesenswert?

asdfasd schrieb:
> Ich würde immer Module nehmen.  Die Function-Sections sind hauptsächlich
> für automatisch generierten Code gedacht und sind mMn eher eine
> Notlösung (da erstmal alles immer generiert wird und im letzten Schritt
> dann ein Großteil wieder weggeschmissen wird).

Das sehe ich ganz anders. Auch bei Libraries ist es ja genauso, daß sie 
erstmal alles enthalten, was eventuell gebraucht werden könnte und erst 
beim Linken entschieden wird, was denn nun wirklich gebraucht wird.

Wenn man einen Unterschied sehen will, dann liegt er eher darin, daß 
Libraries i.d.R. soweit unabhängig von dem sie verwendenden Programm 
sind, daß sie unabhängig übersetzt werden können. Sections im Programm 
hingegen werden bei jedem Übersetzungslauf wieder neu durch den Compiler 
gedreht, auch wenn sie sich nicht geändert haben.

Historisch war das einer der Gründe warum man Programme in viele kleine 
Übersetzungseinheiten modularisiert - damit ein Compilerlauf kürzer 
dauert. Bei heutigen Maschinen ist das weniger problematisch. Die sind 
im Vergleich zu früher rattenschnell.

Obwohl ich mir die Interna nicht angeschaut habe, würde ich sogar sagen, 
daß Sections sich genau die gleichen Mechanismen zu eigen machen wie 
Module. Man kann sich das vereinfacht so vorstellen, daß ein Modul (= 
Objektcodefile) genauso in Sections gesplittet wird wie eine Library in 
Module. Am Ende hat man einfach eine Menge an unabhängigen Objektcode- 
Schnipselchen, aus denen der Linker die benötigten heraussucht und 
zusammenfügt.

> Na ja, eine Datei in einem Makefile eintragen ist nicht so die Welt,
> aber wie Axel schrieb, mit gmake's Wildcards könnte ein Makefile so
> aussehen:

Yep. Ungefähr so hatte ich das gemeint.

von Bastler (Gast)


Lesenswert?

Es fehlt dem GAS (Gnu Assembler) eben die LTO Option. Mit der können 
GCC/GNU Linker in meist weniger als einer Sekunde entscheiden, welche 
Maschinenbefehle zur Lösung eines Problems benötigt werden. Daß sie 
dabei manchmal anders optimieren, als das der ASM-Programmierer gemacht 
hätte ... was soll's, der braucht auch etwas länger ;-)

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.