Autor Thema: Lochstreifen und Opcodes  (Gelesen 13958 mal)

Survari

  • Beiträge: 11
    • Profil anzeigen
Gespeichert
« am: 02. June 2020, 19:22 »
Hallo :). Lange ist es her, seit ich auf dieser Seite mal vorbeigeschaut habe. Derzeit grabe ich in den Tiefen der Computergeschichte und bin auf die Lochstreifen gestoßen. Irgendwie haben mich die so fasziniert, dass ich auf die Idee gekommen bin, einfach mal selber welche zu machen und ein Lesegerät dafür zu bauen: Und dann ging es weiter... Natürlich hab ich mir die Frage gestellt, was ich denn eigentlich mit den Lochstreifen machen will und dachte da dann, man könnte sich ja mal an 'ner Prozessor-Emulation wagen und dann dafür Programme schreiben :roll: .

Ich will später einen Assembler bauen, der dann kleine Programme kompiliert und mir zeigt, wo ich welche Löcher stanzen muss :-D . Jetzt aber zum eigentlichen Problem: Ich habe Probleme beim Verstehen von Opcodes. Und mehr Lowlevel als per Hand Opcodes schreiben geht wohl nicht! Da muss ich mich doch an dieses Forum hier wenden :wink: .

Mal ein Beispiel an einem Pseudoassembler:

mov 2, 4 # speicher an stelle 2 auf 4 setzen
Die obige Zeile kann ich auch in Pseudo-Opcodes für meinen Emulator übersetzen (hier einfach mal Beispielsweise wären die in Dezimal dann "1 2 4" oder so). Das Problem ist aber so ein Beispiel:

mov 1, [1+1] # wert von speicherstelle 2 (= 1+1) nach speicherstelle 1
So ähnlich kann man das ja in NASM machen. Mein Problem dabei: Wie stelle ich das als Opcode dar? Einen Ansatz habe ich bereits: Ich habe bereits einige Pseudoassembler und Programmiersprachen geschrieben (nur recht kleine). In einer handhabe ich das so, dass alles, was in [] steht, als mathematischer Ausdruck gilt ([] haben da eine andere Bedeutung als die [] in NASM) und vom Compiler per einfachem Shunting-Yard-Algorithmus in neue Befehle übersetzt wird, die dann den Stack als Zwischenspeicher der Ergebnisse nutzen. Ach, am Besten ich zeig einfach ein Beispiel:

println [ 2*4+1 ]

Würde übersetzt werden (auch wieder nur Pseudo-Code):

push 2 # 2 auf den stack
push 4 # 4 auf den stack
mul # 2 obersten werte vom stack nehmen, multiplizieren, ergebnis auf den stack
push 1 # 1 auf den stack
add # 2 obersten werte vom stack nehmen, addieren, ergebnis auf den stack
println $!  # $! ist hier einfach ein operator, der den obersten wert vom stack löscht und ihm den befehl übergibt. alle []-ausdrücke werden durch $! ersetzt, da die Ergenisse immer auf dem Stack liegen

Wenn ich das oben mit meinem Pseudo-Assembler für den Emulator kombiniere, würde aus mov 1, [1+1] Folgendes:

push 1
push 1
adds # befehl zum addieren von den obersten 2 stackelementen, ergebnis auf den stack
mov 1, [sp] # adresse des obersten stackelements auslesen und dessen wert nach speicherstelle 1

Das könnte ich dann ja übersetzen (IDs: mov=1, push=2, adds=3): 2 1 2 1 3 1 2 [sp] (Für die Darstellung von [sp] denke ich mir noch was aus, auch da gilt: Wenn ihr Ideen habt, sagt gerne was!)

Meine Frage wäre daher: Wie machen Assembler, wie zum Beispiel NASM, das? Und wie findet ihr meinen Ansatz? Habt ihr eigene Ideen? Allgemeine Kommentare und Hinweise zum Projekt?

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #1 am: 04. June 2020, 00:35 »
Hallo,

