아니다
에이
N 및 전원 공급
장치
웹에서 에뮬레이트된 NAND 게이트와 시계로 완전히 만들어진 Turing과 동등한 16비트 컴퓨터입니다. NAND는 자체 CPU, 기계 코드 언어, 어셈블리 언어, 어셈블러, 가상 기계 언어, 가상 기계 번역기, 프로그래밍 언어, 컴파일러, IDE 및 사용자 인터페이스를 갖추고 있습니다. NAND는 Nand to Tetris 과정 및 관련 서적에 지정된 Jack-VM-Hack 플랫폼을 기반으로 합니다.
일부 숫자를 입력하고 그 평균을 계산하여 제어 흐름, 산술 연산, I/O 및 동적 메모리 할당을 보여주는 간단한 프로그램입니다.
프로그램 출력:
How many numbers? 4
Enter a number: 100
Enter a number: 42
Enter a number: 400
Enter a number: 300
The average is 210
이 프로그램은 Nand에서 Tetris 소프트웨어 제품군에 제공되었습니다.
언어의 객체지향 모델을 보여주는 Pong 게임. 화살표 키를 사용하여 패들을 좌우로 움직여 공을 튕겨보세요. 바운스할 때마다 패들은 작아지고, 공이 화면 하단에 닿으면 게임이 종료됩니다.
이 프로그램은 Nand에서 Tetris 소프트웨어 제품군에 제공되었습니다.
재귀와 복잡한 응용 로직을 선보이는 2048년의 게임. 화살표 키를 사용하여 4x4 그리드 주위로 숫자를 이동하세요. 같은 숫자가 서로 이동하면 합이 됩니다. 2048 타일에 도달하면 게임에서 승리하게 되지만, 패배할 때까지 계속 플레이할 수 있습니다. 보드가 꽉 차서 더 이상 움직일 수 없게 되면 게임에서 패배하게 됩니다.
가상 머신 탈출을 수행하기 위해 의도적으로 무한 재귀를 통해 스택 오버플로를 발생시키는 프로그램입니다. 스택 오버플로를 방지하기 위해 런타임 검사가 없다는 사실을 활용합니다. 다른 어떤 최신 플랫폼도 이를 허용하지 않습니다 :-)
실행 시 프로그램은 지속적으로 스택 포인터를 화면에 인쇄합니다. 이 표시된 값이 2048을 초과하면 스택이 의도한 메모리 공간의 끝에 도달하여 힙 메모리 공간으로 쏟아져 인쇄 문이 폭발적인 방식으로 오작동하게 됩니다.
주목할만한 흥미로운 점 두 가지는 지적할 가치가 있습니다.
0으로 가득 찬 빈 RAM에서 이 프로그램을 실행하면(사용자 인터페이스를 통해 RAM을 지울 수 있음) "재설정" 버튼을 누르지 않았음에도 불구하고 프로그램이 실행 중간에 재설정되는 것을 볼 수 있습니다. 이런 일이 발생하는 이유는 간단합니다. 탈옥된 런타임은 프로그램 카운터의 값을 0으로 설정하는 명령을 실행하여 프로그램이 첫 번째 명령으로 점프하고 다시 시작하도록 효과적으로 지시합니다.
GeneticAlgorithm 예제 프로그램을 실행한 후 즉시 이를 실행하면 프로그램이 난동을 부리면서 결코 덮어쓰지 않은 오래된 RAM 메모리를 읽습니다.
런타임이 스택 스매싱을 방지하지 않는다는 사실을 활용하여 그렇지 않으면 액세스할 수 없는 함수를 호출하는 프로그램입니다. 이것이 어떻게 작동하는지 이해하기 위해 NAND의 스택 프레임 레이아웃 그림을 살펴보겠습니다.
Nand에서 Tetris 책으로 가져왔습니다.
스택 레이아웃에 익숙하지 않은 경우 익스플로잇의 주요 아이디어는 다음과 같습니다. 함수가 반환될 때마다 실행 흐름을 진행하기 위해 어디로 가야 하는지(어떤 기계 코드 명령어 메모리 주소)를 알아야 합니다. 따라서 함수가 처음 호출될 때 이 메모리 주소는 다른 중요하지 않은 데이터와 함께 반환할 위치에 대한 참조로 스택 프레임이라고 하는 메모리 영역의 스택에 임시 저장됩니다. 그림에서는 함수 호출을 기준으로 이 반환 주소의 정확한 위치, 즉 리버스 엔지니어링이 가능한 위치를 설명합니다.
이 프로그램을 사용하면 사용자는 RAM의 단일 메모리 주소를 임의의 값으로 덮어쓸 수 있습니다. 2개와 2개를 합쳐서 사용자가 스택 프레임의 반환 주소를 다른 함수의 주소로 덮어쓰면 본질적으로 프로그램에 포함된 임의의 코드를 실행할 수 있는 능력을 얻게 됩니다.
실제로 메모리 위치로 267을 입력하고 덮어쓸 값으로 1715를 입력하고 스택 메모리 공간과 어셈블러를 수동으로 검사하여 두 숫자를 리버스 엔지니어링하면 이 아이디어가 작동하는 것을 볼 수 있습니다.
이는 NAND에만 있는 취약점이 아닙니다. C에도 존재합니다! 정말 멋지다!
믿거나 말거나, NAND의 수많은 다양한 구성 요소 중에서 이 제품이 단독으로 개발하는 데 가장 오랜 시간이 걸렸습니다!
본 프로그램은 간단한 머신러닝을 활용한 생물 시뮬레이션입니다. Code Bullet의 인공지능 코딩 시리즈(1부, 2부)를 따릅니다. 그의 채널을 꼭 확인해 보세요. 그는 정말 멋진 것들을 만들고 있습니다!
간단히 설명하면 다음과 같습니다.
모든 점에는 가속 벡터의 자체 "두뇌"가 있으며 자연 선택을 통해 목표에 도달하도록 진화합니다. 매 세대마다 목표에 더 가깝게 '죽는' 점들은 다음 세대의 부모로 선택될 가능성이 더 높습니다. 번식은 본질적으로 뇌의 일부에 돌연변이를 일으키고 자연 진화를 완전히 효과적으로 시뮬레이션합니다.
그럼에도 불구하고 바라는 점이 많습니다. 성능으로 인해 도트가 진화하는 데 사용하는 유일한 요소는 사망 시 목표에 대한 근접성이며 자연 선택 알고리즘에 낮은 엔트로피를 부여합니다. 메모리 사용량으로 인해 점 수와 두뇌 크기에는 만족할 만한 제한이 없습니다. 마지막으로, 기술적 복잡성으로 인해 시뮬레이션 중에 장애물을 교체한다고 해서 점이 목표에 도달할 만큼 충분히 큰 두뇌를 갖게 된다는 보장은 없습니다. 두뇌 크기는 프로그램 시작 시에만 결정됩니다.
저는 다음과 같은 하드웨어 제한 사항을 피하고 이를 가능하게 하기 위해 수많은 최적화 기술을 활용했습니다.
난제를 피하기 위해 관심 있는 사람들을 위해 이 프로그램의 코드베이스에 이러한 기술과 추가 통찰력을 문서화하는 데 전념했습니다.
시작하기 전에 Jack에서 프로그램 작성에 관해 기억해야 할 가장 중요한 세부 사항은 연산자 우선 순위가 없다는 것입니다. 이것이 아마도 프로그램이 작동하지 않는 이유일 것입니다.
예를 들어 다음을 변경해야 합니다.
4 * 2 + 3
~ (4 * 2) + 3
if (~x & y)
에서 if ((~x) & y)
로
그러나 연산자 모호성이 없으므로 if (y & ~x)
동일하게 유지할 수 있습니다.
괄호가 없으면 모호한 표현식의 평가 값은 un Defined 입니다.
NAND는 자체적인 완전한 기술 스택을 자랑합니다. 결과적으로 NAND는 약한 유형의 객체 지향 프로그래밍 언어인 Jack에서만 프로그래밍할 수 있습니다. 일반인의 관점에서 Jack은 Java 구문을 사용하는 C입니다.
사례 기반 학습의 접근 방식을 취하고 바로 시작해 보겠습니다.
/**
* 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 ;
}
}
}
Nand에서 Tetris 강의 슬라이드로 가져왔습니다.
이미 프로그래밍 경험이 있다면 이는 매우 친숙해 보일 것입니다. Jack이 Java에서 많은 영감을 받은 것은 분명합니다. 프로그램의 진입점인 Main.main
변수의 기본 사용법과 제어 흐름을 위한 while 루프를 보여줍니다.
또한, Keyboard.readLine
및 Keyboard.readInt
사용하여 사용자의 입력을 읽고, Output.printString
및 Output.println
사용하여 화면에 출력을 인쇄합니다. 모두 Jack OS Reference에 자세히 정의되어 있습니다. 기본적으로 Jack OS는 문자열, 메모리, 하드웨어 등과의 인터페이스를 활성화하기 위해 컴파일 중에 프로그램과 함께 번들로 제공됩니다.
모든 프로그래밍 언어에는 고정된 기본 데이터 유형 세트가 있습니다. Jack은 int
, char
및 boolean
세 가지를 지원합니다. 필요에 따라 고유한 추상 데이터 유형을 사용하여 이 기본 레퍼토리를 확장할 수 있습니다. 객체 지향 프로그래밍에 대한 사전 지식이 이 섹션으로 바로 이어집니다.
/** 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
Nand에서 Tetris 강의 슬라이드로 가져왔습니다.
공간의 추상 지점을 나타내기 위해 Point
클래스를 정의합니다. field
변수를 사용하여 데이터 유형의 인스턴스별 속성을 선언합니다. 이는 포인트와 인터페이스하는 데 사용할 수 있는 공용 method
함수를 노출하여 호출자에게 두 포인트를 함께 추가하고 두 포인트 사이의 거리를 계산하는 기능을 제공합니다.
모든 field
변수는 비공개 범위로 지정됩니다. 클래스 선언 외부에서 이러한 변수를 가져오거나 설정하려면 해당 변수에 해당 기능을 제공하는 해당 method
함수가 있어야 합니다.
주제를 유지하기 위해 코드 샘플에서 생략되었으며, 객체가 더 이상 필요하지 않은 경우 데이터 클래스에서 할당 취소를 위한 dispose
메서드를 정의하는 것이 일반적입니다. 수동 메모리 관리를 참조하세요.
필요한 경우 function
및 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
}
}
Nand에서 Tetris 강의 슬라이드로 가져왔습니다.
Jack이 Java와 유사하다고 말한 것을 기억하십니까? 그것은 겉모습이거나 기껏해야 오해의 소지가 있었습니다. Java는 강력한 형식을 가지며 다운 캐스팅, 다형성 및 상속과 같은 복잡한 형식 기능을 지원하지만 Jack은 이러한 기능을 지원하지 않으며 내부적으로는 부호 있는 16비트 정수라는 한 가지 형식만 지원합니다. 이것이 Jack의 유형이 약한 주된 이유입니다. 실제로 컴파일러는 할당 및 작업에서 서로 다른 유형을 혼합하고 일치시키는지 신경 쓰지 않습니다.
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
Nand에서 Tetris 강의 슬라이드까지 가져온 모든 코드 세그먼트.
이것을 잘못된 방향으로 받아들이지 마십시오. Jack은 여전히 강력하고 기능적인 객체 지향 모델을 제공합니다. 이 통찰력은 필요에 따라 유형 변환을 수행해야 하는 시기와 방법을 이해하는 데 도움이 됩니다.
당신이 나처럼 미친 고양이 애호가라고 가정해 봅시다! 그리고 당신은 고양이를 얼마나 좋아하는지 보여주기 위해 이 프로그램을 작성하고 싶었습니다.
class Main {
function void main ( ) {
while ( true ) {
do Output . printString ( "Kittens are so adorable! " ) ;
}
}
}
몇 초 후에 프로그램이 "ERR6" 또는 힙 오버플로와 함께 충돌한다는 사실을 알면 놀랄 수도 있습니다!
Jack은 수동으로 메모리를 관리하는 프로그래밍 언어입니다. 즉, 더 이상 필요하지 않은 메모리를 적절하게 할당 해제하도록 주의해야 합니다. 그렇지 않으면 Jack OS가 다르게 생각하여 메모리 누수를 용이하게 할 것입니다. 가장 좋은 방법은 할당 해제를 적절하게 캡슐화하는 개체를 나타내는 각 클래스에 대한 dispose
메서드를 제공하는 것입니다. 따라서 객체가 더 이상 필요하지 않은 경우 해당 dispose
메서드를 호출하여 결국 힙 메모리가 부족해지지 않도록 할 수 있습니다.
C와 같이 수동으로 메모리를 관리하는 다른 언어로 프로그래밍한 경우 이는 매우 익숙해 보일 것입니다. 주요 차이점 중 하나는 Jack OS가 스택이 아닌 힙에 배열과 문자열을 저장한다는 것입니다. 이는 프로그램이 힙 오버플로로 인해 충돌하는 이유를 암시합니다.
동료 고양이 매니아들을 위해 이 프로그램을 수정해 봅시다.
class Main {
function void main ( ) {
var String s ;
while ( true ) {
let s = "Kittens are so adorable! " ;
do Output . printString ( s ) ;
do s . dispose ( ) ;
}
}
}
또는 문자열에 대한 메모리를 한 번만 할당할 수도 있습니다.
class Main {
function void main ( ) {
var String s ;
let s = "Kittens are so adorable! " ;
while ( true ) {
do Output . printString ( s ) ;
}
}
}
이러한 대체 버전은 문자열을 훨씬 빠르게 인쇄할 뿐만 아니라 이번에는 실제로 영원히 인쇄한다는 점을 알 수 있습니다! 만세!
자신만의 dispose
메서드를 작성하는 방법을 더 잘 이해할 수 있도록 String.dispose
를 빠르게 살펴보겠습니다.
method void dispose ( ) {
do stringArray . dispose ( ) ;
do Memory . deAlloc ( this ) ;
}
stringArray
에 의해 호출되는 Array.dispose
method void dispose ( ) {
do Memory . deAlloc ( this ) ;
}
적절한 dispose
메서드는 먼저 필드 변수에 대해 적절하게 dispose
호출한 다음 do Memory.deAlloc(this);
로 마무리해야 합니다. 객체 인스턴스 자체의 할당을 취소합니다.
Jack과 NAND가 얼마나 원시적인가를 고려하면 언어 내의 풋건은 불가피합니다. 나는 여러분이 알아야 할 다음과 같은 정의되지 않은 동작의 인스턴스를 (제 생각에는) 가장 중요한 것부터 가장 덜 중요한 것 순으로 정리했습니다.
나는 이 주의 사항이 매우 중요하다고 생각하여 이 섹션의 시작 부분으로 옮겼습니다.
잭 표현
a > b
a < b
믿을 수 없을 정도로 간단합니다. 수학적으로 항상 정확하지는 않으며 각각 Java 표현식과 동일합니다.
( ( a - b ) & ( 1 << 15 ) ) == 0 && a != b
( ( a - b ) & ( 1 << 15 ) ) != 0
뉘앙스가 어때요? 가상 머신 구현은 a > b
a - b > 0
으로 변환합니다. 문제는 다음과 같습니다. a - b
오버플로될 수 있습니다.
20000 > -20000
무엇으로 평가되나요? 가상 머신은 이를 20000 - (-20000) > 0
으로 변환하여 -25336 > 0
으로 평가합니다. 불행히도 대답은 false
입니다.
그러나 20000 > -10000
30000 > 0
또는 true
로 평가됩니다.
짐작하셨겠지만, a
와 b
사이의 절대 거리가 32767보다 크면 a > b
및 a < b
가 잘못된 것입니다. 그렇지 않으면 괜찮습니다.
이는 구현 버그가 아니라 Nand와 Tetris 자체의 불일치입니다. 자세한 내용은 여기를 참조하세요. 호환성상의 이유로 이 동작은 수정되지 않습니다.
-32768은 그 종류 중 하나입니다. 이는 -(-32768) = -32768, 즉 양수 대응 * 이 없는 싱글톤과 같은 속성을 보유하는 유일한 숫자입니다. 이로 인해 불건전함과 논리 오류가 발생할 수 있습니다.
/**
* 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
내부적으로 Math.abs
양수를 반환할 것으로 예상합니다. -32768의 경우에는 그렇지 않으므로 Jack OS가 오작동합니다.
주요 관심사는 부정 연산자를 사용하여 논리 오류를 처리하는 것입니다. 프로그래머로서 음수의 음수가 양수임을 보장하려면 -32768의 경우를 확인하고 적절한 조치를 취하는 것은 귀하의 책임입니다.
* 이는 NAND의 ALU가 내부적으로 Jack 표현 -x
~(x - 1)
로 처리하기 때문에 사실입니다. x
-32768
로 설정하고 단계별로 평가해 보겠습니다. 다음은 계산에 대한 해당 16비트 2의 보수 이진 표현입니다.
x
= 1000 0000 0000 0000
x - 1
= 0111 1111 1111 1111
~(x - 1)
= 1000 0000 0000 0000
= x
그것은 같은 것입니다! 여기서 무슨 일이 일어났나요? NAND는 16비트 시스템이기 때문에 -32768은 여기서 1을 빼면 반전된 비트를 얻을 수 있는 유일한 숫자입니다. 즉, -32768은 x - 1 = ~x
충족하여 표현식을 ~(~x)
또는 x
로 단순화합니다.
이것은 설명이 필요 없으므로 여기에 간단한 데모가 있습니다.
/**
* 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." ) ;
}
}
반면에 인수가 너무 많은 함수를 호출하는 것은 완벽하게 유효합니다. 추가 함수 인수를 색인화하려면 arguments
키워드를 사용할 수 있습니다. 인수 개수에 대한 표시기가 없습니다.
Array
활용하여 변수를 다른 유형으로 캐스팅할 수 있습니다. 유형 캐스팅 변수에 존재하지 않는 인스턴스 메서드를 호출하는 것은 정의되지 않은 동작입니다. 컴파일러는 여러분이 이 일을 하고 있다는 것을 깨달을 만큼 똑똑하지 않습니다.
/**
* 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 ( ) ) ;
}
}
자세한 예제는 Overflow 프로그램을 참조하세요.
메모리 주소 256
~ 2047
및 1
~ 15
에 각각 상주하는 스택 프레임이나 내부 레지스터를 수정하면 정의되지 않은 동작이 발생할 수 있습니다. 이는 일반적으로 Memory.poke
또는 음수 배열 인덱싱을 잘못 사용하지 않고는 불가능합니다. 자세한 예는 SecretPassword 프로그램을 참조하세요.
NAND는 .jack
파일에 대해서는 프로그램 검증을 제공하지만 .vm
파일에는 프로그램 검증을 제공하지 않습니다. 이는 NAND의 가상 머신 컴파일러가 존재하지 않는 함수를 호출하거나 할당되지 않은 변수를 참조하거나 기타 논리적으로 유효하지 않은 메모리 작업을 수행할 수 있는 자유를 제공한다는 것을 의미합니다. 이러한 정의되지 않은 동작의 대부분의 경우 가상 머신은 탈출하고 화면에는 아무 것도 표시되지 않습니다. 프로그램을 직접 디버깅하는 것은 귀하의 책임입니다.
1970년대 등장한 이래로 16비트 컴퓨팅이 현대 시대에 인기를 끌지 못한 데에는 그럴 만한 이유가 있습니다. 32비트 또는 64비트 컴퓨팅과 비교하여 16비트 컴퓨팅은 제한된 처리 능력과 메모리 용량을 제공하여 최신 소프트웨어 및 애플리케이션의 요구 사항을 충족하지 못했습니다.
NAND도 이러한 현실에서 예외는 아닙니다.
Nand에서 Tetris 강의 슬라이드로 가져왔습니다.
16GiB MacBook과 비교하면 NAND는 0.00002% 비율로 적은 4KiB RAM을 사용합니다! 그럼에도 불구하고 우리를 달까지 데려가기에 충분했는데, NAND도 그럴 수 없다고 누가 말할 수 있겠습니까?
Jack OS는 14,336개의 메모리 주소를 예약합니다.