前回の続きです。今回はコンピュータシステムの理論と実装(以下、nand2tetris本)の6章のアセンブラをC言語*1で実装してみました。
今回のコード
下記、タグv0.0.1
になります。
下記で動かせます。
git clone -b v0.0.1 https://github.com/nihemak/nand2tetris.git cd nand2tetris # download nand2tetris environment ./setup.sh # test all ./test.sh
概要
今回、実装したのはASMファイル(アセンブリコード)
を入力として受け取りHACKファイル(バイナリコード)
を生成するコマンドです。
ASMファイル(アセンブリコード)
には下記の要素が含まれます。このうちの命令
を対応するバイナリコード
に翻訳していきます。
- スキップするもの
- 空白
- コメント
- 改行(EOL)
- EOF
- 命令
- A命令:
@value
value
- アドレス(数値)
- シンボル(数値以外から始まる)
- 定義済みシンボル
- ラベルシンボル
- 変数シンボル
- C命令:
dest=comp;jump
-
dest
が省略の場合は=
も省略 -
jump
が省略の場合は;
も省略
-
- A命令:
- ラベルシンボル:
(Xxx)
下記はASMファイル(アセンブリコード)
の例です。
// Multiplies R0 and R1 and stores the result in R2. // (R0, R1, R2 refer to RAM[0], RAM[1], and RAM[2], respectively.) @R1 D=M @n M=D @R2 M=0 (LOOP) @n D=M @END D;JEQ @R0 D=M @R2 M=D+M @n M=M-1 @LOOP 0;JMP (END) @END 0;JMP
生成するHACKファイル(バイナリコード)
は次の形式です。
下記はHACKファイル(バイナリコード)
の例です。
04/mult/mult.asmのバイナリコード
0000000000000001 1111110000010000 0000000000010000 1110001100001000 0000000000000010 1110101010001000 0000000000010000 1111110000010000 0000000000010010 1110001100000010 0000000000000000 1111110000010000 0000000000000010 1111000010001000 0000000000010000 1111110010001000 0000000000000110 1110101010000111 0000000000010010 1110101010000111
なお、今回は書籍で提示された設計にできるだけ従うようにしてみました。*2またASMファイル(アセンブリコード)
にエラーが含まれていない前提としています。そして、実行時の例外に対応する処理もだいぶ省いています。*3
シンボルを含まないプログラムのためのアセンブラ
書籍ではまず下記の要素が含まれない前提のアセンブラを実装します。
- 命令
- A命令:
@value
value
- シンボル(数値以外から始まる)
- 定義済みシンボル
- ラベルシンボル
- 変数シンボル
- シンボル(数値以外から始まる)
- A命令:
- ラベルシンボル:
(Xxx)
下記のように実装を行いました。コードは06/AssemblerL/です。
下記で使えます。
cp ./nand2tetris/projects/06/max/MaxL.asm 06/AssemblerL/ # ...(省略)... cd 06/AssemblerL/ clang --std=c11 -Wall -Wextra -o AssemblerL main.c Parser.c Code.c echo "MaxL" ./AssemblerL MaxL.asm
Parserモジュール
ASMファイル(アセンブリコード)
をパースするためのモジュールです。
main.c
で利用する関数はParser.h
で下記のように定義しました。parser
構造体はtypedef
して定義はParser.c
内に隠蔽するようにしてオブジェクトとして使うようにしました。それぞれの実装はParser.c
で行いました。
06/AssemblerL/Parser.h:L19-L28
typedef struct parser * Parser; Parser Parser_init(FILE *fpAsm); bool Parser_hasMoreCommands(Parser thisObject); void Parser_advance(Parser thisObject); Parser_CommandType Parser_commandType(Parser thisObject); void Parser_symbol(Parser thisObject, char *symbol); void Parser_dest(Parser thisObject, char *dest); void Parser_comp(Parser thisObject, char *comp); void Parser_jump(Parser thisObject, char *jump);
上記の関数の中で使う関数はParser.c
内で下記のように定義しました。ASMファイルストリーム(FILE *fpAsm
)に対するfgetc
およびungetc
は下記で行うようにしています。
06/AssemblerL/Parser.c:L15-L23
bool isSpace(FILE *fpAsm); bool isComment(FILE *fpAsm); bool isEndOfFile(FILE *fpAsm); bool isEndOfLine(FILE *fpAsm); bool isAdvance(FILE *fpAsm); void skipSpaces(FILE *fpAsm); void skipEndOFLines(FILE *fpAsm); void skipComment(FILE *fpAsm); void moveNextAdvance(FILE *fpAsm);
Codeモジュール
C命令のニーモニックとバイナリのマッピング
するためのモジュールです。
main.c
で利用する関数はCode.h
で下記のように定義しました。それぞれの実装はCode.c
で行いました。
void Code_dest(char *mnemonic, char *dest); void Code_comp(char *mnemonic, char *comp); void Code_jump(char *mnemonic, char *jump);
それぞれの関数はC命令のニーモニックとバイナリのマッピング
を行いますが可読性を向上させるためCode.c
に下記の関数マクロを定義して使いました。
#define IF_CMP_CPY(condVar, condStr, destVar, destStr) if (strcmp(condVar, condStr) == 0) strcpy(destVar, destStr)
それぞれのマッピングは下記の通りです。
void Code_dest(char *mnemonic, char *dest) { IF_CMP_CPY(mnemonic, "", dest, "000"); IF_CMP_CPY(mnemonic, "M", dest, "001"); IF_CMP_CPY(mnemonic, "D", dest, "010"); IF_CMP_CPY(mnemonic, "MD", dest, "011"); IF_CMP_CPY(mnemonic, "A", dest, "100"); IF_CMP_CPY(mnemonic, "AM", dest, "101"); IF_CMP_CPY(mnemonic, "AD", dest, "110"); IF_CMP_CPY(mnemonic, "AMD", dest, "111"); } void Code_comp(char *mnemonic, char *comp) { IF_CMP_CPY(mnemonic, "0", comp, "0101010"); IF_CMP_CPY(mnemonic, "1", comp, "0111111"); IF_CMP_CPY(mnemonic, "-1", comp, "0111010"); IF_CMP_CPY(mnemonic, "D", comp, "0001100"); IF_CMP_CPY(mnemonic, "A", comp, "0110000"); IF_CMP_CPY(mnemonic, "M", comp, "1110000"); IF_CMP_CPY(mnemonic, "!D", comp, "0001101"); IF_CMP_CPY(mnemonic, "!A", comp, "0110001"); IF_CMP_CPY(mnemonic, "!M", comp, "1110001"); IF_CMP_CPY(mnemonic, "-D", comp, "0001111"); IF_CMP_CPY(mnemonic, "-A", comp, "0110011"); IF_CMP_CPY(mnemonic, "-M", comp, "1110011"); IF_CMP_CPY(mnemonic, "D+1", comp, "0011111"); IF_CMP_CPY(mnemonic, "A+1", comp, "0110111"); IF_CMP_CPY(mnemonic, "M+1", comp, "1110111"); IF_CMP_CPY(mnemonic, "D-1", comp, "0001110"); IF_CMP_CPY(mnemonic, "A-1", comp, "0110010"); IF_CMP_CPY(mnemonic, "M-1", comp, "1110010"); IF_CMP_CPY(mnemonic, "D+A", comp, "0000010"); IF_CMP_CPY(mnemonic, "D+M", comp, "1000010"); IF_CMP_CPY(mnemonic, "D-A", comp, "0010011"); IF_CMP_CPY(mnemonic, "D-M", comp, "1010011"); IF_CMP_CPY(mnemonic, "A-D", comp, "0000111"); IF_CMP_CPY(mnemonic, "M-D", comp, "1000111"); IF_CMP_CPY(mnemonic, "D&A", comp, "0000000"); IF_CMP_CPY(mnemonic, "D&M", comp, "1000000"); IF_CMP_CPY(mnemonic, "D|A", comp, "0010101"); IF_CMP_CPY(mnemonic, "D|M", comp, "1010101"); } void Code_jump(char *mnemonic, char *jump) { IF_CMP_CPY(mnemonic, "", jump, "000"); IF_CMP_CPY(mnemonic, "JGT", jump, "001"); IF_CMP_CPY(mnemonic, "JEQ", jump, "010"); IF_CMP_CPY(mnemonic, "JGE", jump, "011"); IF_CMP_CPY(mnemonic, "JLT", jump, "100"); IF_CMP_CPY(mnemonic, "JNE", jump, "101"); IF_CMP_CPY(mnemonic, "JLE", jump, "110"); IF_CMP_CPY(mnemonic, "JMP", jump, "111"); }
main.c
アセンブラのエントリポイントです。
main.c
ではコマンド引数の解析、ParserモジュールおよびCodeモジュールを用いたアセンブル処理を行います。main.c
の中で使う関数はmain.c
内で下記のように定義しました。
bool getHackFileName(char *asmFileName, char *hackFileName); void assemble(FILE *fpAsm, FILE *fpHack); void assembleACommand(Parser parser, FILE *fpHack); void assembleCCommand(Parser parser, FILE *fpHack); void getACommandValueString(int valueNumber, char *valueString);
アセンブル処理の実装は下記の通りです。ASMファイルとHACKファイルのファイルポインタを受け取りアセンブル出来次第、HACKファイルにfputs
でバイナリコードを追記しています。シンボルを含まない前提のためA命令は数値の前提で処理しています。
void assemble(FILE *fpAsm, FILE *fpHack) { Parser parser = Parser_init(fpAsm); while (Parser_hasMoreCommands(parser)) { Parser_advance(parser); switch (Parser_commandType(parser)) { case PARSER_COMMAND_TYPE_A_COMMAND: assembleACommand(parser, fpHack); break; case PARSER_COMMAND_TYPE_C_COMMAND: assembleCCommand(parser, fpHack); break; default: break; } } } void assembleACommand(Parser parser, FILE *fpHack) { char symbol[PARSER_SYMBOL_MAX_LENGTH + 1]; int valueNumber; char value[A_COMMAND_VALUE_LENGTH + 1]; Parser_symbol(parser, symbol); sscanf(symbol, "%d", &valueNumber); getACommandValueString(valueNumber, value); // 0 value fputs("0", fpHack); fputs(value, fpHack); fputs("\n", fpHack); } void assembleCCommand(Parser parser, FILE *fpHack) { char mnemonic[PARSER_MNEMONIC_MAX_LENGTH + 1]; char dest[PARSER_DEST_LENGTH + 1], comp[PARSER_COMP_LENGTH + 1], jump[PARSER_JUMP_LENGTH + 1]; Parser_dest(parser, mnemonic); Code_dest(mnemonic, dest); Parser_comp(parser, mnemonic); Code_comp(mnemonic, comp); Parser_jump(parser, mnemonic); Code_jump(mnemonic, jump); // 111 comp dest jump fputs("111", fpHack); fputs(comp, fpHack); fputs(dest, fpHack); fputs(jump, fpHack); fputs("\n", fpHack); }
シンボルを含むプログラムのためのアセンブラ
下記のように実装を行いました。コードは06/Assembler/です。
下記で使えます。
cp ./nand2tetris/projects/06/add/Add.asm 06/Assembler/ # ...(省略)... cd 06/Assembler/ clang --std=c11 -Wall -Wextra -o Assembler main.c Parser.c Code.c SymbolTable.c echo "Add" ./Assembler Add.asm
SymbolTableモジュール
シンボルとアドレスの対応を記録するためのモジュールです。
main.c
で利用する関数はSymbolTable.h
で下記のように定義しました。symbol_table
構造体はtypedef
して定義はSymbolTable.c
内に隠蔽するようにしオブジェクトとして使うようにしました。後処理が必要となるため書籍と違いSymbolTable_delete
があります。それぞれの実装はSymbolTable.c
で行いました。
06/Assembler/SymbolTable.h:L6-L12
typedef struct symbol_table * SymbolTable; SymbolTable SymbolTable_init(void); void SymbolTable_addEntry(SymbolTable thisObject, char *symbol, int address); bool SymbolTable_contains(SymbolTable thisObject, char *symbol); int SymbolTable_getAddress(SymbolTable thisObject, char *symbol); void SymbolTable_delete(SymbolTable thisObject);
今回は単純な単方向連結リストで実装しました。
06/Assembler/SymbolTable.c:L7-L18
typedef struct entry Entry; struct entry { char symbol[PARSER_SYMBOL_MAX_LENGTH + 1]; int address; Entry *nextEntry; }; struct symbol_table { Entry *table; };
main.c
下記が新規および変更した関数です。A命令のアセンブルを行うassembleACommand
関数はSymbolTable
への変数シンボル登録に対応するために引数にSymbolTable symbolTable
とint ramAddress
を追加してramAddress
を返却するよう変更しました。
int assembleACommand(Parser parser, SymbolTable symbolTable, int ramAddress, FILE *fpHack); void setDefinedSymbol(SymbolTable symbolTable); void setLabelSymbol(SymbolTable symbolTable, Parser parser);
まず、定義済みシンボルをSymbolTable
へ登録する関数です。
void setDefinedSymbol(SymbolTable symbolTable) { // LABEL, RAM Address SymbolTable_addEntry(symbolTable, "SP", 0); SymbolTable_addEntry(symbolTable, "LCL", 1); SymbolTable_addEntry(symbolTable, "ARG", 2); SymbolTable_addEntry(symbolTable, "THIS", 3); SymbolTable_addEntry(symbolTable, "THAT", 4); SymbolTable_addEntry(symbolTable, "R0", 0); SymbolTable_addEntry(symbolTable, "R1", 1); SymbolTable_addEntry(symbolTable, "R2", 2); SymbolTable_addEntry(symbolTable, "R3", 3); SymbolTable_addEntry(symbolTable, "R4", 4); SymbolTable_addEntry(symbolTable, "R5", 5); SymbolTable_addEntry(symbolTable, "R6", 6); SymbolTable_addEntry(symbolTable, "R7", 7); SymbolTable_addEntry(symbolTable, "R8", 8); SymbolTable_addEntry(symbolTable, "R9", 9); SymbolTable_addEntry(symbolTable, "R10", 10); SymbolTable_addEntry(symbolTable, "R11", 11); SymbolTable_addEntry(symbolTable, "R12", 12); SymbolTable_addEntry(symbolTable, "R13", 13); SymbolTable_addEntry(symbolTable, "R14", 14); SymbolTable_addEntry(symbolTable, "R15", 15); SymbolTable_addEntry(symbolTable, "SCREEN", 16384); SymbolTable_addEntry(symbolTable, "KBD", 24576); }
そして、ラベルシンボルをSymbolTable
へ登録する関数です。書籍に1回目のパス
と記されている処理です。
void setLabelSymbol(SymbolTable symbolTable, Parser parser) { int romAddress = 0; char symbol[PARSER_SYMBOL_MAX_LENGTH + 1]; while (Parser_hasMoreCommands(parser)) { Parser_advance(parser); switch (Parser_commandType(parser)) { case PARSER_COMMAND_TYPE_A_COMMAND: case PARSER_COMMAND_TYPE_C_COMMAND: romAddress++; break; case PARSER_COMMAND_TYPE_L_COMMAND: Parser_symbol(parser, symbol); SymbolTable_addEntry(symbolTable, symbol, romAddress); break; default: break; } } }
最後がアセンブル処理です。シンボルに関する処理が追加されています。書籍に2回目のパス
と記されている処理です。assembleCCommand
関数は変更ありませんので省略しました。
void assemble(FILE *fpAsm, FILE *fpHack) { Parser parser = Parser_init(fpAsm); int ramAddress = VARIABLE_SYMBOL_RAM_ADDRESS_START; SymbolTable symbolTable = SymbolTable_init(); setDefinedSymbol(symbolTable); setLabelSymbol(symbolTable, parser); parser = Parser_init(fpAsm); while (Parser_hasMoreCommands(parser)) { Parser_advance(parser); switch (Parser_commandType(parser)) { case PARSER_COMMAND_TYPE_A_COMMAND: ramAddress = assembleACommand(parser, symbolTable, ramAddress, fpHack); break; case PARSER_COMMAND_TYPE_C_COMMAND: assembleCCommand(parser, fpHack); break; default: break; } } SymbolTable_delete(symbolTable); } int assembleACommand(Parser parser, SymbolTable symbolTable, int ramAddress, FILE *fpHack) { char symbol[PARSER_SYMBOL_MAX_LENGTH + 1]; int valueNumber; char value[A_COMMAND_VALUE_LENGTH + 1]; Parser_symbol(parser, symbol); if (! isdigit(symbol[0])) { // symbol is /^[^\d].*/ if (SymbolTable_contains(symbolTable, symbol)) { valueNumber = SymbolTable_getAddress(symbolTable, symbol); } else { SymbolTable_addEntry(symbolTable, symbol, ramAddress); valueNumber = ramAddress; ramAddress++; } } else { sscanf(symbol, "%d", &valueNumber); } getACommandValueString(valueNumber, value); // 0 value fputs("0", fpHack); fputs(value, fpHack); fputs("\n", fpHack); return ramAddress; }
まとめ
前回から間が空いてしまいましたがようやくアセンブラの実装ができました。7章以降もできれば進めたいです。C言語が大変だった。。