Wie arbeitet ein Assembler?



  • Hallo,

    ich versuche zur Zeit einen Assembler für eine spezielle CPU-Architektur zu schreiben (in C++). Irgendwie stehe ich momentan vor einem geistigem Knoten.
    Wie ist ein vernünftiger (also keine simple Single-Pass Variante) Assembler aufgebaut? Wie arbeiten die intern?

    Mein Assembler soll erstmal nur aus einer Assembler-Source-Datei eine simple Binär-Datei erzeugen, später soll er auch linkbare Object-Dateien generieren können.

    Ich habe die Idee den Quell-Code (von Kommentaren befreit) als erstes in eine Objekt-Liste zu wandeln. Jedes Objekt präsentiert einen Befehl, ein Daten-Element, ein Label oder eine Steuer-Anweisung. Zusätzlich kennt jedes Objekt sein Position im Source-Code und die vorausgegangenen Dinge von denen es abhängt z.B. in welches Segment (.code oder .data oder .bss) es gehört.
    Aber was mache ich dann mit dieser Liste? Gibts da irgendwelche gut dokumentierten Vorbilder oder gute Tutorials an denen ich mich orientieren kann?

    Danke schon mal für alle hilfreichen Antworten!

    Grüße
    Erik



  • Diese Liste stellt im Prinzip eine Art Zwischencode dar. Vom Assembler Quelltext zur ausführbaren Datei sind eigentlich nur wenige Schritte nötig. Primär musst du einmal alle Abstraktionen, welche auf der Maschinenebene nicht mehr existieren, elimieren. Dazu gehört vor allem, dass du Labels auflöst, was dann in einer konkreten Adresse oder häufiger auch in einem IP/PC-Offset resultiert (also eine für die Maschine selbst eindeutige Zahl, welche im Kontext des Befehls, in welchem sie verwendet wird, dein Label identifiziert). Wenn du dies geschafft hast, kannst du dich grundsätzlich bereits daran machen, die Befehle von der abstrakten Repräsentation in deiner Liste in die Binärform der Zielarchitektur umzuwandeln (z. B. aus beq $t0, $t1, target hast du vorhin beq $t0, $t1, ¬adresse_von_target_als_zahl gemacht. Nun machst du daraus eine einzige Zahl, einen Befehl, den die Maschine versteht...). Diese Daten kannst du dann in eine Datei schreiben etc.

    Du kannst dir einen der unzähligen open source Assembler anschauen, aber einen einfachen Assembler hast du auch ohne Hilfe schnell zusammengebaut 🙂

    MfG



  • binutils und der GNU/Assembler wären die erste Anlaufstelle - diese wurden für jede nur erdenkliche Plattform und CPU portiert...



  • Hallo,

    erstmal Danke für Eure Antworten.

    Vom Assembler Quelltext zur ausführbaren Datei sind eigentlich nur wenige Schritte nötig. ...

    Das beruhigt mich erstmal. Ich hab jetzt schon eine weile überlegt was alles nötig ist um vom Zwischen-Code in die Binärform zu kommen und signifikant mehr als das Auflösen der Labels ist mir auch nicht eingefallen aber ich hatte immer das Gefühl etwas wichtiges übersehen zu haben.
    "Der Mensch denkt oft richtig aber nur selten vollständig!"

    Du kannst dir einen der unzähligen open source Assembler anschauen,

    Kannst Du mir da einen empfehlen? Alle die ich mir bisher angesehen hab sind bereits reichlich komplex so das es wohl ne Zeit dauern dürfte bis man genug durchgestiegen ist um nützliche/korrekte Informationen extrahieren zu können.
    Richtige Macro-Feature oder z.B. Strukturen möchte ich gar nicht unterstützen so das ich mich lieber an einem Exemplar der mittleren Komplexitätsstufe orientieren würde.

    aber einen einfachen Assembler hast du auch ohne Hilfe schnell zusammengebaut 🙂

    Das habe ich jetzt auch in Angriff genommen, bin grad am Parser für die Zwischencode-Liste.

    binutils und der GNU/Assembler wären die erste Anlaufstelle

    Das währe dann so ziemlich die komplexeste Variante der vorhandenen Assembler (oder in diesem Fall besser Tool-Chain). Die Einstiegs-Hürde für eine neue Target-Plattform ist IMHO enorm hoch. Das geht kaum um nur mal eben schnell ein unstrukturiertes Binär-File zu erstellen. Man benötigt mindestens noch ein ordentlich linkbares Object-File-Format. Da meine Ziel-CPU voll auf Segmentierung setzt könnte ich nichts von dem was in den binutils bereits drin ist (wie z.B. ELF oder a.out) verwenden, vieles wahrscheinlich noch nicht mal als Konzeptvorlage.

    diese wurden für jede nur erdenkliche Plattform und CPU portiert

    Ein Umstand der die binutils nicht gerade übersichtlicher macht. Dazu kommt das man damit praktisch jeden erdenklichen Anwendungsfall für "Dateien mit ausführbaren Programm-Code" abhandeln können soll.

    Ich versuche mich da jetzt selber durch zu kämpfen.

    Grüße
    Erik



  • erik.vikinger schrieb:

    ...Alle die ich mir bisher angesehen hab sind bereits reichlich komplex...

    Ich weiss nicht, wie komplex Dein Assembler werden soll, aber vielleicht wäre folgender Ansatz besser. Vielleicht kennst Du Blackfin DSPs Assembler und hast deren Syntax gesehen. Ungefähr kann man das in C++ nachbilden, die Register, CPU Operationen, Variablen in C++ Klassen kapseln, Operatoren wie =, +, ++ usw. überladen. Dann kann man mit diesen Klassen ein C++ Programm schreiben, das wie ein Assembler Programm aussieht. Ungefähr so:

    int main(int argc, char *argv[])
    {
    	CRegister R0;
    	CRegister R1;
    	CInputOutput Port;
    	CVariable MeineVar;
    
    	R0 = 10;
    	R1 = R0;
    	--R1;
    	Port = R0;
    	MeineVar = R0;
    	MeineVar--;
    
    	return 0;
    }
    

    Die Operatoren überschreibst Du so, dass sie richtige Operationscodes ausgeben, in eine Datei schreiben o.ä, also was immer gewünscht.
    Dann kompiliert man das Programm und lässt es am PC laufen, das Programm läuft und erstellt für Dich eine exe oder Objektdatei o.ä.
    Vorteil: Man benutzt fertigen Compiler 😉



  • Hallo,

    Ich weiss nicht, wie komplex Dein Assembler werden soll

    Nicht allzu komplex. Er soll vor allem eine lineare Kette von Assembler-Befehlen in ihre Binär-Form übersetzen, dazu Labels auflösen und Variablen (also Labels in Nicht-Code-Segmenten) verwalten. Später sollen noch ".extern" und ".public" dazu kommen um linkbare Object-Files zu erstellen.

    Vielleicht kennst Du Blackfin DSPs Assembler und hast deren Syntax gesehen.

    Nein, kannst Du ein hübsches Beispiel verlinken?

    Ungefähr kann man das in C++ nachbilden, ....

    Deinen Vorschlag verstehe ich irgendwie nicht. Sollen die Operator-Methoden quasie meine Zwischencode-Liste erzeugen? Das ist vielleicht etwas einfacher als ein Parser aber danach müsste doch trotzdem das selbe Procedere kommen. Außerdem wüsste ich nicht wie ich diesen Operator-Methoden zusätzliche Parameter für den zu erzeugenden Assembler-Befehl mitgebe. Die meisten Befehle meiner CPU haben 3 oder 4 Parameter, dazu kommt das jeder Befehl bedingt ausführbar ist und viele Befehle die Größe (8Bit, 16Bit, 32Bit oder 64Bit) ihrer Parameter wissen müssen.

    Meine Ziel-CPU ist der ARM-Architektur nicht unähnlich, kennt z.B. auch Shift-Operanden.

    Grüße
    Erik



  • erik.vikinger schrieb:

    Nein, kannst Du ein hübsches Beispiel verlinken?

    Hier z.B, eine Assembler Datei für einen Blackfin DSP: http://www.google.com/codesearch/p?hl=de&sa=N&cd=1&ct=rc#3C3saXwSWsQ/trunk/linux-2.6.24.3/arch/blackfin/mach-bf537/head.S&q=blackfin startup

    erik.vikinger schrieb:

    Deinen Vorschlag verstehe ich irgendwie nicht. ...

    Dann lass es sein 🙂 Ist nur ein Vorschlag. Es müssen nicht unbedingt Operatoren sein, "einfache" Funktionen z.B.:

    void CCpu::mov(CRegister dst, int param);
    void CCpu::mov(CRegister dst, int param1, int param2);
    void CCpu::mov(CRegister src, CRegister dst);
    

    Also irgendwie so...



  • Hallo,

    Hier ...

    Danke. Sieht sehr nett aus. An diese Syntax könnte ich mich durchaus gewöhnen. Vielleicht baue ich das in meinen Parser auch ein. 🙄

    Ist nur ein Vorschlag.

    Schon klar. Ich bin Dir auch dankbar für Deine Mühe aber so wirklich verstanden hab ich, glaube ich, immer noch nicht was Du eigentlich meinst.
    Soll ich (bzw. der Benutzer meines Assemblers, also hoffentlich mal der GCC) ein C++ Programm schreiben/erzeugen das so ähnlich aussieht wie ein "normaler" Assembler-Quell-Code und diesen (zusammen mit ner speziellen Header-Datei o.ä.) durch nen Compiler schicken und dessen Executable generiert mir dann das gewünschte Object(Binär)-File? 😕
    Wie gesagt, das einzigste was mir das spart ist der Parser der manuell aus "normalem" Assembler-Quell-Code die interne Zwischencode-Liste generiert (an dem bin ich grad dran und es geht auch einigermaßen vorwärts). Diese Zwischencode-Liste würde mit Deinem Vorschlag durch die Methoden generiert. (Hab ich das richtig verstanden?) Das weiterverarbeiten dieser Liste (z.B. das Auflösen der Labels) bleibt IMHO in jedem Fall erhalten und das ist wahrscheinlich das schwerste am ganzen.

    Grüße
    Erik



  • erik.vikinger schrieb:

    Soll ich (bzw. der Benutzer meines Assemblers, also hoffentlich mal der GCC) ein C++ Programm schreiben/erzeugen das so ähnlich aussieht wie ein "normaler" Assembler-Quell-Code und diesen (zusammen mit ner speziellen Header-Datei o.ä.) durch nen Compiler schicken und dessen Executable generiert mir dann das gewünschte Object(Binär)-File? 😕

    Ja! 🙂 Genau das meine ich damit. Aber das wäre natürlich nichts für einen GCC...

    erik.vikinger schrieb:

    Wie gesagt, das einzigste was mir das spart ist der Parser der manuell aus "normalem" Assembler-Quell-Code die interne Zwischencode-Liste generiert (an dem bin ich grad dran und es geht auch einigermaßen vorwärts). Diese Zwischencode-Liste würde mit Deinem Vorschlag durch die Methoden generiert.

    Ja! 🙂 Und damit ersparst Du nicht nur einen Parser, der Compiler prüft ja z.B. ob die Parameter vom Typ passen usw. oder ob es überhaupt eine "mov" Operation gibt, wie Du sie im Quellcode eingegeben hast. Also, insgesamt hast Du eine Fehlerbehandlung, die vom Compiler gemacht wird und Dein "Quasi"-Assembler-Programm kompiliert nicht, wenn Fehler drin sind...

    erik.vikinger schrieb:

    (Hab ich das richtig verstanden?)

    Ja! 🙂

    erik.vikinger schrieb:

    Das weiterverarbeiten dieser Liste (z.B. das Auflösen der Labels) bleibt IMHO in jedem Fall erhalten und das ist wahrscheinlich das schwerste am ganzen.

    Ja, das muss man dann irgendwie noch implementieren, am Besten auch gekapselt durch ein Objekt, das alle Labels, alle Adressen von Funktionen kennt o.ä. Variablen kann man vielleicht so machen, dass sie ihre Adressen selbst kennen.
    Das wäre mein Vorschlag.



  • Hallo,

    Genau das meine ich damit.

    Die Idee ist strange aber gefällt mir irgendwie. Ich werde es aber trotzdem erst mal mit nem klassischen Parser versuchen. Trotzdem Danke für Deine Gedanken.

    Aber das wäre natürlich nichts für einen GCC...

    Nunja, es währe auf jeden Fall recht ungewöhnlich einen vom GCC erzeugten Assembler-Quelltext noch mal vom GCC verarbeiten zu lassen. Für dieses Vorgehen bräuchte man aber eben auch 2 GCC, einen für das eigentliche Target und zusätzlich einen für den verwendeten Host. Eine Cross-Compiler-Tool-Chain ist eigentlich schon komplex genug.

    Also, insgesamt hast Du eine Fehlerbehandlung, die vom Compiler gemacht wird ...

    Der Gedanke ist reizvoll aber es gibt noch mehr als nur einfache syntaktische Fehler. Nicht jeder Befehl kann mit jeder Kombination an Registern umgehen oder was ist wenn ein Speicherzugriff nur ein Byte laden soll obwohl die Variable als Word definiert wurde. Ich denke einen Großteil der möglichen Fehler muss ich trotzdem manuell finden, nachdem die Zwischencode-Liste aufgebaut wurde.

    Grüße
    Erik



  • @erik.vikinger:
    Also ein nicht-optimierender nicht-makro Assembler sollte nun wirklich kein so grosses Ding sein, dass man dafür so einen ... seltsamen Zwischenschritt brauchen würde.

    Im Prinzip tut das Ding dann ja nicht mehr als ein paar Adressen/Offsets zu verwalten, und Mnemonics (fast) 1:1 in Binärcode zu übersetzen.



  • Hallo,

    hustbaer schrieb:

    Also ein nicht-optimierender nicht-makro Assembler sollte nun wirklich kein so grosses Ding sein

    Prinzipiell hast Du sicher recht aber mein Assembler wird (später) schon einiges optimieren müssen wenn dabei vernünftiger Binär-Code raus kommen soll. Die meisten Befehle gibt es in unterschiedlichen Varianten die jeweils unterschiedlich Groß sind und damit auch unterschiedlich flexibel, trotzdem sieht der Mnemonic immer gleich aus und der Assembler muss entscheiden welchen Binär-Code er benötigt. Bei der Addition z.B. gibts die Varianten r0 := r0 + r1 (ein kleiner 2 Operanden-Befehl : "add.w r0,r0,r1") oder r0 := r1 + r2 (ein größerer 3 Operanden-Befehl : "add.w r0,r1,r2"). Noch komplexer wird es bei Sprüngen, dort muss der Assembler schauen wie weit das Ziel entfernt ist (was von der Anzahl und Größe der dazwischen liegenden Befehle, was wiederum auch Sprünge sein können deren Größe erst ermittelt werden muss, abhängt) um zu ermitteln obs ein kleiner Sprung-Befehl tut oder ob doch ein größerer Sprung-Befehl, mit mehr Reichweite, ran muss. Da gibts übrigens meistens 3 verschiedene Größen so das die Auswahl schon recht komplex werden kann.

    hustbaer schrieb:

    seltsamen Zwischenschritt

    Was meinst Du mit seltsam? Meine Zwischencode-Liste?

    hustbaer schrieb:

    und Mnemonics (fast) 1:1 in Binärcode zu übersetzen

    Selbst auf den meisten Primitiv-Instruction-Set-Architectur-CPUs (wie z.B. die 8Bit AVR von Atmel) ist das oft mehr als nur "fast 1:1". Ein Blick in die äußerst umfangreichen Sourcen der binutils lässt erahnen wie komplex so eine scheinbar simple Aufgabe in Wirklichkeit sein kann.

    Grüße
    Erik



  • Mit "seltsamen Zwischenschritt" meinte ich den Vorschlag von abc.w. Also quasi erst ein C++ Programm erstellen, welches dann compiliert und ausgeführt wird, und dann den Assembler-Code ausspuckt.

    Was den Rest angeht: ich verstehe was du meinst. Trotzdem würde ich sagen dass das alles halbwegs trivial ist.



  • hustbaer schrieb:

    und dann den Assembler-Code ausspuckt.

    Kleine Korrektur: und dann Objekt-Code oder eine "exe" ausspuckt.

    Und ich würde den "seltsamen Zwischenschritt" als "Quasi-Assembler Programm/Generator" bezeichnen 😃



  • Hallo,

    hustbaer schrieb:

    Trotzdem würde ich sagen dass das alles halbwegs trivial ist.

    Diese Einschätzung hatte ich auch mal, heute denke ich darüber anders.

    abc.w schrieb:

    als "Quasi-Assembler Programm/Generator" bezeichnen 😃

    Falls ich später mal Zeit hab, wenn mein Assembler einigermaßen funktioniert, kann ich ja mal probieren sowas umzusetzen. Allzu schwer sollte es hoffentlich nicht sein den Parser gegen was anderes auszutauschen. Das einzigste was mich an dieser Variante stört ist das man den Assembler-Quell-Text kompilieren und ausführen muss (auf dem verwendeten Host-System) damit die Binär-Daten raus kommen.

    Grüße
    Erik



  • abc.w schrieb:

    hustbaer schrieb:

    und dann den Assembler-Code ausspuckt.

    Kleine Korrektur: und dann Objekt-Code oder eine "exe" ausspuckt.

    Und ich würde den "seltsamen Zwischenschritt" als "Quasi-Assembler Programm/Generator" bezeichnen 😃

    rofl 😃
    Ja, sorry, mein Fehler. Natürlich meinte ich Objekt-Code/Binary/Byte-Code 🙂



  • Dieser Thread wurde von Moderator/in rüdiger aus dem Forum Rund um die Programmierung in das Forum Assembler verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.


Anmelden zum Antworten