Nicht
A
N und angetrieben
Gerät
ist ein Turing-äquivalenter 16-Bit-Computer, der vollständig aus einer im Internet emulierten Uhr und NAND-Gattern besteht. NAND verfügt über eine eigene CPU, Maschinencodesprache, Assemblersprache, Assembler, virtuelle Maschinensprache, virtuellen Maschinenübersetzer, Programmiersprache, Compiler, IDE und Benutzeroberfläche. NAND basiert auf der Jack-VM-Hack-Plattform, die im Nand to Tetris-Kurs und dem dazugehörigen Buch beschrieben ist.
Ein einfaches Programm, das einige Zahlen eingibt und deren Durchschnitt berechnet und dabei Kontrollfluss, arithmetische Operationen, E/A und dynamische Speicherzuweisung demonstriert.
Programmausgabe:
How many numbers? 4
Enter a number: 100
Enter a number: 42
Enter a number: 400
Enter a number: 300
The average is 210
Dieses Programm wurde von der Software-Suite Nand to Tetris bereitgestellt.
Das Pong-Spiel, das das objektorientierte Modell der Sprache vorführt. Benutze die Pfeiltasten, um das Paddel nach links und rechts zu bewegen, um einen Ball hüpfen zu lassen. Bei jedem Sprung wird das Paddel kleiner und das Spiel endet, wenn der Ball den unteren Bildschirmrand berührt.
Dieses Programm wurde von der Software-Suite Nand to Tetris bereitgestellt.
Das Spiel von 2048, das Rekursion und komplexe Anwendungslogik zur Schau stellt. Verwenden Sie die Pfeiltasten, um die Zahlen im 4x4-Raster zu verschieben. Die gleichen Zahlen addieren sich zu ihrer Summe, wenn sie ineinander verschoben werden. Sobald das Plättchen 2048 erreicht ist, gewinnen Sie das Spiel, können aber weiterspielen, bis Sie verlieren. Sie verlieren das Spiel, wenn das Spielbrett voll ist und Sie keine weiteren Züge mehr machen können.
Ein Programm, das durch unendliche Rekursion absichtlich einen Stapelüberlauf verursacht, um einen Escapevorgang für eine virtuelle Maschine durchzuführen. Dabei wird die Tatsache ausgenutzt, dass es keine Laufzeitprüfungen gibt, um einen Stapelüberlauf zu verhindern. Keine andere moderne Plattform ermöglicht Ihnen dies :-)
Beim Ausführen gibt das Programm ständig den Stapelzeiger auf dem Bildschirm aus. Sobald dieser angezeigte Wert 2048 überschreitet, hat der Stapel das Ende seines vorgesehenen Speicherplatzes erreicht und läuft auf den Heap-Speicherplatz über, was zu einer explosiven Fehlfunktion der Druckanweisung führt:
Zwei bemerkenswerte Dinge sind hervorzuheben.
Wenn Sie dieses Programm auf einem leeren RAM voller Nullen ausführen (Sie können den RAM über die Benutzeroberfläche löschen), werden Sie feststellen, dass sich das Programm nach der Hälfte der Ausführung selbst zurücksetzt, obwohl Sie nicht auf die Schaltfläche „Zurücksetzen“ klicken. Der Grund dafür ist einfach: Die Laufzeitumgebung mit Jailbreak führt einen Befehl aus, der den Wert des Programmzählers auf 0 setzt und das Programm so anweist, zum ersten Befehl zu springen und von vorne zu beginnen.
Wenn Sie das Beispielprogramm GeneticAlgorithm ausführen und dieses dann direkt anschließend ausführen, liest das Programm in seinem Amoklauf alten RAM-Speicher, der einfach nie überschrieben wurde.
Ein Programm, das die Tatsache ausnutzt, dass die Laufzeit das Stack-Smashing nicht verhindert, um eine Funktion aufzurufen, auf die sonst nicht zugegriffen werden könnte. Um zu verstehen, wie das funktioniert, schauen wir uns diese Abbildung des Stapelrahmenlayouts von NAND an.
entnommen aus dem Nand to Tetris-Buch.
Wenn Sie mit Stack-Layouts nicht vertraut sind, finden Sie hier die Hauptidee hinter dem Exploit. Immer wenn eine Funktion zurückkehrt, muss sie wissen, wohin (an welche Speicheradresse des Maschinencodebefehls) sie gehen soll, um mit dem Ausführungsfluss fortzufahren. Wenn die Funktion zum ersten Mal aufgerufen wird, wird diese Speicheradresse zusammen mit einigen anderen unwichtigen Daten vorübergehend auf dem Stapel in einem Speicherbereich gespeichert, der als Stapelrahmen bezeichnet wird, als Referenz für die Rückkehr. Die Abbildung beschreibt die genaue Position dieser Rücksprungadresse relativ zum Funktionsaufruf, eine Position, die rückentwickelt werden kann.
Das Programm ermöglicht es dem Benutzer, eine einzelne Speicheradresse im RAM mit einem beliebigen Wert zu überschreiben. Wenn man zwei und zwei zusammenzählt: Wenn der Benutzer die Rücksprungadresse eines Stapelrahmens mit der Adresse einer anderen Funktion überschreibt, erhält er im Wesentlichen die Möglichkeit, beliebigen im Programm enthaltenen Code auszuführen.
Wenn Sie tatsächlich 267 als Speicherort und 1715 als zu überschreibenden Wert eingeben, zwei Zahlen, die durch manuelle Überprüfung des Stapelspeicherplatzes und des Assemblers rückentwickelt wurden, werden Sie diese Idee in der Praxis sehen.
Dies ist keine Sicherheitslücke, die nur bei NAND auftritt. Es existiert auch in C! Wie cool!
Ob Sie es glauben oder nicht, von den vielen, vielen verschiedenen Komponenten von NAND hat die Entwicklung dieser im Alleingang am längsten gedauert!
Dieses Programm ist eine Kreaturensimulation, die einfaches maschinelles Lernen nutzt. Es folgt der mit künstlicher Intelligenz codierten Serie (Teil eins und zwei) von Code Bullet. Schauen Sie sich unbedingt seinen Kanal an, er macht einige wirklich coole Sachen!
Einfach erklärt:
Jeder Punkt hat sein eigenes „Gehirn“ aus Beschleunigungsvektoren, und diese entwickeln sich, um durch natürliche Selektion ein Ziel zu erreichen. Mit jeder Generation werden Punkte, die näher am Ziel „sterben“, eher als Eltern für die nächste Generation ausgewählt. Die Fortpflanzung führt von Natur aus dazu, dass ein Teil des Gehirns mutiert, wodurch die natürliche Evolution durchaus simuliert wird.
Dennoch gibt es viel zu wünschen übrig. Aufgrund der Leistung ist der einzige Faktor, den Punkte für ihre Entwicklung nutzen, ihre Nähe zum Ziel nach dem Tod, was dem natürlichen Selektionsalgorithmus eine niedrige Entropie verleiht. Aufgrund der Speichernutzung gibt es keine zufriedenstellenden Grenzen für die Anzahl der Punkte und die Größe ihres Gehirns. Aufgrund der technischen Komplexität ist das Ersetzen von Hindernissen während der Simulation keine Garantie dafür, dass die Punkte über ein ausreichend großes Gehirn verfügen, um das Ziel zu erreichen. Die Gehirngrößen werden erst zu Beginn des Programms bestimmt.
Ich habe unzählige Optimierungstechniken eingesetzt, um die folgenden Hardwareeinschränkungen zu umgehen und dies zu ermöglichen:
Um nicht um den heißen Brei herumzureden, habe ich mich darauf beschränkt, diese Techniken und zusätzliche Einblicke in die Codebasis dieses Programms für Interessierte zu dokumentieren.
Bevor wir beginnen, ist das wichtigste Detail, das Sie beim Schreiben von Programmen in Jack beachten sollten, dass es keine Bedienerpriorität gibt; Dies ist wahrscheinlich der Grund, warum Ihr Programm nicht funktioniert.
Sie sollten zum Beispiel Folgendes ändern:
4 * 2 + 3
bis (4 * 2) + 3
if (~x & y)
bis if ((~x) & y)
Sie können jedoch if (y & ~x)
beibehalten, da es keine Mehrdeutigkeit des Operators gibt.
Ohne Klammern ist der Bewertungswert eines mehrdeutigen Ausdrucks undefiniert .
NAND verfügt über einen eigenen kompletten Tech-Stack. Infolgedessen kann NAND nur in Jack, seiner schwach typisierten objektorientierten Programmiersprache, programmiert werden. Laienhaft ausgedrückt ist Jack C mit der Java-Syntax.
Nehmen wir den Ansatz des beispielbasierten Lernens und tauchen Sie direkt ein.
/**
* This program prompts the user to enter a phrase
* and an energy level. Program output:
*
* Whats on your mind? Superman
* Whats your energy level? 3
* Superman!
* Superman!
* Superman!
*/
class Main {
function void main ( ) {
var String s ;
var int energy , i ;
let s = Keyboard . readLine ( "Whats on your mind? " ) ;
let energy = Keyboard . readInt ( "Whats your energy level? " ) ;
let i = 0 ;
let s = s . appendChar ( 33 ) ; // Appends the character '!'
while ( i < energy ) {
do Output . printString ( s ) ;
do Output . println ( ) ;
let i = i + 1 ;
}
}
}
entnommen aus den Vorlesungsfolien von Nand zu Tetris.
Wenn Sie bereits Erfahrung mit der Programmierung haben, dürfte Ihnen das sehr bekannt vorkommen. Es ist klar, dass Jack stark von Java inspiriert wurde. Main.main
, der Einstiegspunkt in das Programm, demonstriert die grundlegende Verwendung von Variablen sowie die while-Schleife für den Kontrollfluss.
Darüber hinaus werden Keyboard.readLine
und Keyboard.readInt
verwendet, um Eingaben vom Benutzer zu lesen, und Output.printString
und Output.println
um die Ausgabe auf dem Bildschirm auszugeben – allesamt detailliert in der Jack OS-Referenz definiert. Standardmäßig wird das Jack OS während der Kompilierung mit Ihrem Programm gebündelt, um die Schnittstelle zu Strings, Speicher, Hardware und mehr zu ermöglichen.
Jede Programmiersprache verfügt über einen festen Satz primitiver Datentypen. Jack unterstützt drei: int
, char
und boolean
. Dieses Grundrepertoire können Sie bei Bedarf mit Ihren eigenen abstrakten Datentypen erweitern. Vorkenntnisse über objektorientierte Programmierung werden direkt in diesen Abschnitt übernommen.
/** Represents a point in 2D plane. */
class Point {
// The coordinates of the current point instance:
field int x , y ;
// The number of point objects constructed so far:
static int pointCount ;
/** Constructs a point and initializes
it with the given coordinates */
constructor Point new ( int ax , int ay ) {
let x = ax ;
let y = ay ;
let pointCount = pointCount + 1 ;
return this ;
}
/** Returns the x coordinate of the current point instance */
method int getx ( ) { return x ; }
/** Returns the y coordinate of the current point instance */
method int gety ( ) { return y ; }
/** Returns the number of Points constructed so far */
function int getPointCount ( ) {
return pointCount ;
}
/** Returns a point which is this
point plus the other point */
method Point plus ( Point other ) {
return Point . new ( x + other . getx ( ) ,
y + other . gety ( ) ) ;
}
/** Returns the Euclidean distance between the
current point instance and the other point */
method int distance ( Point other ) {
var int dx , dy ;
let dx = x - other . getx ( ) ;
let dy = y - other . gety ( ) ;
return Math . sqrt ( ( dx * dx ) + ( dy * dy ) ) ;
}
/** Prints the current point instance, as "(x, y)" */
method void print ( ) {
var String tmp ;
let tmp = "(" ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
do Output . printInt ( x ) ;
let tmp = ", " ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
do Output . printInt ( y ) ;
let tmp = ")" ;
do Output . printString ( tmp ) ;
do tmp . dispose ( ) ;
}
}
var Point p1 , p2 , p3 ;
let p1 = Point . new ( 1 , 2 ) ;
let p2 = Point . new ( 3 , 4 ) ;
let p3 = p1 . plus ( p2 ) ;
do p3 . print ( ) ; // prints (4, 6)
do Output . println ( ) ;
do Output . printInt ( p1 . distance ( p2 ) ) ; // prints 5
do Output . println ( ) ;
do Output . printInt ( getPointCount ( ) ) ; // prints 3
entnommen aus den Vorlesungsfolien von Nand zu Tetris.
Wir definieren eine Point
Klasse, um einen abstrakten Punkt im Raum darzustellen. Es verwendet field
, um instanzspezifische Attribute des Datentyps zu deklarieren. Es stellt öffentliche method
bereit, die wir als Schnittstelle zum Punkt verwenden können, und gibt dem Aufrufer die Funktionalität, zwei Punkte zu addieren und den Abstand zwischen zwei Punkten zu berechnen.
Alle field
haben einen privaten Gültigkeitsbereich. Wenn Sie diese Variablen von außerhalb der Klassendeklaration abrufen oder festlegen möchten, müssen diese Variablen über entsprechende method
verfügen, um diese Funktionalität bereitzustellen.
Im Codebeispiel weggelassen, um beim Thema zu bleiben, ist es üblich, dass Datenklassen dispose
-Methoden für die Freigabe von Objekten definieren, sobald Objekte nicht mehr benötigt werden. Siehe Manuelle Speicherverwaltung.
Bei Bedarf finden Sie hier eine Referenz zur function
und method
.
class Foo {
...
method void f ( ) {
var Bar b ; // Declares a local variable of class type Bar
var int i ; // Declares a local variable of primitive type int
do g ( ) ; // Calls method g of the current class on the current object instance
// Note: Cannot be called from within a function (static method)
do Foo . p ( 3 ) ; // Calls function p of the current class;
// Note: A function call must be preceded by the class name
do Bar . h ( ) ; // Calls function h of class Bar
let b = Bar . r ( ) ; // Calls function or constructor r of class Bar
do b . q ( ) ; // Calls method q of class Bar on the b object
}
}
entnommen aus den Vorlesungsfolien von Nand zu Tetris.
Erinnern Sie sich, wie wir sagten, Jack sei Java ähnlich? Das war eine Fassade oder bestenfalls irreführend. Während Java stark typisiert ist und daher komplexe Typfunktionen wie Downcasting, Polymorphismus und Vererbung unterstützt, unterstützt Jack keines davon und hat nur einen Typ unter der Haube: die vorzeichenbehaftete 16-Bit-Ganzzahl. Dies ist der Hauptgrund, warum Jack so schwach getippt ist. Tatsächlich ist es dem Compiler egal, ob Sie verschiedene Typen in Zuweisungen und Operationen mischen und anpassen.
var char c ;
var String s ;
let c = 65 ; // 'A'
// Equivalently
let s = "A" ;
let c = s . charAt ( 0 ) ;
var Array a ;
let a = 5000 ;
let a [ 100 ] = 77 ; // RAM[5100] = 77
var Array arr ;
var String helloWorld ;
let helloWorld = "Hello World!"
let arr = Array . new ( 4 ) ; // Arrays are not strictly typed
let arr [ 0 ] = 12 ;
let arr [ 1 ] = false ;
let arr [ 2 ] = Point . new ( 5 , 6 ) ;
let arr [ 3 ] = helloWorld ;
class Complex {
field int real ;
field int imaginary ;
...
}
. . .
var Complex c ;
var Array a ;
let a = Array . new ( 2 ) ;
let a [ 0 ] = 7 ;
let a [ 1 ] = 8 ;
let c = a ; // c == Complex(7, 8)
// Works because it matches the memory layout
// of the Complex type
alle Codesegmente aus den Vorlesungsfolien von Nand bis Tetris.
Verstehen Sie das nicht falsch – Jack bietet immer noch ein leistungsstarkes und funktionales objektorientiertes Modell. Diese Erkenntnisse sollen Ihnen helfen zu verstehen, wann und wie Sie bei Bedarf Typkonvertierungen durchführen sollten.
Nehmen wir an, Sie sind ein verrückter Katzenliebhaber, genau wie ich! Und Sie wollten dieses Programm schreiben, um zu zeigen, wie sehr Sie Katzen lieben.
class Main {
function void main ( ) {
while ( true ) {
do Output . printString ( "Kittens are so adorable! " ) ;
}
}
}
Sie werden überrascht sein, wenn Sie feststellen, dass das Programm nach ein paar Sekunden mit „ERR6“ oder einem Heap-Überlauf abstürzt!
Jack ist eine manuell speicherverwaltete Programmiersprache. Dies bedeutet, dass Sie darauf achten müssen, nicht mehr benötigten Speicher ordnungsgemäß freizugeben, da das Jack-Betriebssystem sonst anders denkt und einen Speicherverlust begünstigt. Der Best-Practice-Ratschlag besteht darin, für jede Klasse eine dispose
-Methode bereitzustellen, die ein Objekt darstellt, das diese Freigabe ordnungsgemäß kapselt. Wenn Objekte also nicht mehr benötigt werden, können Sie deren dispose
-Methoden aufrufen, um sicherzustellen, dass Ihnen nicht irgendwann der Heap-Speicher ausgeht.
Wenn Sie in anderen manuell speicherverwalteten Sprachen wie C programmiert haben, dürfte Ihnen das sehr bekannt vorkommen. Ein wesentlicher Unterschied besteht darin, dass Jack OS Arrays und Strings auf dem Heap und nicht auf dem Stapel speichert, was darauf hindeutet, warum das Programm mit einem Heap-Überlauf abstürzt.
Lassen Sie uns dieses Programm für unsere anderen Katzenfanatiker reparieren.
class Main {
function void main ( ) {
var String s ;
while ( true ) {
let s = "Kittens are so adorable! " ;
do Output . printString ( s ) ;
do s . dispose ( ) ;
}
}
}
Alternativ können Sie der Zeichenfolge nur einmal Speicher zuweisen.
class Main {
function void main ( ) {
var String s ;
let s = "Kittens are so adorable! " ;
while ( true ) {
do Output . printString ( s ) ;
}
}
}
Sie werden feststellen, dass diese alternativen Versionen die Zeichenfolge nicht nur viel schneller drucken, sondern dass sie dieses Mal sogar für immer gedruckt werden! Hurra!
Werfen wir einen kurzen Blick auf String.dispose
, damit Sie besser verstehen, wie Sie Ihre eigenen dispose
-Methoden schreiben.
method void dispose ( ) {
do stringArray . dispose ( ) ;
do Memory . deAlloc ( this ) ;
}
Array.dispose
wird von stringArray
aufgerufen
method void dispose ( ) {
do Memory . deAlloc ( this ) ;
}
Richtige dispose
Methoden müssen zunächst dispose
für ihre Feldvariablen aufrufen und dann mit do Memory.deAlloc(this);
um die Objektinstanz selbst freizugeben.
Da Jack und NAND so primitiv sind, sind Fußfeuer innerhalb der Sprache unvermeidlich. Ich habe die folgenden Fälle undefinierten Verhaltens zusammengestellt, die Sie kennen sollten, geordnet von (meiner Meinung nach) am wichtigsten bis zum unwichtigsten.
Ich fand diesen Vorbehalt so wichtig, dass ich ihn an den Anfang dieses Abschnitts verschoben habe.
Die Jack-Ausdrücke
a > b
a < b
sind täuschend einfach. Sie sind nicht immer mathematisch korrekt und entsprechen jeweils den Java-Ausdrücken
( ( a - b ) & ( 1 << 15 ) ) == 0 && a != b
( ( a - b ) & ( 1 << 15 ) ) != 0
Was ist mit der Nuance los? Die Implementierung der virtuellen Maschine konvertiert a > b
in a - b > 0
. Hier ist das Problem: a - b
kann überlaufen :(
Was bedeutet 20000 > -20000
? Die virtuelle Maschine transpiliert dies auf 20000 - (-20000) > 0
, was -25336 > 0
ergibt. Leider ist die Antwort false
.
Allerdings ergibt 20000 > -10000
30000 > 0
oder true
.
Wie Sie vielleicht schon gedacht haben, sind a > b
und a < b
falsch, wenn der absolute Abstand zwischen a
und b
mehr als 32767 beträgt. Ansonsten geht es dir gut.
Dies ist kein Implementierungsfehler, sondern eher eine Inkonsistenz zwischen Nand und Tetris selbst. Mehr dazu hier. Aus Kompatibilitätsgründen wird dieses Verhalten nicht behoben.
-32768 ist einzigartig. Es ist die einzige Zahl, die die Eigenschaft enthält, dass -(-32768) = -32768, ein Singleton ohne positives Gegenstück * . Dies kann zu Unstimmigkeiten und Logikfehlern führen.
/**
* Program output:
* --.)*(
*/
class Main {
function void main ( ) {
// Note that -32768 must instead be written as ~32767
// because the CPU can't load a number that large
do Output . printInt ( ~ 32767 ) ;
}
}
Output.printInt
erwartet intern, dass Math.abs
eine positive Zahl zurückgibt. Dies ist bei -32768 nicht der Fall, sodass das Jack-Betriebssystem nicht funktioniert.
Ihr Hauptanliegen sollte die Behandlung von Logikfehlern mit dem negativen Operator sein. Wenn Sie als Programmierer sicherstellen möchten, dass das Negative einer negativen Zahl positiv ist, liegt es in Ihrer Verantwortung, den Fall von -32768 zu überprüfen und entsprechende Maßnahmen zu ergreifen.
* Dies gilt, da die ALU von NAND den Jack-Ausdruck -x
intern als ~(x - 1)
verarbeitet. Setzen wir x
auf -32768
und werten wir es Schritt für Schritt aus. Hier sind die entsprechenden 16-Bit-Zweierkomplement-Binärdarstellungen der Berechnung:
x
= 1000 0000 0000 0000
x - 1
= 0111 1111 1111 1111
~(x - 1)
= 1000 0000 0000 0000
= x
Es ist dasselbe! Was ist hier passiert? Da es sich bei NAND um eine 16-Bit-Maschine handelt, ist -32768 die einzige Zahl, bei der man durch Subtrahieren von eins die umgedrehten Bits erhält. Mit anderen Worten, -32768 erfüllt x - 1 = ~x
und vereinfacht den Ausdruck zu ~(~x)
oder einfach x
.
Da dies selbsterklärend ist, finden Sie hier eine kurze Demonstration.
/**
* Program output:
* I have 818 cookies.
*/
class Main {
function void main ( ) {
do Main . cookies ( ) ;
}
function void cookies ( int a ) {
do Output . printString ( "I have " ) ;
do Output . printInt ( a ) ;
do Output . printString ( " cookies." ) ;
}
}
Andererseits ist der Aufruf einer Funktion mit zu vielen Argumenten vollkommen zulässig. Sie können das Schlüsselwort arguments
verwenden, um zusätzliche Funktionsargumente zu indizieren. Beachten Sie, dass es keinen Indikator für die Argumentanzahl gibt.
Sie können Array
verwenden, um eine Variable in einen beliebigen anderen Typ umzuwandeln. Das Aufrufen von Instanzmethoden, die für Typumwandlungsvariablen nicht vorhanden sind, ist ein undefiniertes Verhalten. Der Compiler ist nicht intelligent genug, um zu erkennen, wann Sie dies tun.
/**
* Program output:
* 4
*/
class Main {
constructor Main new ( ) {
return this ;
}
function void main ( ) {
var Array a ;
var Main b ;
var String c ;
let a = Array . new ( 1 ) ;
let b = Main . new ( ) ;
let a [ 0 ] = b ;
let c = a [ 0 ] ;
// Invalidly calling `String.length` on an instance of `Main`.
do Output . printInt ( c . length ( ) ) ;
}
}
Ein ausführliches Beispiel finden Sie im Overflow-Programm.
Das Ändern von Stack-Frames oder der internen Register, die sich jeweils an den Speicheradressen 256
bis 2047
und 1
bis 15
befinden, kann zu undefiniertem Verhalten führen. Dies ist normalerweise nicht möglich, ohne Memory.poke
oder eine negative Array-Indizierung zu missbrauchen. Ein ausführliches Beispiel finden Sie im SecretPassword-Programm.
NAND bietet Programmvalidierung für .jack
-Dateien, jedoch nicht für .vm
-Dateien. Das bedeutet, dass der NAND-Compiler für virtuelle Maschinen Ihnen freie Hand lässt, nicht vorhandene Funktionen aufzurufen, nicht zugewiesene Variablen zu referenzieren oder andere logisch ungültige Speicheroperationen auszuführen. In den meisten Fällen eines solchen undefinierten Verhaltens entkommt die virtuelle Maschine und auf dem Bildschirm wird einfach nichts angezeigt. Es liegt in Ihrer Verantwortung, das Programm selbst zu debuggen.
Seit seinem Aufstieg in den 1970er Jahren gibt es einen guten Grund, warum 16-Bit-Computing in der Neuzeit in Ungnade gefallen ist. Im Vergleich zu 32-Bit- oder 64-Bit-Computing bot 16-Bit-Computing eine begrenzte Verarbeitungsleistung und Speicherkapazität, die den Anforderungen moderner Software und Anwendungen einfach nicht entsprach.
NAND ist keine Ausnahme von dieser Realität.
entnommen aus den Vorlesungsfolien von Nand zu Tetris.
Im Vergleich zu Ihrem 16-GiB-MacBook verfügt NAND über magere 4 KiB RAM, ein Verhältnis von 0,00002 % ! Trotzdem reichte es aus, um uns zum Mond zu bringen, also wer sagt, dass NAND das auch nicht kann?
Das Jack OS reserviert 14.336 Speicheradressen