was du eigentlich beschreibst, sind nicht "Opcodes", sondern der grundlegende Befehlssatz eines Prozessors. Das ist der Teil, der unter "Prozessorarchitektur" fällt (die genaue Implementation in Hardware ist dann die "Mikroarchitektur"). Zu einem Befehl gehört mehr als nur der Opcode selbst. Es gibt verschiedene Ansätze, wie man sowas bauen kann, und wenn man eine Architektur verstanden hat, dann hat man effektiv alle Architekturen verstanden, die den gleichen Ansatz benutzen. Daher ist es sinnvoll, erstmal grob zu klassifizieren, was man vor sich hat.

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. Die meisten "modernen" Architekturen (ARM, AVR, MIPS, RISC-V, ...) zählen zu den RISC-Architekturen, x86 und PowerPC sind CISC-Architekturen.

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. Stackmaschinen kann man sehr einfach bauen und beschreiben, aber da der Stack im Speicher liegt und alle Operanden durch den Stack müssen, begrenzt der Speicher die erreichbare Geschwindigkeit solcher Systeme. Die bekannteste Stackmaschine dürfte der Java-Bytecode sein, und vielleicht noch ZPU (dafür gibt es immerhin einen GCC-Port), aber Registermaschinen haben gewonnen.

Das alles vorweg.

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. (Das Stichwort dafür sind "Adressierungsmodi", und weil x86 davon besonders viele hat, ist es eher die Ausnahme als die Regel.)

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

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.

Auch hier: x86 ist der Außenseiter, weil es mehrere Möglichkeiten gibt, Befehle zu codieren (dann muss sich der Assembler für eine Variante entscheiden), die Befehle sind unterschiedlich lang, und die Anzahl der Muster ist durch die komplexe Befehlsstruktur (Opcode, Modbyte, evtl. weitere Dinge - ein einzelner 8086-Befehl hat zwischen 1 und 6 Bytes!) enorm groß.

Ich würde dir einfach mal empfehlen, eine einfache Rechnerarchitektur im Detail anzuschauen. Und damit meine ich nicht x86 (ich bastle an einem 8086-Emulator und die Codierung ist einfach nur eklig). Recht einfach (sprich: primitiv) ist der 8080, die gesamte Opcode-Tabelle dafür gibt es hier. Auch recht verständlich fand ich RISC-V (Spezifikation gibt es hier), wenn man sich auf RV32I/RV64I beschränkt, und wenn du dich mit echter, günstiger, verständlicher Hardware prügeln willst, dann kannst du dich mit dem AVR-Befehlssatz befassen (den gibt es hier). Oder natürlich die passenden Zusammenfassungen, Blogs, Videos und was es dazu so alles im Internet gibt.

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?

Ich glaube, dann verstehst du etwas besser, wie so ein Befehl aufgebaut ist und was davon der Opcode ist (und was du sonst noch brauchst). :-) Und dann können wir weiterdiskutieren.

Gruß,
Svenska

Survari

  • Beiträge: 11
    • Profil anzeigen
Gespeichert
« Antwort #2 am: 04. June 2020, 23:45 »
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 :roll:. (Zum Teil drücke ich mich auch einfach davor, einen richtigen Emulator zu schreiben, habe halt auf meine Weise dabei mehr Spaß :-D ). Jetzt aber zu den Fragen:

Zitat
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))?

Zitat
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 ;).

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

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

Zitat
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 :roll:. Ich versuche mich dran zu erinnern.)

Zitat
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 :-D . 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.
« Letzte Änderung: 05. June 2020, 15:31 von Survari »

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 09. June 2020, 01:01 »
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. :-)

Zitat
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").

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

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

Zitat
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 :-D . 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).

Survari

  • Beiträge: 11
    • Profil anzeigen
Gespeichert
« Antwort #4 am: 09. June 2020, 17:54 »
Zitat
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").

