Implementing Assembler
ဒီ tutorial မွာ assembly ဆိုၿပီး တခ်ိန္လုံးသုံးေနေပမဲ့ ကၽြန္ေတာ္တို႔ ခုေရးေနတဲ့ push stack တို႕ pop stack တို႕ဆိုတာ machine codes ေတြလို႕ေျပာရင္ ရပါတယ္။ ဟုတ္တယ္ေလ၊ သူတို႕က enum နဲ႔ေၾကညာထား တဲ့ number ေတြမဟုတ္လား။ ဒါက ကၽြန္ေတာ္တို႕ Virtual Machine အတြက္ machine code ေတြေပါ့။ ခုေလာက္ဆို machine codes ေတြကို manually ထည့္ေပးေနရတာ စိတ္ညစ္ေရာေပါ့ :) ဟုတ္တယ္ေလ။ အနဲဆုံး "MyProgram.Add(Instruction(..." ဆိုတာကို instruction တိုင္းမွာထည့္ေပးေနရတယ္။ဒီေတာ့ instruction ေတြကို text file ထဲမွာေရး။ ၿပီးမွ file ထဲကေနလွမ္းဖတ္လို႕ ရေတာ့ရပါတယ္။ ဒါေပမဲ့ Op-code ေတြကို number ေတြအေနနဲ႔ file ထဲမွာ ေရးရပါလိမ့္မယ္ (OpCode ကို enum အျဖစ္ေၾကညာ ထားတယ္ေလ၊ enum ဆိုတာ တကယ္ေတာ့ integer data type ပါဘဲ)။ ဒီေတာ့ေရးရတာ ပိုမခက္ လာဘူးလား။
ဒီအတြက္ solution ကလြယ္ပါတယ္။ File ထဲမွာ Op-code တခုခ်င္းဆီကို နာမည္ (string) နဲ႔ေရး၊ ၿပီးရင္ File ကိုဖတ္ၿပီး ရွိၿပီးသား op-code နာမည္ list ထဲကေန တိုက္စစ္၊ ၿပီးရင္ သူရဲ႕ သက္ဆိုင္ရာ op-code number ကို replace လုပ္ေပးလိုက္ရုံေပါ့။ ဒါဆို ကိုယ္နားလယ္တဲ့ op-code ကို number မဟုတ္ဘဲ နာမည္နဲ႔ေရးလို႕ ရၿပီေလ။ Sound familiar? ဟုတ္ပါတယ္။ ကၽြန္ေတာ္တို႕ ခုေျပာေနတာ Assembler အေၾကာင္းပါ။
ခုကၽြန္ေတာ္တို႕ high-level language အတြက္ compilier ကိုမေရးခင္ low-level machine code ေတြ ထုတ္ေပးဖို႕ assembler အရင္ေရးဖို႕လိုပါတယ္။ ဒါမွ compilier ထုတ္ေပးတဲ့ assembly codes ေတြကို assembler က machine codes (byte-code လို႔လဲေခၚတယ္) ေျပာင္းေပး၊ ရလာတဲ့ machine codes ေတြ ကိုမွ ကၽြန္ေတာ္တို႕ ခုေရးထားတဲ့ Virtual Machine ေပၚတင္ run လို႔ရမွာေလ။ တဆင့္ခ်င္းဆီေပါ့ :) တျခား programming language ေတြအားလုံးလဲ ဒီအစီအစဥ္အတိုင္း အလုပ္လုပ္ၾကတာ သိၾကမွာပါ။
ကၽြန္ေတာ္တို႕ assembler ကိုမေရးခင္ syntax တခ်ိဳ႕သတ္မွတ္ထားဖို႕လိုပါလိမ့္မယ္။ ဒါမွာ file ကေန ဖတ္ရင္ ဘယ္လို format နဲ႔ဖတ္ရမလဲဆိုတာ သိမွာ။ (ဒီေနရာမွာ ကၽြန္ေတာ္ေရးမယ့္ syntax ကိုမႀကိဳက္ရင္ ႀကိဳက္တဲ့ syntax ေတြသတ္မွတ္လို႕ရပါတယ္။ ကိုယ္ပိုင္ language ေရးေနတာဘဲ မဟုတ္ဘူးလား၊ make your own rules ေပါ့)
Instruction ေတြကိုဖတ္တဲ့အခါ လိုင္းတစ္လိုင္းကို instruction တခုလို႕ယူပါမယ္။ တလိုင္းမွာ line no ရယ္၊ instruction ရယ္ မျဖစ္မေနပါရပါမယ္။ ေနာက္ၿပီးရင္ operand တခု (သို႕) ႏွစ္ခု ပါေကာင္းပါႏိုင္ပါတယ္။ မပါရင္လဲ operand ေတြကို zero အျဖစ္ယူဆပါမယ္။ Line no ထဲ့ရတာကေတာ့ ေနာက္ကိုယ္ျပန္ၾကည့္ ခ်င္လဲ လြယ္ေအာင္ number တပ္ထားတာ ျဖစ္ပါတယ္။ ဒီေနရာမွာ မတပ္လဲရပါတယ္။ အဲဒါေတြၾကားထဲ မွာ space (သို႕) tab တခု (ဒါမွမဟုတ္) တခုထက္ပို ခံထားရပါတယ္။ ဘယ္ေနရာမွာ မဆို // (သို႔) ; ဆိုတာရဲ႕ေနာက္ကေန ေအာက္တလိုင္းအထိကို comment အျဖစ္သတ္မွတ္ပါမယ္ (Assembler ကေက်ာ္ဖတ္မွာကို ေျပာတာပါ)။ ဒါဆို ေအာက္က ပုံစံက ကၽြန္ေတာ္တို႕ assembler ရဲ႕ syntax ေပါ့၊
001 OP_CODE <operand1> <operand2> // commentကၽြန္ေတာ္တို႕ခု Assembler စေရးပါမယ္။ Assembler အတြက္လုပ္ေဆာင္ရမဲ့ အဆင့္ ၄ ဆင့္ရွိပါတယ္။ အဲဒါ ေတြကေတာ့၊
၁) assembly file ထဲက စာေတြကို တလိုင္းခ်င္းစီ ခြဲထုတ္ၿပီး ဖတ္ပါမယ္။
၂) ဖတ္လို႔ရလာတဲ့ line ထဲက စာေတြကို token တခုခ်င္းစီခြဲထုတ္ပါတယ္။ ဆိုလိုတာက line number, op-code, operand စသည္ျဖင့္ တခုခ်င္းစီ ခြဲထုတ္ပါမယ္။
၃) ရလာတဲ့ token တခုခ်င္းစီက string ေတြျဖစ္ေနပါတယ္။ သူတို႕ကို သက္ဆိုင္ရာ op-code ဒါမွာမဟုတ္ number ေတြေျပာင္းေပး (parse လုပ္ေပး) ရပါမယ္။
၄) ရလာတဲ့ op-code နဲ႔ operands ေတြကိုသုံးၿပီး ကၽြန္ေတာ္တို႔ရဲ႕ Program object ကိုေဆာက္ပါမယ္။
ကဲ ကၽြန္ေတာ္တို႕ရဲ႕ assembler ကိုေရးဖို႔ အေပၚက အဆင့္ေတြ တခုခ်င္းစီ implement လုပ္ယူ ရပါ မယ္။ ေရးၾကည့္ရေအာင္။
၁) assembly file ထဲက စာေတြကို တလိုင္းခ်င္းစီ ခြဲထုတ္ၿပီးဖတ္ဖို႕အတြက္ AssemblyFile ဆိုတဲ့ class တခုေဆာက္လိုက္ပါ။ သူ႕တာ၀န္ကေတာ့ CRT (Common Runtime) ထဲက FILE stream ကိုသုံးၿပီး assembly file ကို တလိုင္းခ်င္းစီ ဖတ္ဖို႕ျဖစ္ပါတယ္။
class AssemblyFile {public: FILE* f; AssemblyFile(char* file) { f = fopen(file, "rt"); } ~AssemblyFile() { fclose(f); } };AssemblyFile ရဲ႕ constructor ထဲမွာ fopen() ကိုသုံးၿပီး file ကိုဖြင့္ထားပါတယ္။ Parameter ေတြကေတာ့ ကိုယ္ဖြင့္မဲ့ file ရဲ႕လမ္းေၾကာင္းရယ္၊ "rt" ရယ္ေပးထားပါတယ္။ "r" ဆိုတာ file ကို read ဘဲလုပ္မယ္၊ write မလုပ္ဖူးလို႔ေျပာတာပါ။ "t" ဆိုတာ file ကို text mode မွာဖြင့္မယ္လို႔ေျပာထားတာပါ။ Destructor ထဲမွာေတာ့ file ကို close လုပ္ထားပါတယ္။
File ကိုဖြင့္ၿပီးသြားရင္ file ထဲကစာေတြကို တလိုင္းခ်င္းစီဖတ္ၾကည့္ရေအာင္၊
class AssemblyFile { .......... .......... bool ReadLine(char* line) { // if we reach end of file, return false to signal the reading // processing to quit if (feof(f)) return false; int i = 0; while ( !feof(f) ) { fread(&line[i], 1, 1, f); // read up to end of line or up to comments // this skip comments if (line[i] == '\n' || line[i] == '\r') break; i++; } // terminate the string by null character line[i] = 0; return true; } };ပထမ ကၽြန္ေတာ္တို႔ ေသခ်ာေအာင္ file က end of file (အဆုံး) ေရာက္ေနလား စစ္ၾကည့္ပါတယ္။ တကယ္လို႕ file အဆုံးေရာက္ေနရင္ compile လုပ္ေနတဲ့ process အဆုံးသတ္လို႕ရေအာင္ function ကေန false ျပန္ေပးပါမယ္။
ၿပီးရင္ file ထဲက စာတလုံးခ်င္းစီကို line ဆိုတဲ့ variable ထဲထည့္ပါမယ္။ တကယ္လို႕ ဖတ္တဲ့ character က end of line character ျဖစ္တဲ့ '\n' နဲ႔ '\r' ျဖစ္ေနရင္ လက္ရွိဖတ္ေနတာကို ရပ္လိုက္ပါမယ္။ ဒါဆို ကၽြန္ေတာ္ တို႕ တလိုင္းခ်င္းစီဖတ္တဲ့ AssemblyFile class ကိုေရးလို႕ၿပီးသြားပါၿပီ။
၂) ၿပီးရင္ ဖတ္လို႔ရလာတဲ့ line ထဲက စာေတြကို token တခုခ်င္းစီခြဲထုတ္ပါမယ္။ ဒီအတြက္ Tokenizer ဆိုတဲ့ class တခုေဆာက္လိုက္ပါ။ သူ႔ constructor ထဲမွာ ေပးထားတဲ့ line ကို token တခုခ်င္း စီခြဲထုတ္ပါ မယ္။ ဒီအတြက္ strtok() ဆိုတဲ့ function ကိုယူသုံးပါမယ္။ သူက ေပးထားတဲ့ delimiter ေတြအတိုင္း string ကို tokenize လုပ္ေပးပါလိမ့္မယ္။ ဒီေနရာမွာ ကၽြန္ေတာ္တို႕ရဲ႕ delimiter က space နဲ႔ tab ပါ။ ကဲေရးၾကည့္ပါမယ္။
class Tokenizer {public: vector<char*> tokens; Tokenizer(char* line) { const char* DELIMITER = " \t"; char* tok = strtok(line, DELIMITER); while (tok != NULL) { if (tok[0] == '/' || tok[0] == ';') break; tokens.push_back(strdup(tok)); tok = strtok(NULL, DELIMITER); }; } };strtok () function ကရလာတဲ့ string (token) တခုခ်င္းစီကို tokens ဆိုတဲ့ list ထဲမွာ သိမ္းထားပါမယ္။ ဒီေနရာမွာ token ထဲမွာ comment character ေတြလို႔ယူဆမဲ့ // နဲ႔ ; ကိုေတြ႕ရင္လဲ ရပ္လိုက္ပါမယ္။ ဒါဆို comment ေတြကို skip လုပ္ၿပီးသြားျဖစ္သြားပါမယ္။
သိမ္းထားတဲ့ token ေတြကို process လုပ္ၿပီးရင္ ျပန္ delete လုပ္ဖို႕လိုပါလိမ့္မယ္။ ဒါေၾကာင့္ destructor ထဲမွာ tokens list ကိုေအာက္ကလိုု delete လုပ္လိုက္ပါမယ္။
class Tokenizer { .................. .................. ~Tokenizer() { while (!tokens.empty()) { delete[] tokens.back(); tokens.pop_back(); } } };ဒါဆို ခုကၽြန္ေတာ္တို႕ tokenization လုပ္တဲ့အထိၿပီးသြားပါၿပီ။ ရလာတဲ့ token ေတြကို parse လုပ္ဖို႕ဘဲ က်န္ပါေတာ့တယ္။
၃) ရလာတဲ့ Token string ေတြကို ကိုယ္လိုခ်င္တဲ့ op-code ရဖို႕အတြက္ map လုပ္ေပးရပါမယ္။ ဒီအတြက္ map လုပ္မဲ့ table တခုေတာ့လိုပါမယ္။ အဲဒီ table မွာ column တခုက map လုပ္မဲ့ op-code ျဖစ္ၿပီး ေနာက္တခုက သူနဲ႔သက္ဆိုင္တဲ့ string ID။ ဒီလို table မ်ိဳးကို ေအာက္ကလို ေဆာက္လိုက္ပါ။
struct AssemblyCodeTableEntry { int code; char id[256]; }; const int ASM_ENTRY_COUNT = 16; static const AssemblyCodeTableEntry AssemblyCodeTable[ASM_ENTRY_COUNT] = { { OP_TALK, "talk" }, { OP_NUM, "num" }, { OP_PUT_MEM, "put_mem" }, { OP_PUSH_STACK, "push_stack" }, { OP_POP_STACK, "pop_stack" }, { OP_GET_STACK, "get_stack" }, { OP_PUT_STACK, "put_stack" }, { OP_SET_STACK, "set_stack" }, { OP_CLEAR_STACK, "clear_stack" }, { OP_ADD_STACK, "add_stack" }, { OP_ADD, "add" }, { OP_SUBTRACT, "subtract" }, { OP_MULTIPLY, "multiply" }, { OP_DIVIDE, "divide" }, { OP_MODULUS, "modulus" }, { OP_END, "end" } };ဒီေနရာမွာ Op-code အသစ္တခုထည့္တိုင္း ဒီ table မွာ entry အသစ္ထပ္ၿပီး ထည့္ေပးရပါလိမ့္မယ္။ ဒါမွ Assembler ကအသစ္ထည့္တဲ့ op-code ကို compile လုပ္ႏိုင္မွာပါ။ ဒါဆိုခု ကၽြန္ေတာ္တို႕ဆီမွာ parse လုပ္မဲ့ table လဲရွိၿပီဆိုေတာ့ Parser class ေရးဖို႕ဘဲက်န္ပါေတာ့တယ္။ ဒီေတာ့ ေအာက္ကလို Parser class ကိုေရးလိုက္ပါ။
class Parser {public: Tokenizer* tokenizer; int index; Parser(Tokenizer* tok) : tokenizer(tok), index(0) { }; int next() { // parse and retrieve next token if (index >= tokenizer->tokens.size()) return 0; return parse(tokenizer->tokens[index++]); }; int parse(char* data) { int i; // find parsed string in assembly code table for (i = 0; i < ASM_ENTRY_COUNT; i++) { if (strcmp(AssemblyCodeTable[i].id, data) == 0) return AssemblyCodeTable[i].code; } // no found. Simply return integer version of the string return atoi(data); }; };ကၽြန္ေတာ္တို႔ဆီမွာ Assembly Code Table ရွိေနတဲ့အတြက္ Parser ေရးရတာ လြယ္သြားပါတယ္။ သူ႕မွာပါတဲ့ parse() ဆိုတဲ့ function ထဲမွာ Assembly Code Table ထဲက ID string နဲ႕တိုက္စစ္ၿပီး သက္ဆိုင္ ရာ op-code ကို return ျပန္လိုက္ရုံပါဘဲ။ ဒီေနရာမွာ strcmp() ဆိုတဲ့ function သုံးထား တဲ့အ တြက္ ကၽြန္ေတာ္တို႕ assembly code က case sensitive ျဖစ္ပါတယ္။ Case sensitive မျဖစ္ေစခ်င္ရင္ stricmp() function ကိုေျပာင္းသုံးလို႕လဲရပါတယ္။
Parser class က Tokenizer ကို input အျဖစ္လက္ခံၿပီး next() ဆိုတဲ့ function ကေန parsed လုပ္ထားတဲ့ code ကို return ျပန္ပါလိမ့္မယ္။ Default အျဖစ္ (parsed လုပ္စရာ token မက်န္ေတာ့ရင္) zero ကို return ပါတယ္။ ဒီေတာ့ ကၽြန္ေတာ္တို႕ assembly code ထဲမွာ operand ေတြမေပးရင္ default operand ကို 0 လို႕ယူဆပါလိမ့္မယ္။
၄) ကဲ ခုတဆင့္ခ်င္းဆီေရးလာတာ ေနာက္ဆုံးအဆင့္အျဖစ္ ေရွ႕ကေရးလာတဲ့ class ေတြသုံးၿပီး Assembler ဆိုတဲ့ class ကိုေရးရပါေတာ့မယ္။ ေရးၾကည့္ရေအာင္၊
class Assembler {public: void compile(char* file, Program& p) { AssemblyFile f(file); char codeLine[1024]; while (f.ReadLine(codeLine)) { if (codeLine[0] == 0) continue; Tokenizer tok(codeLine); if (tok.tokens.empty()) continue; Parser parser(&tok); int codeLine = parser.next(); OpCode code = (OpCode)parser.next(); int operand1 = parser.next(); int operand2 = parser.next(); p.Add(Instruction(code, operand1, operand2)); } }; };Assembler class မွာ compile ဆိုတဲ့ function တခုဘဲပါပါတယ္။ ဒီအတြက္ Assembler class ကို utility class လုပ္လိုက္လဲရပါတယ္။ Compile ဆိုတဲ့ function ထဲမွာ ပထမ ေပးထားတဲ့ file လမ္းေၾကာင္းနဲ႔ AssemblyFile object ေဆာက္လိုက္ပါတယ္။
ၿပီးရင္ တလိုင္းခ်င္းစီ ဖတ္ပါတယ္။ တကယ္လို႕ f.ReadLine(codeLine) က false လို႕ return ျပန္ရင္ (end of file ေရာက္သြားရင္) ထြက္လိုက္ပါမယ္။ ရထားတဲ့ လိုင္းကို Tokenizer object သုံးၿပီး tokenize လုပ္ခိုင္း ပါတယ္။ တကယ္လို Tokenizer ထဲမွာ ဘာမွမရွိရင္ empty line တခုျဖစ္ပါလိမ့္မယ္။ ဒီေတာ့ အဲဒါကို skip လုပ္လိုက္ပါတယ္။
ၿပီးရင္ Parser ထဲကို Tokenizer ေပးၿပီး parse လုပ္ခိုင္းပါတယ္။ ရလာတဲ့ parsed code ေတြကို line number၊ Op-code နဲ႔ Operand ေတြကို သတ္သတ္စီခြဲၿပီး Program object ထဲသိမ္းေပးလိုက္ပါတယ္။ ဒါဆို ကၽြန္ေတာ္တို႕ရဲ႕ Assembler လုပ္တဲ့ လုပ္ငန္းက ၿပီးသြားပါၿပီ။ ကဲ main() ထဲမွာသြားၿပီး testing လုပ္ၾကည့္ရေအာင္၊
void main() { Program MyProgram; Assembler a; a.compile("test.asm", MyProgram); vm.Run(MyProgram); };ဒီေနရာမွာ "test.asm" ဆိုတဲ့ assembly file ေရးဖို႔လိုပါတယ္။ အရင္ chapter က distance တြက္တဲ့ equation ကိုဘဲ Assembly language သုံးၿပီးေရးၾကည့္ရေအာင္ပါ။ Assembly code ထဲမွာ comment ေတြပါထဲ့ေရးၿပီး အလုပ္ လုပ္မလုပ္စစ္ၾကည့္ရေအာင္ပါ။
"test.asm" ထဲမွာ ေအာက္ကလို သြားေရးပါမယ္။
// calculate: displacement = initial velocity * time + ½ acceleration * time2// initialize data first "time" variable.
Address: 0x0000, value: 200000 put_mem 0 200 // "acceleration" variable. Address: 0x0004, value: 2 001 put_mem 4 2 // "initial velocity" variable. Address: 0x0008, value: 35 002 put_mem 8 35 // calculate time * time 003 push_stack 0 004 push_stack 0 005 multiply // calculate (acceleration * time2) / 2 006 push_stack 4 007 multiply 008 push_stack 2 009 divide // calculate (initial velocity * time) 010 push_stack 4 011 push_stack 0 012 multiply // calculate (initial velocity * time) + // (acceleration * time2) / 2 013 add // save to "displacement" variable. Address: 0x000C 014 pop_stack 12 // print the displacement value on screen 015 push_stack 12 016 num 017 endၿပီးရင္ Program ကို run ၾကည့္ပါအုံး။ ေအာက္ကလို output ရပါမယ္။
Current Number: 400
Press any key to continue . . .
အရင္ chapter ကတြက္တဲ့ အေျဖနဲ႔ တူမတူ တိုက္စစ္ၾကည့္ပါအုံး။ ဒါဆို ခုကၽြန္ေတာ္တို႕ဆီမွာ Assembly ရွိေနၿပီ ျဖစ္တဲ့အတြက္ ေနာက္လာမဲ့ chapter ေတြမွာ assembly ဘဲသုံးၿပီး code ေတြ testing လုပ္ပါေတာ့ မယ္။ ဒီအတြက္ main() ထဲမွာ codes ေတြကို Program object ထဲ manually သြားထည့္ေပးစရာမလုိဘဲ "test.asm" ဆိုတဲ့ text file ထဲမွာဘဲသြားေရးစရာ လိုပါေတာ့တယ္။
No comments:
Post a Comment