1 Programming Language အသစ္ထြင္ၾကမယ္ (၄) 8th May 2009, 5:57 pm
sHa92
Founder
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 ေပါ့၊
ကၽြန္ေတာ္တို႕ခု 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 ကို တလိုင္းခ်င္းစီ
ဖတ္ဖို႕ျဖစ္ပါတယ္။
AssemblyFile ရဲ႕ constructor ထဲမွာ fopen() ကိုသုံးၿပီး file
ကိုဖြင့္ထားပါတယ္။ Parameter ေတြကေတာ့ ကိုယ္ဖြင့္မဲ့ file
ရဲ႕လမ္းေၾကာင္းရယ္၊ "rt" ရယ္ေပးထားပါတယ္။ "r" ဆိုတာ file ကို read
ဘဲလုပ္မယ္၊ write မလုပ္ဖူးလို႔ေျပာတာပါ။ "t" ဆိုတာ file ကို text mode
မွာဖြင့္မယ္လို႔ေျပာထားတာပါ။ Destructor ထဲမွာေတာ့ file ကို close
လုပ္ထားပါတယ္။
File ကိုဖြင့္ၿပီးသြားရင္ file ထဲကစာေတြကို တလိုင္းခ်င္းစီဖတ္ၾကည့္ရေအာင္၊
ပထမ ကၽြန္ေတာ္တို႔ ေသခ်ာေအာင္ 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 ပါ။ ကဲေရးၾကည့္ပါမယ္။
strtok () function ကရလာတဲ့ string (token) တခုခ်င္းစီကို tokens ဆိုတဲ့
list ထဲမွာ သိမ္းထားပါမယ္။ ဒီေနရာမွာ token ထဲမွာ comment character
ေတြလို႔ယူဆမဲ့ // နဲ႔ ; ကိုေတြ႕ရင္လဲ ရပ္လိုက္ပါမယ္။ ဒါဆို comment ေတြကို
skip လုပ္ၿပီးသြားျဖစ္သြားပါမယ္။
သိမ္းထားတဲ့ token ေတြကို process လုပ္ၿပီးရင္ ျပန္ delete
လုပ္ဖို႕လိုပါလိမ့္မယ္။ ဒါေၾကာင့္ destructor ထဲမွာ tokens list
ကိုေအာက္ကလိုု delete လုပ္လိုက္ပါမယ္။
ဒါဆို ခုကၽြန္ေတာ္တို႕ tokenization လုပ္တဲ့အထိၿပီးသြားပါၿပီ။ ရလာတဲ့ token ေတြကို parse လုပ္ဖို႕ဘဲ က်န္ပါေတာ့တယ္။
ဒီ 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 ေပါ့၊
- Code:
001 OP_CODE // 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 ကို တလိုင္းခ်င္းစီ
ဖတ္ဖို႕ျဖစ္ပါတယ္။
- Code:
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 ထဲကစာေတြကို တလိုင္းခ်င္းစီဖတ္ၾကည့္ရေအာင္၊
- Code:
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 ပါ။ ကဲေရးၾကည့္ပါမယ္။
- Code:
class Tokenizer {public: vector 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 လုပ္လိုက္ပါမယ္။
- Code:
class Tokenizer { .................. ..................
~Tokenizer() {
while (!tokens.empty()) {
delete[] tokens.back();
tokens.pop_back();
}
}
};
ဒါဆို ခုကၽြန္ေတာ္တို႕ tokenization လုပ္တဲ့အထိၿပီးသြားပါၿပီ။ ရလာတဲ့ token ေတြကို parse လုပ္ဖို႕ဘဲ က်န္ပါေတာ့တယ္။