Ah, okay! Das bedeutet auch, dass "MOV [BX+0x85], 0xAX24" ein eigener Befehl ist und das [BX+0x85] nicht vom Assembler umgewandelt, sondern direkt zum Befehl gehört? Dieser spezielle Mov-Befehl beinhaltet also das Addieren der ersten beiden Argumente? Dann hätte sich meine Ursprunsfrage geklärt.

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

Nehme ich so (wie beim 8086, weil ein Stack ja ziemlich nützlich sein kann), da sich meine ursprüngliche Frage, die mein einziger Grund für das Einbauen einer Stackmaschine war, sich ja geklärt hat. Aber ich denke, dass ich ihn dann auch nur als Zwischenspeicher nutzen werde (also keine Extra-Befehle, die nur mit dem Stack arbeiten, außer es gibt einen guten Grund dafür). Dann reichen push und pop als Befehle.

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

Gut, das habe ich dann verstanden. Das ist ziemlich nützlich.

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

Da hast du natürlich recht. Und tatsächlich überlege ich doch einen Assembler zu bauen, der meinen Code 1:1 übersetzt (dann von Compiler -> Assembler -> Computersprache).  Doch in welcher Weise hilft dies bei der Optimierung? (Kurz dazu: Ich muss bei "Optimierung" im Bezug auf Programmiersprachen immer an die Optimierung und Vereinfachungen von Compilern wie die GCC denken, bei denen die Optimierung des Programmcodes ja echt gut ist. Falls du was anderes meinst, lass es mich wissen.)

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

Ich hab noch ein Indexloch. Dann kann ich doch das 8. Bit als Wert noch dazu nehmen. Ist das das was du meintest? Nicht, dass ich wieder an deiner Aussage vorbeirede :-D .

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

Komplizierter und langsamer sind für mich kein Problem, da es ja, wie gesagt, nicht perfekt sein muss :wink:.

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

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

Gut, dann habe ich deine Frage nur falsch verstanden. Ich habe überlegt, dass wie bei meinem 5byte-MOV-Befehl (keine Sorge, der wird so auf gar keinen Fall eingebaut), einfach ein Argument anzugeben, das aussagt, ob das nächste Byte ein Immediate oder eine Speicheradresse ist. Also sowas wie "mov ax, <t>, <n>", wobei t angibt, ob n eine Speicheradresse oder ein Immediate ist. Je nachdem wie viele Befehle ich am Ende (im Allgemeinen) habe und je nachdem, ob sich es lohnt, könnte ich auch noch jeweils die in einzelne Instruktionen aufteilen, die dann t weglassen und n entweder als Immediate oder als Speicheradresse nehmen (wie du es beschrieben hast mit "MOV IMMEDIATE INTO REGISTER" bzw. "MOV INDIRECT INTO REGISTER"). Das muss ich dann aber entscheiden, wenn ich weiter mit der Planung bin. Ich muss mir dazu wahrscheinlich mehrere Befehlssätze als Referenzen suchen, um zu entscheiden, welche Instruktionen ein CPU noch bräuchte und ob ich dann mit 8bit noch genug Möglichkeiten habe, so viele hinzuzufügen (nun gut, 255 mögliche Befehle sollten genug sein :-D ).

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

Ja, das System ergibt Sinn. Danke!

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

Ich führe den Code nicht direkt vom Lochstreifen aus. Ich lese den Lochstreifen erst ein und speichere den Code, wie alle anderen. Habe ich was anderes gesagt? Vielleicht habe ich mich falsch ausgedrückt.

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

Das soll alles digital im Emulator ablaufen. Ich will am Ende nur den Code vom Lochstreifen lesen. Der Code wird dem Emulator übergeben, dann gehts von da aus weiter.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #5 am: 09. June 2020, 23:30 »
Ah, okay! Das bedeutet auch, dass "MOV [BX+0x85], 0xAX24" ein eigener Befehl ist und das [BX+0x85] nicht vom Assembler umgewandelt, sondern direkt zum Befehl gehört? Dieser spezielle Mov-Befehl beinhaltet also das Addieren der ersten beiden Argumente?
Jaein. Wie gesagt, x86 ist kompliziert und eigentlich eine schlechte Vorlage. Ich wollte das eher als schlechtes Beispiel nehmen.

