私のマイクロプロセッサクラスのインストラクターは私たちに課題を与えて言った:
"Cでアセンブラーを記述します。"-私の最愛の教授
だから、それは私には少し不合理に思えました。
私が間違っていなければ、アセンブリ言語は、機械語から高水準言語の旅への第一歩です。 Cはアセンブリよりも高水準の言語です。では、Cでアセンブラを作成する意味は何でしょうか。 C言語がない中で、彼らは過去に何をしていましたか?彼らは機械語でアセンブラーを書いていましたか?
高水準言語で低水準言語用の機械コードトランスレータを書くのは、私には意味がありません。
たとえば、そのアーキテクチャ用のCコンパイラすらない、まったく新しいマイクロプロセッサアーキテクチャを作成したとします。 Cで記述されたアセンブラは、新しいアーキテクチャをシミュレートできますか?それは役に立たないでしょうか?
ちなみに、GNUアセンブラーとNetwideアセンブラーはCで書かれていることを知っています。また、なぜCで書かれているのでしょうか。
最後に、これは私たちの教授が私たちにくれた簡単なアセンブラのサンプルソースコードです。
// to compile, gcc assembler.c -o assembler
// No error check is provided.
// Variable names cannot start with 0-9.
// hexadecimals are twos complement.
// first address of the code section is zero, data section follows the code section.
//fout tables are formed: jump table, ldi table, label table and variable table.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//Converts a hexadecimal string to integer.
int hex2int( char* hex)
{
int result=0;
while ((*hex)!='\0')
{
if (('0'<=(*hex))&&((*hex)<='9'))
result = result*16 + (*hex) -'0';
else if (('a'<=(*hex))&&((*hex)<='f'))
result = result*16 + (*hex) -'a'+10;
else if (('A'<=(*hex))&&((*hex)<='F'))
result = result*16 + (*hex) -'A'+10;
hex++;
}
return(result);
}
main()
{
FILE *fp;
char line[100];
char *token = NULL;
char *op1, *op2, *op3, *label;
char ch;
int chch;
int program[1000];
int counter=0; //holds the address of the machine code instruction
// A label is a symbol which mark a location in a program. In the example
// program above, the string "lpp", "loop" and "lp1" are labels.
struct label
{
int location;
char *label;
};
struct label labeltable[50]; //there can be 50 labels at most in our programs
int nooflabels = 0; //number of labels encountered during Assembly.
// Jump instructions cannot be assembled readily because we may not know the value of
// the label when we encountered a jump instruction. This happens if the label used by
// that jump instruction appear below that jump instruction. This is the situation
// with the label "loop" in the example program above. Hence, the location of jump
// instructions must be stored.
struct jumpinstruction
{
int location;
char *label;
};
struct jumpinstruction jumptable[100]; //There can be at most 100 jumps
int noofjumps=0; //number of jumps encountered during Assembly.
// The list of variables in .data section and their locations.
struct variable
{
int location;
char *name;
};
struct variable variabletable[50]; //There can be 50 varables at most.
int noofvariables = 0;
//Variables and labels are used by ldi instructions.
//The memory for the variables are traditionally allocated at the end of the code section.
//Hence their addresses are not known when we assemble a ldi instruction. Also, the value of
//a label may not be known when we encounter a ldi instruction which uses that label.
//Hence, the location of the ldi instructions must be kept, and these instructions must be
//modified when we discover the address of the label or variable that it uses.
struct ldiinstruction
{
int location;
char *name;
};
struct ldiinstruction lditable[100];
int noofldis=0;
fp = fopen("name_of_program","r");
if (fp != NULL)
{
while(fgets(line,sizeof line,fp)!= NULL) //skip till .code section
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".code")==0 )
break;
}
while(fgets(line,sizeof line,fp)!= NULL)
{
token=strtok(line,"\n\t\r "); //get the instruction mnemonic or label
//======================================== FIRST PASS ======================================================
while (token)
{
if (strcmp(token,"ldi")==0) //---------------LDI INSTRUCTION--------------------
{
op1 = strtok(NULL,"\n\t\r "); //get the 1st operand of ldi, which is the register that ldi loads
op2 = strtok(NULL,"\n\t\r "); //get the 2nd operand of ldi, which is the data that is to be loaded
program[counter]=0x1000+hex2int(op1); //generate the first 16-bit of the ldi instruction
counter++; //move to the second 16-bit of the ldi instruction
if ((op2[0]=='0')&&(op2[1]=='x')) //if the 2nd operand is twos complement hexadecimal
program[counter]=hex2int(op2+2)&0xffff; //convert it to integer and form the second 16-bit
else if (( (op2[0])=='-') || ((op2[0]>='0')&&(op2[0]<='9'))) //if the 2nd operand is decimal
program[counter]=atoi(op2)&0xffff; //convert it to integer and form the second 16-bit
else //if the second operand is not decimal or hexadecimal, it is a laber or a variable.
{ //in this case, the 2nd 16-bits of the ldi instruction cannot be generated.
lditable[noofldis].location = counter; //record the location of this 2nd 16-bit
op1=(char*)malloc(sizeof(op2)); //and the name of the label/variable that it must contain
strcpy(op1,op2); //in the lditable array.
lditable[noofldis].name = op1;
noofldis++;
}
counter++; //skip to the next memory location
}
else if (strcmp(token,"ld")==0) //------------LD INSTRUCTION---------------------
{
op1 = strtok(NULL,"\n\t\r "); //get the 1st operand of ld, which is the destination register
op2 = strtok(NULL,"\n\t\r "); //get the 2nd operand of ld, which is the source register
ch = (op1[0]-48)| ((op2[0]-48) << 3); //form bits 11-0 of machine code. 48 is ASCII value of '0'
program[counter]=0x2000+((ch)&0x00ff); //form the instruction and write it to memory
counter++; //skip to the next empty location in memory
}
else if (strcmp(token,"st")==0) //-------------ST INSTRUCTION--------------------
{
//to be added
}
else if (strcmp(token,"jz")==0) //------------- CONDITIONAL JUMP ------------------
{
//to be added
}
else if (strcmp(token,"jmp")==0) //-------------- JUMP -----------------------------
{
op1 = strtok(NULL,"\n\t\r "); //read the label
jumptable[noofjumps].location = counter; //write the jz instruction's location into the jumptable
op2=(char*)malloc(sizeof(op1)); //allocate space for the label
strcpy(op2,op1); //copy the label into the allocated space
jumptable[noofjumps].label=op2; //point to the label from the jumptable
noofjumps++; //skip to the next empty location in jumptable
program[counter]=0x5000; //write the incomplete instruction (just opcode) to memory
counter++; //skip to the next empty location in memory.
}
else if (strcmp(token,"add")==0) //----------------- ADD -------------------------------
{
op1 = strtok(NULL,"\n\t\r ");
op2 = strtok(NULL,"\n\t\r ");
op3 = strtok(NULL,"\n\t\r ");
chch = (op1[0]-48)| ((op2[0]-48)<<3)|((op3[0]-48)<<6);
program[counter]=0x7000+((chch)&0x00ff);
counter++;
}
else if (strcmp(token,"sub")==0)
{
//to be added
}
else if (strcmp(token,"and")==0)
{
//to be added
}
else if (strcmp(token,"or")==0)
{
//to be added
}
else if (strcmp(token,"xor")==0)
{
//to be added
}
else if (strcmp(token,"not")==0)
{
op1 = strtok(NULL,"\n\t\r ");
op2 = strtok(NULL,"\n\t\r ");
ch = (op1[0]-48)| ((op2[0]-48)<<3);
program[counter]=0x7500+((ch)&0x00ff);
counter++;
}
else if (strcmp(token,"mov")==0)
{
//to be added
}
else if (strcmp(token,"inc")==0)
{
op1 = strtok(NULL,"\n\t\r ");
ch = (op1[0]-48)| ((op1[0]-48)<<3);
program[counter]=0x7700+((ch)&0x00ff);
counter++;
}
else if (strcmp(token,"dec")==0)
{
//to be added
}
else //------WHAT IS ENCOUNTERED IS NOT AN INSTRUCTION BUT A LABEL. UPDATE THE LABEL TABLE--------
{
labeltable[nooflabels].location = counter; //buraya bir counter koy. error check
op1=(char*)malloc(sizeof(token));
strcpy(op1,token);
labeltable[nooflabels].label=op1;
nooflabels++;
}
token = strtok(NULL,",\n\t\r ");
}
}
//================================= SECOND PASS ==============================
//supply the address fields of the jump and jz instructions from the
int i,j;
for (i=0; i<noofjumps;i++) //for all jump/jz instructions
{
j=0;
while ( strcmp(jumptable[i].label , labeltable[j].label) != 0 ) //if the label for this jump/jz does not match with the
j++; // jth label in the labeltable, check the next label..
program[jumptable[i].location] +=(labeltable[j].location-jumptable[i].location-1)&0x0fff; //copy the jump address into memory.
}
// search for the start of the .data segment
rewind(fp);
while(fgets(line,sizeof line,fp)!= NULL) //skip till .data, if no .data, also ok.
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".data")==0 )
break;
}
// process the .data segment and generate the variabletable[] array.
int dataarea=0;
while(fgets(line,sizeof line,fp)!= NULL)
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".code")==0 ) //go till the .code segment
break;
else if (token[strlen(token)-1]==':')
{
token[strlen(token)-1]='\0'; //will not cause memory leak, as we do not do malloc
variabletable[noofvariables].location=counter+dataarea;
op1=(char*)malloc(sizeof(token));
strcpy(op1,token);
variabletable[noofvariables].name=op1;
token = strtok(NULL,",\n\t\r ");
if (token==NULL)
program[counter+dataarea]=0;
else if (strcmp(token, ".space")==0)
{
token=strtok(NULL,"\n\t\r ");
dataarea+=atoi(token);
}
else if((token[0]=='0')&&(token[1]=='x'))
program[counter+dataarea]=hex2int(token+2)&0xffff;
else if (( (token[0])=='-') || ('0'<=(token[0])&&(token[0]<='9')) )
program[counter+dataarea]=atoi(token)&0xffff;
noofvariables++;
dataarea++;
}
}
// supply the address fields for the ldi instructions from the variable table
for( i=0; i<noofldis;i++)
{
j=0;
while ((j<noofvariables)&&( strcmp( lditable[i].name , variabletable[j].name)!=0 ))
j++;
if (j<noofvariables)
program[lditable[i].location] = variabletable[j].location;
}
// supply the address fields for the ldi instructions from the label table
for( i=0; i<noofldis;i++)
{
j=0;
while ((j<nooflabels)&&( strcmp( lditable[i].name , labeltable[j].label)!=0 ))
j++;
if (j<nooflabels){
program[lditable[i].location] = (labeltable[j].location)&0x0fff;
printf("%d %d %d\n", i, j, (labeltable[j].location));
}
}
//display the resulting tables
printf("LABEL TABLE\n");
for (i=0;i<nooflabels;i++)
printf("%d %s\n", labeltable[i].location, labeltable[i].label);
printf("\n");
printf("JUMP TABLE\n");
for (i=0;i<noofjumps;i++)
printf("%d %s\n", jumptable[i].location, jumptable[i].label);
printf("\n");
printf("VARIABLE TABLE\n");
for (i=0;i<noofvariables;i++)
printf("%d %s\n", variabletable[i].location, variabletable[i].name);
printf("\n");
printf("LDI INSTRUCTIONS\n");
for (i=0;i<noofldis;i++)
printf("%d %s\n", lditable[i].location, lditable[i].name);
printf("\n");
fclose(fp);
fp = fopen("RAM","w");
fprintf(fp,"v2.0 raw\n");
for (i=0;i<counter+dataarea;i++)
fprintf(fp,"%04x\n",program[i]);
}
}
人々は機械語でアセンブラを書いています。また、アセンブリ言語で記述されていることもよくあります。多くの場合、翻訳言語のサブセットなので、アセンブラの単純な「ブートストラップ」バージョンから始め、アセンブラ自体に必要な機能を追加します。
ただし、これは特に必要なことではありません。結局のところ、アセンブラは(通常はかなり)単純な翻訳プログラムです。ファイルを1つの(テキスト)形式で取り込み、ファイルを別の形式(通常はオブジェクトファイル形式)で書き出します。
入力されたテキストがテキスト形式の機械語命令を表し、結果がバイナリ形式の同じ命令を表すという事実は、アセンブラの実装に使用される言語に大きな違いはありません-実際、Cよりもさらに高い言語SNOBOLとPythonは非常にうまく機能します。私は(かなり)最近Pythonで書かれたアセンブラーで作業しましたが、この作業にはかなりうまくいきました。
bootstrap最初に物事を行う方法:通常、適切な開発ツールなどを備えた別のマシン上。新しいハードウェアを開発している場合は、通常、シミュレーター(または少なくともエミュレーター)を作成することから始めます。 )とにかく新しいマシンの場合、最初は、いずれにしても、ホストシステムでコードをビルドして実行します。
存在しない接続が表示されています。
「アセンブラーの作成」は、他のプログラミングタスクと同じようにプログラミングタスクです。ツールを使用して、そのタスクに最適なタスクを処理します。アセンブラを書くことについて特別なことは何もありません。高級言語で書かない理由はまったくありません。 Cは実際にはかなり低いレベルにあり、私はおそらくC++または他のより高いレベルの言語を好むでしょう。
アセンブリ言語は、実際にはこのような作業にはまったく適していません。アセンブリ言語を合理的に使用するケースは非常にまれです。高級言語では表現できないことをする必要がある場合のみ。
C言語がない中で、彼らは過去に何をしていましたか?彼らは機械語でアセンブラーを書いていましたか?
アセンブリは、本質的にはマシンコードのニーモニックです。機械語の各オペコードには、アセンブリニーモニックが与えられます。つまり、x86ではNOPは0x90です。これにより、アセンブラーはかなり単純になります(n.b.ほとんどのアセンブラーには2つのパスがあり、1つは変換するため、2つ目はアドレス/参照を生成/解決するためです。)より優れたバージョンが作成され、手動で「組み立てられた」アセンブラで組み立てられます。このようにして新しい機能が追加されます。この方法で新しい言語のコンパイラを構築できます。以前は、コンパイラーがアセンブリーを出力し、バックエンドにアセンブラーを使用するのが一般的でした!
高水準言語で低水準言語用の機械コードトランスレータを書くのは、私には意味がありません。 ... [既存のアセンブラー]はCで書かれています。なぜCで書かれているのでしょうか。
self-hosting はプログラミング言語に共通のマイルストーン/望ましい機能ですが、アセンブリは非常に低いレベルなので、ほとんどのプログラマはより高いレベルで作業することを好みます。つまり、アセンブリでアセンブラを書くことを誰も望んでいない(または実際には他の何か)
たとえば、そのアーキテクチャ用のCコンパイラすらない、まったく新しいマイクロプロセッサアーキテクチャを作成したとします。
Bootstrapping は、新しいアーキテクチャでツールチェーンを取得するプロセスです。
基本的なプロセスは次のとおりです。
これを行うために、アセンブリ(新旧)で記述する必要は一度もありません。アセンブラ/バックエンド/コードジェネレータを記述するのに最適な言語を選択する必要があります。
Cで記述されたアセンブラは、新しいアーキテクチャをシミュレートできますか?
アセンブラはシミュレートしません!
新しい(または既存の)機械語を使用して新しいCPUを開発していた場合、通常、テストにはシミュレータが必要です。つまり、シミュレーターでランダムな命令とデータを実行し、プロトタイプCPUで同じ命令とデータと出力を比較します。次に、バグを見つけ、バグを修正し、繰り返します。
質問のこの部分のみに具体的に対処します。
「ちなみに、GNU AssemblerとNetwide AssemblerがCで記述されていることを知っています。なぜCで記述されているのでしょうか。」
Netwide Assemblerを最初に作成したチームの一部として言えば、当時、私たちにはその決定が非常に明白であるように思われましたが、基本的に他のオプションは考慮していませんでした。次の理由:
これにより、決定が非常に簡単になりました。ANSI準拠のC(最近ではC89とも呼ばれます)は、これらすべての点に本当に当てはまった唯一の言語でした。当時標準化されたC++があったとしたらmayはそれを考慮しましたが、異なるシステム間のC++サポートはその当時かなりパッチが多かったため、移植可能なC++を書くことは少し悪夢でした。
C(またはその他の高水準言語)でアセンブラーを作成する理由の中には、その高水準言語で他のプログラムを書くことを正当化するために使用できるすべての理由があります。この場合の主なものは、おそらく移植性と使いやすさです。
移植性:ネイティブ言語でアセンブラーを作成する場合、そのプラットフォーム上にアセンブラーがあります。 Cで作成する場合、Cコンパイラを備えた任意のプラットフォームにアセンブラがあります。これにより、たとえば、ワークステーション上の組み込みプラットフォーム用のコードをコンパイルして、ターゲットデバイスで直接すべてを実行する必要がなく、バイナリを移動できます。
使いやすさ:ほとんどの人にとって、プログラムが高水準言語である場合は、アセンブラーまたは(より悪い)生のマシンコードよりも、プログラムの読み取り、推論、および変更がはるかに自然です。したがって、下位言語で責任を負う特徴点について考える必要がなく、上位言語によって提供される抽象化の観点から考えることができるので、上位言語でアセンブラを開発および保守する方が簡単です。
1つは他とまったく関係ありません。 Webブラウザーはhtmlまたはphpまたはその他のWebコンテンツ言語を使用して厳密に記述されていますか?いいえ、なぜそうするのでしょうか。車は人間ではなく他の車でのみ運転できますか?
1つのビットの塊(一部のASCII)を別のビットの塊(一部のマシンコード)に変換することは単なるプログラミングタスクであり、そのタスクに使用するプログラミング言語は任意のものです。多くの異なる言語で書かれたアセンブラを使用できます。
新しい言語は、コンパイラ/アセンブラがまだないため、最初は自分の言語で書くことはできません。新しい言語用の既存のコンパイラがない場合は、他の言語で最初のコンパイラを作成する必要があり、最終的にbootstrapそれがブートストラップでも意味がある場合。(htmlおよびWebブラウザ、いくつかのビットを受け取り、いくつかのビットを吐き出すプログラムは、決してhtmlで書かれることはありません)。
新しい言語である必要はなく、既存の言語でもかまいません。新しいCまたはC++コンパイラは、ゲートから自動的にコンパイルされません。
アセンブリ言語とCの場合、ほとんどすべての新規または変更された命令セットの最初の2つの言語。私たちは過去ではなく、現在にいます。 CまたはJavaまたはpythonまたは、まだ存在しない場合でも、必要な命令セットおよびアセンブリ言語用の何でも)でアセンブラを簡単に生成できます。同様にアセンブラがまだ存在しない場合でも、必要なアセンブリ言語のアセンブリ言語を出力できる多くの再ターゲット可能なCコンパイラです。
それがまさに新しい命令セットで行うことです。新しい命令セットやアセンブラ用にコンパイルされていないCコンパイラを使用して、新しい命令セットで実行されていないコンピュータをいくつか取り、クロスアセンブラとクロスコンパイラを作成します。ロジックを作成およびシミュレーションしながら、それを開発して使用します。理想的にはすべてのツールとロジックが準備できていると見なされるまで、バグを見つけてバグを修正し、テストを繰り返すという通常の開発サイクルを実行します。ターゲットによっては、それがオペレーティングシステムを実行できないマイクロコントローラーであるとすると、bootstrapという理由で、ツールチェーンがネイティブの命令セットを使用して生成および実行する理由はありません。ウェイバックマシン以外では、アセンブラーでアセンブラーを記述することは意味がありません。
はい、戻ったり戻ったりするふりをすることができたとしても、最初のアセンブラーは鉛筆と紙を持った人間であり、彼らにとって意味のある何かを書き、次にその論理に意味のあるビットを書きました。次に、スイッチまたは他の方法を使用してビットをマシン(google pdp8またはpdp11またはaltair 8800)に取り込み、何かを実行させました。最初は十分に長くじっと見つめるか、チップのいくつかの回転を回転させることによってロジックを正しくする必要があっただけのコンピューターシミュレーターはありませんでした。今日のツールは十分に優れているので、A0の成功を収めることができます。これは、単なる大きな抵抗ではありません。多くの機能が動作しますが、完全にシミュレートできなかったものにはまだスピンが必要な場合がありますが、今すぐ起動できます。 verlogとvhdlツールの膨大な配列があるため(単にverilogにもvhdlにも書かれておらず、新しく作成されていないプロセッサーで実行されていないため)、3番目または4番目のスピンを待つ必要がない最初のspi。
予想通り、ウェイバックマシンでは、手作業で組み立てたコードを取得し、それを使用してテープやカードからプログラムをロードします。また、アセンブラーをマシンコードで手作業でコーディングします。これは本格的なものではなく、プログラミングを少し簡単にするものです。次に、そのツールを使用して、より高度なまたは複雑な言語(マクロアセンブラ)を処理できるツールを作成し、そのツールを使用して、さらに複雑な言語を作成すると、FORTRAN、BASIC、Bなどになります。そして、同じ言語でのブートストラップについて考え始め、クロスコンパイラーをネイティブコンパイラーに書き直します。もちろん、理想的には、そのための何らかの環境またはオペレーティングシステムが必要です。
シリコンを作成またはテストするときは、1と0の信号を見つめる必要があります。ツールはデフォルトでバイナリまたは16進数を表示します。一部のツールではルックアップを行うことも可能です。そのため、ツールはニーモニック(おそらくアセンブリ)を表示しますが、エンジニア(シリコン/ハードウェアおよびソフトウェア)は十分に読み取ることができます。マシンコード、または逆アセンブリ/リストを使用して、指示を「確認」します。
何をしているのかによっては、テストを書き直して再コンパイルまたは再アセンブルするのではなく、いくつかのマシンコードをテストベクタに押し込むだけの場合もあります。たとえば、ある深さでパイプラインとプリフェッチがある場合、プログラムの終わりを超えていくつかのnopsまたは他の実際の命令を埋めて、パイプが未定義の命令を吐き出さないようにする必要がある場合があります。コンパイラー、アセンブラー、またはリンカーに実行させるのではなく、最低レベルのリスト/ファイルにマシンコードを入力します。
もちろん、プロセッサをテストするときは、未定義のビットやおそらく気にしないビットなどを処理する必要があります。そのため、マシンコードにアクセスして、正常に動作するプログラムの命令の1つ以上の特定のビットを変更する必要があります。これを行うためのプログラムを作成する価値はありますか、それとも手動で行うだけですか。同様に、ECCをテストするときは、1つまたは複数のビットを反転して、それらが訂正またはトラップされることを確認します。確かに、それを行うためのプログラムを書くのははるかに簡単ですし、手作業でそれを行うこともできます。
もちろん、プロセッサ、初期のPascal、Java、Pythonなどで実行されるコードを生成しない言語もあります。これらの言語を使用するには、VMが他の言語で書かれている必要があります。 Javaコンパイラを使用してJava vmを作成することはできません。言語の設計によっては意味がありません。
(これらの言語の純粋な実装の後、最終的に誰かがvm命令セットではなく実際の命令セットをターゲットとする可能性のある未純粋なバックエンドを構築し、その場合、その言語を使用して自分自身またはそのvmをコンパイルして、必要です。gnuJava gccのフロントエンドなど)。
時間の経過とともに、おそらくCでCコンパイラを記述しない可能性があります。Bison/ Flexなど、他のプログラミング言語を使用して、Cの生成に使用しましたが、自分で記述したくありませんでした。一部の割合はCにありますが、一部の割合は、ビットを入力および出力する他のコンパイラを使用する他の言語にあります。このアプローチは、アセンブラの生成にも使用される場合があります。コンパイラー/アセンブラー(ビットを入力してから他のビットを出力するタスクを持つプログラム)の設計者は、それをどのように実装するかについて。プログラムで生成されたパーサーは、手作業で確実にプログラムできますが、時間がかかるため、ショートカットを探す必要があります。あなたがアセンブラーでアセンブラーを書くことができるように、人々は近道を探します。
Webブラウザは、いくつかのビットを取り込み、他のビットを吐き出すプログラムにすぎません。アセンブラは、いくつかのビットを受け取り、他のいくつかのビットを吐き出す単なるプログラムです。コンパイラは、いくつかのビットを取り込み、他のビットを吐き出すプログラムにすぎません。これらすべてについて、各プログラミングタスクの入力ビットと出力ビットに関する文書化された一連のルールがあります。これらのタスクとビットは十分に汎用的であり、あらゆるAVAILABLEプログラミング言語を使用できます(ビット/バイト操作を実行し、入力と出力を処理できます)。ここのキーが利用可能です。 Linuxをゼロから入手して試してください。フロントパネルをシミュレートしたpdp8、pdp11、altair 8800、またはその他のシミュレーターを試してください。