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.
Kannst du ja auch machen. Muss ja nicht perfekt sein.
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)?
Jeder AVR-Befehl besteht aus 16 Bits (=1 Flash-Word, bzw. 2 Bytes). Jeder ARM-Befehl hat 32 Bits (=1 Word). Ein 8086-Befehl hat mindestens 1 Byte (sowas wie "CLI") und maximal 6 Byte (sowas wie "MOV [BX+0x85], 0xAC24"). Dazu kommen dann noch eventuelle Präfixe (wie "LOCK", "ES:" oder "REP").
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.
Das kannst du zwar machen, aber damit verkomplizierst du nur das System und gewinnst nichts. Der Vorteil einer Stackmaschine ist, dass du nur ein Register (den Stackpointer) für die Verwaltung brauchst. Da auf einer Stackmaschine alle Operationen implizit auf dem Stack arbeiten, brauchst du in den Befehlen keine Operanden codieren und bekommst damit einen sehr platzeffizienten Befehlssatz.
Wenn du jetzt zusätzliche Befehle einbaust, die auf einem Registerfile arbeiten (auch wenn die den gleichen Namen haben, haben sie andere Opcodes, sonst könnte die CPU sie nicht unterscheiden!), dann musst du die jeweiligen Quell- und Zielregister mit eincodieren. Dann kannst du auch gleich eine Registermaschine bauen und auf den Stack verzichten. Das einzige, was du mit "ich kann aber beides" erreichst, ist mehr Aufwand für die Hardware, mehr Aufwand für die Software, und mehr Aufwand für irgendwelche Compiler. Niemand hindert dich daran, in einer Registermaschine einen Stack zu unterstützen - allerdings kannst du dann gleich mehrere davon haben.
Als Beispiel: Ein 8086 bindet den Stack ziemlich fest an das SP-Register (PUSH/POP arbeiten damit). Auf ARM hingegen kannst du (fast) jedes Register als Stackpointer nehmen, weil das keine Sonderbefehle sind, sondern Adressierungsmodi (post-increment bzw. pre-decrement). Das sind aber keine Stackmaschinen. Wenn du z.B. einen FORTH-Interpreter bauen willst (das ist eine stackbasierte Programmiersprache), dann willst du eigentlich zwei Stacks in Hardware unterstützen.
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.
Naja, du hast es "Pseudoassembler" genannt.
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.
In deinem Beispiel wäre jeder MOV-Befehl also 5 Byte lang, richtig? Das kann man machen, ist aber ziemliche Platzverschwendung.
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.
Genau das ist die Idee. Der Prozessor betrachtet die alle als vollkommen unterschiedliche Befehle, aber für den Programmierer sind das alles MOVs. Der Assembler muss dann raussuchen, welche Variante er nimmt.
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.
Du wirst ihn aber wahrscheinlich trotzdem haben, auch wenn das nur verschiedene Funktionen im gleichen Programm sind. Der Punkt ist, dass ein Assembler ein recht einfaches Programm ist (eben weil er einfach nur 1:1 von Assemblerbefehlen in Maschinenbefehle umwandelt), während ein Compiler - oder dein Transformator - beliebig komplex werden kann. Stichwort "Optimierung".
Im Übrigen erzeugst du keine "Opcodes", sondern "Befehle" (Instruktionen). Der Opcode sind nur ein paar Bits davon. Wieviele und welche, legst du fest.
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.
Lochstreifen sind nicht die zuverlässigste Technologie - nicht jetzt, und auch nicht vor 50 Jahren. Übliche Lochstreifensysteme sind daher 7 Bit, und das 8. Bit ist ein Paritätsbit, damit du wenigstens weißt, wenn dein Lesegerät falsch gelesen hat.
Es ist für mich praktikabler und übersichtlicher, auch wenn es größer ist. Oder muss der gesamte Syntax eine feste größe haben?
Das ist das, was ich oben meinte: Du kannst einen Befehlssatz bauen, wo jeder Befehl die gleiche Anzahl Bits hat (und das muss auch nicht ein Lochstreifenelement sein, das können auch immer zwei sein). Oder du baust einen Befehlssatz, wo verschiedene Befehle unterschiedlich viele Bits haben. Der Vorteil ist, dass du deinen Befehlssatz komplexer und damit ausdrucksstärker machen kannst. Der Nachteil ist, dass deine Hardware wesentlich komplizierter wird. Einen Teil könntest du ausgleichen, indem du geschickt codierst.
Beispiel: Der Lochstreifen besteht aus 7-Bit-Elementen (plus Parität). Du hast also Werte zwischen 0x00 (_000 0000) und 0x7F (_111 1111); der Unterstrich ist das Paritätsbit. Ein Befehl besteht aus 1-3 Elementen, wobei das erste Element immer das erste Bit gleich 1 setzt, also 0x40 (_100 0000) bis 0x7F (_111 1111). Alle folgenden Elemente haben das erste Bit immer gleich 0, also 0x00 (_000 0000) bis 0x3F (_011 1111). Um die Daten einzulesen, nimmst du ein Schieberegister mit 24 Bit. Immer, wenn dein Lochstreifenleser ein Byte liest, schiebst du das Byte hinten in das Schieberegister rein. Die Paritätsbits ignorierst du, also gehen aus dem Schieberegister 21 Datenleitungen raus. Wenn das erste Bit im Register eine "1" ist, dann kann deine Schaltung die folgenden 20 Bits nehmen und den Befehl ausführen. (Wenn nicht alle Bits genutzt werden, ist das auch egal. Außerdem kannst du Nullbytes als Füllbytes zwischen Befehle stopfen.)
Es gibt sicherlich auch andere Ansätze. Der Gedanke ist, dass du einen Datenstrom aus Bytes vom Lochstreifen vorne in die Schaltung reinkippst, und die Schaltung selbstständig rausfindet, wo ein Befehl anfängt (Stichwort "selbstsynchronisierender Code"). Wenn du das nicht machst und du schiebst den Lochstreifen schlecht rein (so dass er in der Mitte eines Befehls anfängt), dann macht dein Prozessor wahrscheinlich Unsinn und fängt sich auch nicht wieder.
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?
Ich meine etwas anderes. Schau dir mal an, wie ein "MOV R4, 0x12" auf einem ARM-Prozessor codiert wird, und "MOV A, 0x12" auf einem i8080: Bei ARM ist jeder Befehl 32 Bit breit, und die "0x12" sind einfach 8 Bit davon (die restlichen Bits sind z.B. das Zielregister, und der Opcode "MOV IMMEDIATE INTO REGISTER"). Das heißt aber auch, dass du 0x12345678
nicht mit einem Befehl in ein Register schreiben kannst, weil das 32 Bit bräuchte und die hast du nicht! Auf dem i8080 ist das kein Problem: Das erste Byte ist ein "MOV IMMEDIATE INTO A", das zweite Byte dann 0x12.
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?
Gefühl ist kacke. Du baust auch kein Haus "nach Gefühl". Solltest du zumindest nicht.
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.
Jaein. Was du meinst, sind üblicherweise verschiedene Opcodes ("MOV IMMEDIATE INTO REGISTER" bzw. "MOV INDIRECT INTO REGISTER"). Was ich meine ist, welche dieser Opcodes existieren.
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?
Du beschreibst die Syntax der Assemblersprache im Editor auf dem Entwicklungssytem. Das hat nur wenig damit zu tun, wie die CPU die einzelnen Bits interpretiert, die da rauskommen. Will sagen: Nein, die CPU sieht weder "[]" noch "@" noch "0x", sondern die sieht nur Bits.
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.
Ja. Das Ergebnis sind dann zusätzliche Befehle, und das kann man beliebig weit treiben. Aktuelle Architekturen haben gern mal >5000 verschiedene Befehle, der MIPS I-Befehlssatz hatte 31. Faustregel: Je mehr Befehle du im Befehlssatz hast, desto weniger Befehle brauchst du in einem Programm für deinen Algorithmus (= Programme sind kleiner). Aber je mehr Befehle du im Befehlssatz hast, desto komplexer ist dein Prozessor (= Programme sind langsamer). Was optimal ist, hängt von der Anwendung ab.
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?
Richtig. Aber das nennt man nicht "dann mache ich viele neue Opcodes", sondern das nennt man "Operanden".
Beispiel: Nehmen wir einen Prozessor mit 4 Registern (A, B, C und D), und 16 Befehlen insgesamt. Die ersten 4 Bits sind der Opcode, die nächsten 2 Bits sind der Zieloperand, die nächsten 2 Bits sind der Quelloperand. Macht 8 Bits (=1 Byte) pro Befehl. Nehmen wir als XOR den Opcode 4, dann ergeben sich die folgenden Kombinationen:
0x40 = 0100 0000 = XOR A,A
0x41 = 0100 0001 = XOR A,B
0x42 = 0100 0010 = XOR A,C
0x43 = 0100 0011 = XOR A,D
0x44 = 0100 0100 = XOR B,A
...
0x47 = 0100 0111 = XOR B,D
0x48 = 0100 1000 = XOR C,A
...
0x4F = 0100 1111 = XOR D,D
Für ADD mit Opcode 8 sieht das dann so aus:
0x80 = 1000 0000 = ADD A,A
0x81 = 1000 0001 = ADD A,B
...
0x8F = 1000 1111 = ADD D,D
Ich hoffe, du siehst da ein System drin.
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.
Nönö. Ich bin nur nicht jeden Tag im Forum unterwegs, daher dauert das mit den Antworten auch mal länger.
Der Punkt ist, was du eigentlich willst, ist eine Prozessorarchitektur entwerfen. Das ganze Lochstreifengerödel ist eigentlich nebensächlich.
Im Übrigen hast du noch ein Problem komplett vernachlässigt: Wenn du direkt Code vom Lochstreifen ausführen willst - wie willst du mit Sprüngen (und damit auch Schleifen) umgehen? Da folgt auf einen Befehl ja nicht "der nächste" Befehl, sondern irgendein anderer. Alle Computer, von denen ich weiß und die mit Lochstreifen umgehen können, lesen den Lochstreifen immer komplett in den Arbeitsspeicher ein und führen den dann von dort aus. Und wenn der Code erstmal im Speicher ist, ist es ja auch egal, ob er nun via Lochstreifen, Festplatte oder Funk da reingekommen ist. Du kannst auf Sprünge und sowas auch verzichten, aber dann ist das Teil nicht mehr turingmächtig, d.h. du kannst keine beliebigen Programme mehr dafür bauen.
Und dann kommt noch eine Frage: Was willst du eigentlich für Speicher nehmen? Oder soll dein System sowohl Lochstreifenstanzer und Lochstreifenleser haben, und du mischst Code und Daten auf dem Lochstreifen? Das verbraucht eine Menge Papier (und produziert Unmengen an Konfetti - und Lochstreifenkonfetti ist eklig aufzuräumen).