Egal, dröseln wir das mal für den 8086 auf (und 0xAX24 ist natürlich keine gültige Hexadezimalzahl...):
$ cat test.asm
mov word [bx+0x85], 0xAF24
$ nasm -fbin -o test test.asm
$ hd test
00000000  c7 87 85 00 24 af                                 |....$.|
00000006

Das sind also tatsächlich 6 Bytes. Das erste Byte ist 0xC7, und meine Opcode-Tabelle besagt, dass das ein unvollständiger Opcode mit zwei Argumenten "Ev" und "Iv" ist. Es folgt also ein weiteres Byte (Modbyte), hier 0x87 (1000 0111).

Dessen Bits 5-3 geben an, welcher Unterbefehl von 0xC7 das genau ist. Diese Bits sind 000, also handelt es sich um "MOV" (andere Kombinationen sind übrigens ungültig, zumindest für 8086). Alle Unterbefehle von 0xC7 sind WORD-Befehle (auf dem 8086: 16 Bit), das Zielargument wird im Modbyte näher beschrieben, und danach folgt das Quellargument als imm16.

Die Bits 7-6 im Modbyte geben das Ziel an: Sind sie "11", dann ist das Ziel ein normales Register (und die Bits 2-0 beschreiben das Register). Sind sie "00" und die Bits 2-0 sind "110", dann folgen auf das Modbyte zwei weitere Bytes mit der Zieladresse. Andernfalls (das ist hier der Fall) handelt es sich um eine "effektive Adresse", also eine Speicheradresse, die auf verschiedene Formen gebildet werden kann.

Für diesen Fall geben die Bits 2-0 (in unserem Beispiel: 111) an, welches Basisregister wir haben (111 = BX). Die Bits 7-6 (im Beispiel: 10) geben an, welcher Wert dazu addiert wird (10 = imm16). "imm16" heißt, dass zwei weitere Bytes folgen, die direkt den Wert angeben (als "16 bit signed int"). Die folgenden Bytes sind "0x85 0x00", die musst du vertauschen (weil x86 ist "little endian"), also ist das Ziel vom MOV die Adresse (BX+0x0085).

Die Quelle (das Iv aus dem Decoder) ist ebenfalls ein imm16, also folgen nochmal zwei weitere Bytes, welche wieder direkt den Wert (wieder als "16 bit signed int") angeben. Diese Bytes sind "0x24 0xAF", und nach dem Vertauschen hast du "0xAF24". Und so macht die CPU aus den sechs Bytes den Befehl. Und NASM macht das Umgekehrte.

Kurz: Das ist alles wesentlich komplizierter als du dir das vorstellst, weil x86 einfach kompliziert ist.

Und tatsächlich überlege ich doch einen Assembler zu bauen, der meinen Code 1:1 übersetzt (dann von Compiler -> Assembler -> Computersprache).
Gute Entscheidung. :-)

Doch in welcher Weise hilft dies bei der Optimierung?
Garnicht. :-) Optimierung ist nicht Aufgabe des Assemblers, darum sind Assembler relativ einfach zu programmieren. Optimierung findet im Compiler statt, und deswegen sind Compiler kompliziert. Darum erstmal weglassen und per Hand optimieren.

(Kurz dazu: Ich muss bei "Optimierung" im Bezug auf Programmiersprachen immer an die Optimierung und Vereinfachungen von Compilern wie die GCC denken, bei denen die Optimierung des Programmcodes ja echt gut ist. Falls du was anderes meinst, lass es mich wissen.)
Nein, du hast schon recht.

