Hi Svenska! Bei deiner Beschreibung sind mir einige Sachen klar geworden. Aber es haben sich mir auch einige neue Fragen gestellt. Doch kurz vorab würde ich gerne noch etwas klären: Ich nehme mir bei meinen Projekten meist nur die grundlegenden Ideen von bereits bestehenden Prinzipien. Von da aus reiße ich gerne aus verschiedenen Konventionen aus. Ich probiere sehr gerne rum, daher wird das auch kein hundertprozentig akkurater Emulator
. (Zum Teil drücke ich mich auch einfach davor, einen richtigen Emulator zu schreiben, habe halt auf meine Weise dabei mehr Spaß
). Jetzt aber zu den Fragen:
Eine übliche Klassifikation unterscheidet zwischen RISC und CISC. Was das im Detail bedeutet, darüber kann man lange streiten, aber für die Diskussion hier vereinfache ich das mal so: In einem RISC-Prozessor hat jeder Befehl die gleiche Anzahl an Bits, in einem CISC-Prozessor nicht.
Könntest du dafür vielleicht ein Beispiele bringen (zu Befehlen mit unterschiedlich vielen Bits)? Oder meint die Anzahl der Bits eines Befehls "den Namen" des Befehls (ich meine jetzt nicht wortwörtlich den Namen wie z.B. "mov", sondern die Instruktion dahinter (<- da hast du mich ein kleines bisschen verwirrt, ist das nicht der Opcode?), z.B. (ausgedacht:) 0010 (ein 4-bit Befehl) und 01010010 (ein 8-bit Befehl))?
Eine weitere Klassifikation unterscheidet zwischen Stack- und Registermaschinen. Was du beschreibst, ist eindeutig eine Stackmaschine, da Operanden auf den Stack gelegt werden und von dort verarbeitet werden.
Ah, ja, deshalb wollte ich das am Anfang kurz klarstellen. Ich dachte mir tatsächlich, dass ich beide Varianten vereine. Wenn man "add" also ohne Parameter angibt (hab das im Beispiel glaube ich "adds" gennant, also dem einen eigenen Namen gegeben), wird der Stack als Zwischenspeicher usw. benutzt. Wenn man aber Parameter angibt, verhalten sich die Befehle anders. Das kann zwar für andere zu Verwirrung führen, aber für mich ist das so angenehm. Und am Ende bin ich ja der einzige der das Programm benutzt. Und wenn einem das nicht gefällt, muss man den Befehl "adds" ja nicht nutzen
.
Assembler als Programmiersprache ist normalerweise eine direkte Abbildung der Fähigkeiten einer CPU. Da es so kaum Architekturen gibt, die dein "mov 2,4" direkt abbilden können, ist das fast nie ein gültiger Befehl.
Deswegen war das auch nur Pseudocode. Wir rennen gerade komplett am Thema und der eigentlichen Frage vorbei. Das ist aber kein Problem, ich lerne dabei ja viel neues. Eigentlich hatte ich vor den Mov-Befehl so ähnlich aufzubauen:
syntax: mov <t1> <w1> <t2> <w2>
bsp.: mov 1 1 3 2 # al = 2
(Alle Werte werden in 8bit-Blöcke aufgeteilt: t1, t2, w1 und w2 sind allesamt 8bit lang, genau wie der mov Befehl an sich) t1/t2 würde dabei dafür stehen, welche Art von Daten als Nächstes kommen. w1/w2 sind die Werte. Wenn t1 0 ist, muss w1 eine Speicheradresse sein. Wenn t1 1 ist, muss w1 die ID eines Registers (ich würde denen dann halt IDs geben; hier wäre ax jetzt 1) beinhalten. Ist t2 1 -> w2=Register-ID; 2 -> Speicheradresse; 3 -> Zahl in den nächsten 8bit. Das Beispiel sieht also die 1, nimmt die nächste 1 als Register-ID (al), sieht die 3 und läd die darauf folgenden 8 bit in Register al. Aber das ist wahrscheinlich zu kompliziert, oder? Ich muss mir eh erstmal ein paar Befehlssätze anschauen und wie die das Problem lösen. Aber gerne würde ich trotzdem eine Meinung dazu hören.
Einfache Assembler bestehen z.B. aus einer Liste von Mustern, und die Eingangsdaten werden damit abgeglichen. Für einen Befehl (z.B. "mov ax, bx") wird dann ein passendes Muster gefunden (z.B. "mov <reg16>,<reg16>"), dann werden die Parameter in das Muster eingesetzt ("mov <reg16>,<reg16>" ist "1000100111SSSDDD", S=Quellregister, D=Zielregister, also generiert "mov ax,bx" ein "0x89 0xD8"). Wenn es kein passendes Muster gibt, dann war der Befehl ungültig.
(A1) Das nehme ich sehr gerne als Alternative! Das bedeutet dann, dass es verschiedene Mov-Befehle gibt, oder? Einer, der Zahlen in Register verschiebt; einer, der Register in Register schiebt; usw... Die haben dann alle einen unterschiedlichen Opcode, oder? Wenn ich das richtig verstanden habe, hört sich das für mich super lohnend an.
Was du als Pseudoassembler bezeichnest, geht eher in Richtung einer Hochsprache, weil du den Code noch transformierst, also grundlegend veränderst ("mov 2, 4" ist etwas anderes als "push 4; push 2; mov"). Das machen einfache Assembler nicht (Makroassembler lasse ich hier mal außen vor).
Das finde ich gar nicht schlimm. Am Ende soll der Compiler (da es ja anscheinend kein Assembler ist) meinen Code einfach nur in Opcodes umwandeln, die ich in den Lochstreifen stanze. Den Umweg über einen "wirklichen" Assembler lohnt sich meiner Meinung nach dann nicht wirklich. (Wahrscheinlich werde ich das Teil trotzdem weiter "Assembler" nennen, einfach weil ich es meistens vergessen werde "Compiler" zu nennen
. Ich versuche mich dran zu erinnern.)
Achte dabei mal auf folgende Fragen:
- Wie werden die Befehle codiert? Wie viele Bytes hat ein Befehl?
- Wie werden "immediates" (im Befehl hingeschriebene Zahlen, z.B. "mov r4, 4") codiert?
- Welche Adressierungsmodi gibt es, und wie sind sie beschränkt?
Die erste Frage kann ich schon beantworten: 8bit. Einfach, weil mein Lochstreifen 8 Löcher hat und ich jeden Befehl in einer Zeile haben möchte. Es ist für mich praktikabler und übersichtlicher, auch wenn es größer ist. Oder muss der gesamte Syntax eine feste größe haben? Muss also "push" (den ich jetzt beispielsweise 2 Bytes groß machen würde. Das 1. Byte wäre für push an sich und das 2. Bytes für das Argument, was zu pushen ist.) genau so groß sein wie "mov" (dem ich dann jetzt Beispielsweise einfach mal 3 Bytes Platz geben würde. Das 1. Byte für mov an sich, das 2. Byte für das Ziel und das 3. Byte für den Wert.)? (Beide Befehle waren reine Beispiele, mir ist klar, dass die Befehle am Ende anders aussehen und ein mov-Befehl auch nur 2 Bytes oder 1 Byte groß sein kann (siehe (A2) weiter unten). Ich wollte nur versuchen, das Prinzip mit der Länge eines Befehls, so wie ich es verstanden habe, auszudrücken.)
Zur zweiten Frage habe ich eine Gegenfrage: Meinst du mit der Codierung von Zahlen deren System (Hex/Dez/Bin, würde ich alle einbauen per 0x..., 0b... und 0d..., wobei Dezimal ohne Präfix Standard wäre), oder die Bit der Zahlen? Auch hier wäre die Antwort 8bit, wegen genannten Gründen. Oder meinst du etwas anderes?
Und zu den Adressierungsmodi muss ich mir noch echt Gedanken machen... Vielleicht entscheide ich das spontan nach Gefühl. Oder sollte ich mich an ein einziges Prinzip halten? Addressierungsmodi habe ich jetzt so verstanden, dass sie Aussagen, wie die Operanden interpretiert werden: Also ob eine gegebene Zahl bei z.B. "mov ax, 1", dass entweder 1 in ax geladen wird, oder ob die 1 dort für eine Speicheradresse steht. Und auch hier würde ich beides kombinieren. In NASM wird das doch mit [] geregelt. Ich bin auf die Darstellung mit @ gestoßen. Dann würde ich alle Immediates (<- so heißen doch konstante Zahlen im Programmcode?), die ohne @ stehen direkt als deren Wert nehmen. Wenn dann ein @ dazukommt, ist eine Speicheradresse gemeint. Also "mov ax, 1" setzt ax auf 1, während "mov ax, @1", ax auf die ersten 16bit im Speicher setzt (weil ax ja 16bit groß ist, wäre das gut, oder?). Apropos würde ich dazu gerne noch was sagen: Ich würde den Speicher auch in 8bit-Blöcke aufteilen. Man soll also nicht auf kleinere Einheiten zugreifen können. Dadurch würden also alle Speicheradressen als Vielfache von 8 umgerechnet werden. Schreibt man also "mov al, @2", würden der 2. 8bit-Block im Speicher in al (nicht 16bit sondern 8bit, weil al ja nur 8bit groß ist. Oder?) geladen werden. Wie findest du das?
(A2) Aber wieder habe ich etwas auf der Recherche (zu Adressierungsmodi) dazugelernt: Man kann ja den Programmcode kürzen, indem man häufige Instruktionen in einem Programmcode zusammenführt. In Kombination von deiner Erklärung zu (A1) könnte man aus Instruktionen wie "mov ax, bx" für jede Kombination von Registern einzelne Opcodes anfertigen, oder? Wäre natürlich vielleicht etwas zu viel, aber man könnte dann ja vielleicht Unterschiedliche auslegungen des Mov-Befehls machen, einer, der zum Beispiel gegebene Werte nach ax verschiebt, der Register nach ax verschiebt usw. Dann könnte ich (wenn "mov ax, bx" in Opcodes "0x01 0x02 0x03" (0x01=mov reg16 zu reg16; 0x02=ax; 0x03=bx) wäre) einfach ein Argument weglassen (z.B. wird dann aus "mov ax, bx" "0x04 0x03" (0x04=mov reg16 zu ax), ein Byte weniger, das gestanzt werden muss).
Wie du siehst, hast du es mit einem Dulli auf diesem Fachgebiet zu tun
. Ich hoffe, dass ich dich mit meinen Ideen und Fragen nicht vergraule. Ich denke gerne über alle meine Positionen nochmal nach. Und ich will mich nochmal für diesen Post entschuldigen, da er recht durcheinander ist und nur teilweise das Konzept, dass ich mir vorstelle, wirklich aufzeigen kann. Ich hoffe, dass du mir trotzdem Feedback dazu geben kannst.