Zitat
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.
Ich hab noch ein Indexloch. Dann kann ich doch das 8. Bit als Wert noch dazu nehmen. Ist das das was du meintest? Nicht, dass ich wieder an deiner Aussage vorbeirede :-D
Das Indexloch sagt dem Lesegerät, dass hier Löcher sind. Aber es trägt selbst keine weitere Information (schließlich ist es immer gelocht). Ein Paritätsbit gibt an, ob die Summe aller Einsen gerade oder ungerade ist. Wenn der Leser also z.B. "10101010" sieht, aber das Paritätsbit auf "ungerade" steht, dann weißt du, dass du einen Lesefehler hattest.

Komplizierter und langsamer sind für mich kein Problem, da es ja, wie gesagt, nicht perfekt sein muss :wink:.
Naja, wenn du nicht den Anspruch hast, dass es funktionieren soll... :-)

Ich muss mir dazu wahrscheinlich mehrere Befehlssätze als Referenzen suchen, um zu entscheiden, welche Instruktionen ein CPU noch bräuchte und ob ich dann mit 8bit noch genug Möglichkeiten habe, so viele hinzuzufügen (nun gut, 255 mögliche Befehle sollten genug sein :-D ).
Frage: Wenn jeder Befehl 8 Bit hat, und du davon 8 Bit für den Opcode reservierst, wie würdest du dann "schreibe die Zahl 42 in Register 3" codieren? ;-)

Ich führe den Code nicht direkt vom Lochstreifen aus. Ich lese den Lochstreifen erst ein und speichere den Code, wie alle anderen. Habe ich was anderes gesagt? Vielleicht habe ich mich falsch ausgedrückt.
Achso, ich dachte, du wolltest den Code direkt vom Lochstreifen ausführen.

Ich will am Ende nur den Code vom Lochstreifen lesen. Der Code wird dem Emulator übergeben, dann gehts von da aus weiter.
Achso. Das ist einfach.

Ein Lochstreifenlesegerät ist ein serielles Gerät (wie ein Modem). Da kommen einfach Bytes rein, die schreibst du in ein Array und wenn du fertig bist, lässt du den Emulator mit dem Array loslaufen. Als Bytecode-Interpreter kannst du was fertiges nehmen, z.B. das hier. Da musst du nur die Funktion load_program() von COM10 lesen lassen und fertig. :-)
Der Assembler dafür ist in Python geschrieben und erzeugt dir die Binärdaten, die du dann einfach byteweise nacheinander in den Lochstreifen stanzen musst.

Hast du denn schon einen Stanzer und ein Lesegerät?
« Letzte Änderung: 09. June 2020, 23:32 von Svenska »

Survari

  • Beiträge: 11
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 10. June 2020, 18:37 »
Zitat
Kurz: Das ist alles wesentlich komplizierter als du dir das vorstellst, weil x86 einfach kompliziert ist.

Da hast du recht. Ich denke mal, dass ich das einfach nicht umsetze. Der ganze Aufwand für das kleine Projekt ist schon groß genug, ich muss es mir nicht noch komplizierter machen :-D .

Zitat
Garnicht. :-) Optimierung ist nicht Aufgabe des Assemblers, darum sind Assembler relativ einfach zu programmieren. Optimierung findet im Compiler statt, und deswegen sind Compiler kompliziert. Darum erstmal weglassen und per Hand optimieren.

Super. Dann habe ich dich nur falsch verstanden.

Zitat
Das Indexloch sagt dem Lesegerät, dass hier Löcher sind. Aber es trägt selbst keine weitere Information (schließlich ist es immer gelocht). Ein Paritätsbit gibt an, ob die Summe aller Einsen gerade oder ungerade ist. Wenn der Leser also z.B. "10101010" sieht, aber das Paritätsbit auf "ungerade" steht, dann weißt du, dass du einen Lesefehler hattest.

Aber wenn das Paritätsbit auch falsch eingelesen wird, ist das doch eh egal, oder nicht? Oder meinst du, dass man per Hand nachschauen soll, ob nun ein Paritätsbit richtig eingelesen wurde, oder nicht?

Zitat
Naja, wenn du nicht den Anspruch hast, dass es funktionieren soll...

Ich bin verwirrt. Du meintest, dass ich mich dafür entscheiden könnte, entweder jeden Befehl gleich lang zu machen, oder unterschiedlich lange Befehle zu haben und hast dann Vor- und Nachteile aufgezählt. Ich empfand die Nachteile (kompliziert und langsam) nicht als überwiegend und habe mich daher zur Methode mit den unterschiedlich langen Befehlen entschieden. Warum sollte es jetzt nicht funktionieren?

Zitat
Frage: Wenn jeder Befehl 8 Bit hat, und du davon 8 Bit für den Opcode reservierst, wie würdest du dann "schreibe die Zahl 42 in Register 3" codieren?

Argh, ich habe mich mal wieder falsch ausgedrückt. 8bit soll nur der Opcode für die eigentliche Instruktion (<- ist das jetzt das richtige Fachwort? Ich bin langsam verwirrt, wenn für mich "Instruktion", "Anweisung" und "Befehl" fast das gleiche bedeuten und doch in verschiedenen Kontexten anders gedeutet werden können), ohne Argumente sein, also nicht der komplette Befehl. Daher komme ich auch auf maximal 255 unterschiedliche Instruktionen (ohne Argumente). (Wenn ich also 50 verschiedene Mov-Befehle habe, die unterschiedliche Dinge tun, habe ich nur noch 205 Instruktionen frei, die ich belegen kann, bevor ich meine 8bit verbraucht habe.)

Zitat
Ein Lochstreifenlesegerät ist ein serielles Gerät (wie ein Modem). Da kommen einfach Bytes rein, die schreibst du in ein Array und wenn du fertig bist, lässt du den Emulator mit dem Array loslaufen. Als Bytecode-Interpreter kannst du was fertiges nehmen, z.B. das hier. Da musst du nur die Funktion load_program() von COM10 lesen lassen und fertig. :-)
Der Assembler dafür ist in Python geschrieben und erzeugt dir die Binärdaten, die du dann einfach byteweise nacheinander in den Lochstreifen stanzen musst.

Das ist cool, aber ich finde die Idee vom eigenen Emulator und Befehlssatz gerade so spannend, dass ich gerne mal die Erfahrung machen möchte, beides zu erstellen. Ich bin gerade super motiviert dafür und diese Motivation kann ich echt gebrauchen.

Zitat
Hast du denn schon einen Stanzer und ein Lesegerät?

Der Stanzer ist da, das Lesegerät ist noch in Arbeit. Ich baue es selbst, daher habe ich noch Probleme mit Ungenauigkeit beim Einlesen. Aber ich muss erst mal verschiedene Dinge ausprobieren, optimieren usw. Damit habe ich wahrscheinlich noch etwas zu tun, daher habe ich währenddessen genug Zeit mich mit dem Befehlssatz hier auseinanderzusetzen :wink:.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #7 am: 13. June 2020, 13:54 »
Aber wenn das Paritätsbit auch falsch eingelesen wird, ist das doch eh egal, oder nicht? Oder meinst du, dass man per Hand nachschauen soll, ob nun ein Paritätsbit richtig eingelesen wurde, oder nicht?
Nein. Ein Paritätsbit kostet dich genau 1 Bit (egal, wie lang die Daten sind) und gibt dir dafür 50% Fehlererkennung und 0% Fehlerkorrektur. Die 50% kommen daher, dass das Bit nur erkennt, wenn die Anzahl der kaputten Bits (einschließlich des Paritätsbits selbst) ungerade ist. Doppelbitfehler werden nicht erkannt, Dreifachbitfehler schon. Und so weiter.

8bit soll nur der Opcode für die eigentliche Instruktion (<- ist das jetzt das richtige Fachwort? Ich bin langsam verwirrt, wenn für mich "Instruktion", "Anweisung" und "Befehl" fast das gleiche bedeuten und doch in verschiedenen Kontexten anders gedeutet werden können), ohne Argumente sein, also nicht der komplette Befehl.
Korrekt. Der Opcode gibt an, was getan werden soll; die Argumente/Parameter geben an, wie es getan werden soll; alles zusammen ist eine Instruktion/ein Befehl.

Das ist cool, aber ich finde die Idee vom eigenen Emulator und Befehlssatz gerade so spannend, dass ich gerne mal die Erfahrung machen möchte, beides zu erstellen. Ich bin gerade super motiviert dafür und diese Motivation kann ich echt gebrauchen.
Ich will dich nicht davon abbringen, die juckenden Finger sinnvoll anzuwenden - aber schau da einfach mal rein, wie einfach man solche Systeme bauen kann. (Und wie kompliziert x86 dagegen ist.)

Zitat
Hast du denn schon einen Stanzer und ein Lesegerät?
Der Stanzer ist da, das Lesegerät ist noch in Arbeit. Ich baue es selbst, daher habe ich noch Probleme mit Ungenauigkeit beim Einlesen. Aber ich muss erst mal verschiedene Dinge ausprobieren, optimieren usw. Damit habe ich wahrscheinlich noch etwas zu tun, daher habe ich währenddessen genug Zeit mich mit dem Befehlssatz hier auseinanderzusetzen :wink:.
Genau das meinte ich mit "Lochstreifenleser sind ungenau". :-) Wenn du ein Standardformat benutzt, dann kannst du auch Ausschau nach einem alten Lesegerät halten und damit testen. Vor langer Zeit habe ich ein Youtube-Video gefunden, was einen (vermutlich selbst gebauten) Leser zeigt: https://www.youtube.com/watch?v=uZnuu18FtQk.

Survari

  • Beiträge: 11
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 15. June 2020, 17:06 »
Zitat
Genau das meinte ich mit "Lochstreifenleser sind ungenau". :-) Wenn du ein Standardformat benutzt, dann kannst du auch Ausschau nach einem alten Lesegerät halten und damit testen. Vor langer Zeit habe ich ein Youtube-Video gefunden, was einen (vermutlich selbst gebauten) Leser zeigt: https://www.youtube.com/watch?v=uZnuu18FtQk.

Danke dir, das habe ich aber tatsächlich schon gesehen. Hab recht viel recherchiert zu verschiedenen Techniken, wie man ein Lesegerät bauen kann. Am Ende wollte ich dann eins mit Lichtsensoren bauen. Mein neuster Versuch klappt, das aktuelle Gerät liest zuverlässig ein. Nur wenn es zu schnell wird, gibt's Fehler. Und für den Fall der Fälle generiere ich für jeden Lochstreifen, den ich stanze, einen Hash mit 8 oder 16 Zeichen. Die kann man schnell auf den Streifen schreiben und leicht überprüfen. Wenn es Unterschiede zwischen dem Hash der Originaldatei und dem des gelesenen Code gibt, weiß ich ja, dass ich zu schnell beim Lesen war :roll:.

Edit: Habe gerade den Compiler und den Assembler fertig bekommen. Jetzt fehlen nur noch die restlichen Instruktionen im Emulator und dann bin ich tatsächlich fertig mit dem Projekt. Läuft bisher tatsächlich echt gut, hätte ich nicht gedacht. Besonders beim Compiler hatte ich erst Angst, dass er mir extrem viel Zeit kostet. Aber habe ihn an zwei Tagen fertig bekommen. Nachdem der Emulator fertig ist, baue ich ein kleines Nummern-Rate-Spiel. Bin gespannt, wie und ob das hinhaut.
« Letzte Änderung: 18. June 2020, 00:30 von Survari »

 

Einloggen