(Learn with යෝකිම් බයර් - series)
පරිගණක ලෝකයේ සැරිසරන මිත්රවරුනි! ඔබ මීට පෙර පැරණි 8-bit බූරුවන් (Intel 8032 වැනි) සමඟ වැඩ කර තිබේ නම්, ඔබ දැන් ඇතුළු වීමට යන්නේ ඉතාමත් වේගවත්, වෙනස්ම ලෝකයකටයි.
අප පැරණි මයික්රොප්රොසෙසර වල එක් බයිටක් කියවීමට මයික්රෝ තත්පර ගණන් ගතකල සුවපහසු කාලයෙන් මිදී, දැන් x64 මහා බූරුවා (x64 Mega-Donkey) ගේ රාජධානියට පැමිණ සිටිමු. මූ තත්පරයකට ගිගාහර්ට්ස් 4ක (4 GHz) වේගයෙන්, බිට් 64ක විශාල කුර ගසමින්, තත්පරයකට බයිනරි උපදෙස් බාල්දි බිලියන ගණනක් හපන්නෙකි!
නමුත් ඔහුගේ වේගයට රැවටෙන්න එපා. අප Flat Assembler (FASM) සමඟ මෙම ගමන ආරම්භ කරන විට අපගේ මූලික සත්යය සැමවිටම මතක තබා ගන්න: මයික්රෝප්රොසෙසරය බූරුවෙකි (2 වන වාදය). ඌට තමන්ගේම කියා මොළයක් නැත. ඌ කිසිවක් නොදනී, ඔබ කියන දේ පමණක්ම කරයි. ඔබ ඌට මෝඩ උපදෙස් දුන්නොත්, ඌ ඉතාමත් බිහිසුණු වේගයෙන් සහ නිරවද්යතාවයෙන් එම මෝඩ වැඩේම කරනු ඇත!
පැරණි 8032 මයික්රෝකොන්ට්රෝලරය තුල දත්ත හැසිරවීමට තිබුණේ බිට් 8ක (8-bit) කුඩා ඉඩකි. එහි උපරිම අගය 255 (0xFF) විය. නමුත් අපේ x64 මහා බූරුවාට බිට් 64ක විශාල ඉඩක් ඇත! බිට් 64ක් යනු දශම ක්රමයෙන් (Quintrillion 18ක්!) දක්වා වූ අතිවිශාල අංකයකි. මෙයින් අදහස් වන්නේ බූරුවාට එක වරකට ඉතා විශාල දත්ත ප්රමාණයක් තම කුර මත තබාගෙන වැඩ කල හැකි බවයි.
දත්ත වල පළල (Data Width) බිට් 64ක් වීම මෙන්ම, මතක ලිපින (Memory Addresses) ද බිට් 64ක් වේ. මෙයින් අදහස් කරන්නේ බූරුවාට කොළඹ නැව් තොටුපලේ (RAM) ඇති බිලියන සංඛ්යාත පෙට්ටි කෙලින්ම ඇමතීමේ හැකියාව ලැබෙන බවයි. පැරණි 8-bit බූරුවාට තිබූ ලිපින 65,536 සීමාව (16-bit address bus) මෙන් නොව, මේ මහා බූරුවාට හෙක්ටයාර මිලියන ගණනක තොටුපල භූමියක් පාලනය කල හැක!
මයික්රෝප්රොසෙසරය තුල ඉතාමත් වේගයෙන් වැඩ කිරීමට භාවිතා කරන කුඩා ලාච්චු "රෙජිස්ටර (Registers)" ලෙස හඳුන්වයි. බූරුවාට මතක තොටුපල (RAM) වෙත ගොස් බඩු රැගෙන ඒමට සාපේක්ෂව, තමාගේම බංකුව මත ඇති ලාච්චු වලින් බඩු ගැනීම අතිශය වේගවත්ය. x64 බූරුවාට ප්රධාන වැඩ කරන්නන් (පොදු කාර්ය රෙජිස්ටර) 16 දෙනෙකු ඇත:
මතක තබා ගන්න 56 වන වාදය: රජතුමා ඔහුට අණ දෙන්නේද, වඳුරා සැමවිටම එය අනුගමනය කරයි. x64 මහා බූරුවා තවමත් තම මුතුන් මිත්තන්ගේ (8-bit, 16-bit, 32-bit) සාක්කු තම ශරීරය තුල සඟවාගෙන සිටී. එනම්, සෑම 64-bit රෙජිස්ටරයක්ම කුඩා කොටස් වලට බෙදා භාවිත කල හැක.
උදාහරණයක් ලෙස RAX රෙජිස්ටරය සලකමු:
AX හි ඉහළ බිට් 8 (8-bit) වේ.AX හි පහළ බිට් 8 (8-bit) වේ.මෙය පහත සටහනෙන් වටහා ගන්න:
|---------------------------------------------------------------|
| RAX | (64-bit)
|-------------------------------|-------------------------------|
| EAX | (32-bit)
|---------------|---------------|
| AX | (16-bit)
|-------|-------|
| AH | AL | (8-bit)
රෙජිස්ටර 16 න් මුල් රෙජිස්ටර 8 ටම (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP) මේ ආකාරයෙන්ම 32-bit, 16-bit සහ 8-bit සාක්කු ඇත (උදා: RSI -> ESI -> SI -> SIL).
R8 සිට R15 දක්වා ඇති රෙජිස්ටර සඳහා සාක්කු නම් කරන්නේ මෙසේය:
32-Bit ශුන්ය ව්යාප්ති රීතිය (The 32-bit Zero-Extension Rule):
x64 බූරුවා සතුව ඉතා සුවිශේෂී ගතිලක්ෂණයක් ඇත. ඔබ 32-bit රෙජිස්ටරයක් (උදාහරණයක් ලෙස EAX) වෙත යම් අගයක් ලියූ විට, බූරුවා ස්වයංක්රීයව එම රෙජිස්ටරයේ ඉහළ බිට් 32ම බිංදුව (0) බවට පත් කරයි!
උදාහරණයක් ලෙස:
mov eax, 0x12345678 ක්රියාත්මක කල පසු, RAX හි සම්පූර්ණ අගය වන්නේ 0x0000000012345678 මිස කලින් තිබූ ඉහළ අගයන් නොවේ.
නමුත් 16-bit හෝ 8-bit සාක්කු වලට (AX, AH, AL) ලියන විට ඉතිරි බිට් වලට හානියක් නොවේ! මෙය අමතක වුවහොත් 1 වන වාදය උල්ලංඝනය වී මොළයට හානි සිදුවනු නොඅනුමානය!
පැරණි 8-bit පරිගණක වල තිබූ "ප්රෝග්රෑම් කවුන්ටරය (Program Counter - PC)" x64 ව්යුහය තුල හඳුන්වන්නේ RIP (Instruction Pointer Register) ලෙසයි.
මූ තමයි බූරුවා මෙහෙයවන කසය! බූරුවා ඊළඟට කෑ යුතු බයිනරි ආහාරය (Instruction) තිබෙන මතක ලිපිනය (Memory Address) සැමවිටම අඩංගු වන්නේ RIP තුලය.
6 වන වාදය: මයික්රෝප්රොසෙසරය ක්රමාණුකූලත්වයට හුරුවී ඇත.
බූරුවා වැඩ කරන්නේ ඉතා පිළිවෙලටය. ඌ එක් උපදෙසක් කා (Execute කර) අවසන් වූ සැණින්, RIP රෙජිස්ටරය ස්වයංක්රීයව ඊළඟ උපදෙස ඇති ලිපිනය වෙත යොමු වේ.
ඔබට බූරුවාගේ මෙම ක්රමාණුකූල ගමන වෙනස් කිරීමට අවශ්ය නම් (උදාහරණයක් ලෙස යම් කොන්දේසියක් මත වෙනත් තැනකට පැනීමට), ඔබට RIP වෙත කෙලින්ම අගයන් ලිවිය නොහැක (උදාහරණයක් ලෙස mov rip, 100 වැනි දේ ලිවීම සපුරා තහනම්ය!).
ඒ වෙනුවට ඔබ JMP (Jump), CALL හෝ කොන්දේසි සහිත පැනීම් (Conditional Jumps - JE, JNE ආදී) භාවිතා කල යුතුය. එවිට බූරුවා තම කසය වන RIP ඔබ පවසන නව ලිපිනයට හරවා ගනී.
මයික්රෝප්රොසෙසර බූරුවා සමඟ සෘජුවම ගනුදෙනු කිරීමට අපට උපකාර වන මෙවලම වන්නේ ඇසෙම්බ්ලර් (Assembler) එකයි. වෙළඳපොලේ MASM, NASM වැනි නොයෙකුත් ඇසෙම්බ්ලර් වර්ග තිබුණද, අප අපගේ ගමන සඳහා තෝරා ගන්නේ Flat Assembler (FASM) ය.
ඇයි FASM? අනෙකුත් ඇසෙම්බ්ලර් හරියට වැඩකට නැති නීති පනවන, වටයෙන් යන රජයේ නිලධාරීන් වැනිය. MASM හෝ NASM වලදී ඔබ මුලින්ම කේතය ලියා, එය ඇසෙම්බ්ල් කර වස්තු ගොනුවක් (Object File - .obj) සාදා, ඉන්පසු එය ලින්කර් (Linker) එකක් නැමති තවත් වෙනම ප්රෝග්රෑම් එකක් හරහා රන් කරවා අවසාන ක්රියාත්මක කල හැකි ගොනුවක් (.exe) සාදාගත යුතුය. මෙය හරියට කොළඹ වරායට එන බඩු ලොරි, තීරුබදු නිලධාරීන් කිහිපදෙනෙකු ලවා පරීක්ෂා කරවා ප්රමාද කරනවා වැනි කරදරකාරී ක්රියාවකි!
FASM යනු "Flat" ඇසෙම්බ්ලර් එකකි. මෙයින් අදහස් කරන්නේ ඌට වෙනම ලින්කර්ලා අවශ්ය නොවන බවයි! FASM විසින් ඔබ ලියන ඇසෙම්බ්ලි පෙළ කෙලින්ම කියවා, තත්පරයෙන් සියයෙන් පංගුවකින් වින්ඩෝස් වලට සෘජුවම ක්රියාත්මක කල හැකි PE64 (Portable Executable 64-bit) ගොනුවක් නිර්මාණය කර දෙයි. ඌ ඉතා සරල, කාර්යක්ෂම, කිසිදු බාහිර සැරසිලි නැති සූපවේදියෙකි. බූරුවාට අවශ්ය ආහාරය (Binary code) බඳුනටම කෙලින්ම බෙදා දීමට ඌ දනී.
අප බූරුවා සමඟ සන්නිවේදනය කිරීමේදී, අප ලියන සියලුම දේ බූරුවාට ක්රියාත්මක කල නොහැක. අපට අපේ මොළය ආරක්ෂා කර ගැනීම සඳහා කේතය අස්සේ විවිධ සටහන් (Comments) තබා ගැනීමට සිදුවේ.
බූරුවාට දෙන විධානයන් සහ අපේ සටහන් එකිනෙකින් වෙන් කරන ආරක්ෂක වැට වන්නේ සෙමිකෝලන් (😉 සලකුණයි.
mov rax, 100 ; RAX රෙජිස්ටරය නැමති ලාච්චුවට 100 දමන්න.
සෙමිකෝලන් එකක් යෙදූ සැනින්, FASM විසින් එම පේළියේ ඉතිරි කොටස බූරුවාට නොපෙනෙන සේ වසා දමයි. බූරුවා දකින්නේ සෙමිකෝලන් එකට වම් පැත්තේ ඇති mov rax, 100 යන කොටස පමණි. දකුණු පැත්තේ ඔබ සිංහලෙන් හෝ ඉංග්රීසියෙන් ලියා ඇති "RAX එකට 100 දමන්න" යන්න බූරුවාට අදාළ නැත.
ඔබ සෙමිකෝලන් එකක් දැමීමට අමතක කළහොත්, බූරුවා එම සිංහල හෝ ඉංග්රීසි වචනද තමාගේ කෑමක් (විධානයක්) යැයි සිතා සැපීමට උත්සාහ කරයි. එහි ප්රතිඵලය වන්නේ "Invalid Instruction" කියා FASM ඇසෙම්බ්ලර් එක කෑගසා වැඩ වර්ජනය කිරීමයි!
FASM හි කේත ලිවීමේදී පිළිපැදිය යුතු මූලික නීති කිහිපයක් ඇත.
සැමවිටම ඇසෙම්බ්ලි විධානයක් ලියනු ලබන්නේ පහත ආකාරයටය:
විධානය ගමනාන්තය, ප්රභවය
(Instruction Destination, Source)
එනම්, දත්ත ගමන් කරන්නේ දකුණේ සිට වමටයි!
mov rbx, 5 ; RBX (ගමනාන්තය) වෙත 5 (ප්රභවය) ඇතුල් කරන්න. rbx = 5
මෙතැනදී විධානය (mov) සහ පළමු රෙජිස්ටරය (rbx) අතර අවම වශයෙන් එක් හිස්තැනක් (Space) හෝ ටැබ් (Tab) එකක් තිබිය යුතුය. එමෙන්ම ගමනාන්තය සහ ප්රභවය වෙන් කිරීමට කොමාවක් (,) අනිවාර්යයෙන්ම යෙදිය යුතුය. කොමාව අමතක කිරීමෙන් බූරුවා ව්යාකූල වී 178 වන වාදය (කිසිවෙකු පරිපූර්ණ නැත!) සිහිපත් කරවනු ඇත.
[...] (Brackets vs Immediate values) ඇසෙම්බ්ලි ලියන බොහෝ නවකයින්ට මොළයට හානි සිදුවන්නේ (1 වන වාදය උල්ලංඝනය වන්නේ) මෙම වරහන් නීතිය නිසි ලෙස වටහා නොගැනීම නිසාය.
හොඳින් සිතන්න, RBX රෙජිස්ටරය තුල 0x2000 නැමති අගය ඇතැයි සිතමු.
වරහන් රහිතව (No Brackets):
mov rax, rbx
මෙහි තේරුම: RBX නැමති ට්රක් රථයේ ලියා ඇති අංකය (0x2000) කෙලින්ම RAX නැමති ට්රක් රථය වෙත පිටපත් කරන්න. දැන් RAX සතුවද 0x2000 අගය ඇත.
වරහන් සහිතව (With Brackets):
mov rax, [rbx]
මෙහි තේරුම සම්පූර්ණයෙන්ම වෙනස්ය! [rbx] යනු "කොළඹ ජැටියේ (RAM) rbx රෙජිස්ටරය තුල ඇති අංකයට (0x2000 ලිපිනයට) අදාළ තොටුපල පෙට්ටිය" යන්නයි.
එනම්, බූරුවා RBX හි ඇති අගය (0x2000) කියවා, වහාම කොළඹ ජැටියේ 0x2000 ලිපිනය ඇති තොටුපල පෙට්ටිය (Memory location) වෙත දිව ගොස්, එම පෙට්ටිය ඇතුළේ ඇති සැබෑ බඩු (Cargo/Data) මොනවාදැයි බලා, එම බඩු ටික රැගෙන විත් RAX රෙජිස්ටරය තුල තැන්පත් කරයි!
කොළඹ ජැටියේ රීතිය (The Jetty Rule):
වරහන් නැතිව ලියූ විට ඔබ ගනුදෙනු කරන්නේ රෙජිස්ටරයේ ඇති අංකය සමඟ පමණි.
වරහන් [...] යෙදූ විට, ඔබ බූරුවාට අණ කරන්නේ එම අංකය ලිපිනයක් (Address) ලෙස සලකා, කොළඹ ජැටියේ (RAM) අදාළ ලිපිනයට ගොස් පෙට්ටිය ඇර එහි ඇති දත්ත රැගෙන එන ලෙසයි!
අප ලියන කේතය වින්ඩෝස් රාජසභාව තුල ක්රියාත්මක වීමට නම්, වින්ඩෝස් රජු බලාපොරොත්තු වන ව්යුහයකට අපේ කේතය සකස් කල යුතුය. පහත දැක්වෙන්නේ FASM මගින් වින්ඩෝස් කොන්සෝලයක් සඳහා ලියන සරලම වැඩසටහනක ව්යුහයයි:
; ==========================================================
; වින්ඩෝස් 64-bit සඳහා සරලම FASM වැඩසටහන
; ==========================================================
format PE64 console ; අපි සාදන්නේ වින්ඩෝස් 64-bit කොන්සෝල් (Console) වැඩසටහනක් බව FASM ට කියයි
entry start ; ප්රෝග්රෑම් එක පටන් ගන්නා තැන "start" ලේබලය ලෙස සලකුණු කරයි
section '.code' code readable executable
start:
; අපේ බූරුවාට දෙන උපදෙස් මෙතැන ලියමු
mov ecx, 0 ; ExitCode = 0 (පළමු පරාමිතිය ECX රෙජිස්ටරයට)
call [ExitProcess] ; වින්ඩෝස් රාජ සභා නිලධාරියා අමතා ප්රෝග්රෑම් එක නවත්වන්න
section '.idata' import data readable writeable
library kernel32, 'kernel32.dll'
import kernel32,\
ExitProcess, 'ExitProcess'
format PE64 console: මෙයින් FASM සූපවේදියාට පවසන්නේ අපට අවශ්ය ලින්කර් කරදරයක් නැති වින්ඩෝස් PE (Portable Executable) 64-bit Console ගොනුවක් බවයි.entry start: බූරුවාට වැඩේ පටන් ගැනීමට සිතියමේ ලකුණු කර ඇති ආරම්භක කණුව (Entry Point) "start" ලෙස නම් කරයි.section '.code' ...: බූරුවාට කෑමට දෙන බයිනරි ආහාර (instructions/code) අඩංගු වන සිතියමේ කොටසයි. මෙයට කියවීමේ (readable) සහ ක්රියාත්මක කිරීමේ (executable) අවසර ඇත.section '.idata' ...: අපගේ ප්රෝග්රෑම් එකට වින්ඩෝස් රාජසභාවෙන් (Windows OS) ලබාගන්නා සහය සේවාවන් (APIs) ආනයනය (Import) කරගන්නා තොටුපලයි. මෙහිදී අප kernel32.dll නැමති නැවෙන් ExitProcess නැමති රාජකාරි නිලධාරියාව ආනයනය කර ගෙන ඇත.call [ExitProcess]: බූරුවාට රාජකාරිය අවසන් කර ගෙදර යාමට වින්ඩෝස් නිලධාරියා කැඳවයි.මෙම ව්යුහය ඔබේ කේතයේ පදනම වේ. මෙය නිරවුල්ව තබා ගැනීමෙන් 1 වන වාදය උල්ලංඝනය වීම වළක්වා ගත හැක!
අපේ මහා බූරුවා වැඩ කරන ස්ථානයේ ඇති ප්රධානතම මෙවලම වන්නේ MOV (Move) විධානයයි. ඌට මෙම විධානය හරියට සවලක් (Shovel) වැනිය. ඌ කරන්නේ මෙම සවලෙන් එක තැනක ඇති දත්ත ගෙන තවත් තැනක තැන්පත් කිරීමයි.
නමුත් මතක තබා ගන්න, බූරුවාට සවල් හැසිරවීමේදී වින්ඩෝස් රාජසභාවේ දැඩි නීති රීති මාලාවකට යටත් වීමට සිදුවේ. ඌට ඕනෑම විදියකට දත්ත හෙලවිය නොහැක.
රෙජිස්ටරයක සිට තවත් රෙජිස්ටරයකට (Register to Register):
mov rax, rbx ; RBX ලාච්චුවේ ඇති දේ RAX ලාච්චුවට දමන්න
මෙය ඉතා වේගවත්ය. බූරුවා තම බංකුව මතම ඇති ලාච්චු දෙකක් අතර දත්ත මාරු කරයි.
නියත අගයක් රෙජිස්ටරයකට (Immediate to Register):
mov rcx, 100 ; RCX ලාච්චුවට කෙලින්ම 100 අගය ඇතුල් කරන්න
මතකයේ සිට රෙජිස්ටරයකට (Memory to Register):
mov rdx, [rax] ; RAX හි ඇති ලිපිනයට අදාළ මතක පෙට්ටියෙන් (RAM) බඩු ගෙන RDX ලාච්චුවට දමන්න
රෙජිස්ටරයක සිට මතකයට (Register to Memory):
mov [rsi], rdi ; RDI ලාච්චුවේ ඇති බඩු RSI හි ඇති ලිපිනයට අදාළ මතක පෙට්ටියට (RAM) දමන්න
නියත අගයක් මතකයට (Immediate to Memory):
mov dword [rax], 500 ; RAX හි ඇති ලිපිනයට අදාළ මතක පෙට්ටියට 500 අගය දමන්න
පෙට්ටියේ ප්රමාණය හැඟවීමේ රීතිය (Size Directives Rule):
ඔබ mov [rax], 500 ලෙස පමණක් ලියුවහොත් බූරුවා වහාම වැඩ නතර කර ව්යාකූල වේ! ඌ අසන්නේ: "මහා ස්වාමීනි, මම මෙම 500 අගය දැමිය යුත්තේ කොතරම් විශාල පෙට්ටියකටද? බයිට් 1ක (byte) පෙට්ටියකටද? බයිට් 2ක (word) පෙට්ටියකටද? බයිට් 4ක (dword) පෙට්ටියකටද? නැතහොත් බයිට් 8ක (qword) පෙට්ටියකටද?"
ඌට ප්රමාණය නොකිව්වොත් ඌ එම බර පෙට්ටිය තම කුර මත හෙළාගෙන 1 වන වාදය උල්ලංඝනය කරගනී. එබැවින් අප සැමවිටම ප්රමාණය පැහැදිලි කල යුතුය:
byte [rax] (බිට් 8)word [rax] (බිට් 16)dword [rax] (බිට් 32)qword [rax] (බිට් 64)මතකයේ සිට මතකයට සෘජුවම දත්ත මාරු කිරීම සපුරා තහනම්ය!
mov [rax], [rbx] ; !!! මෙය වැරදියි (Syntax Error) !!!
බූරුවාට එකවර මතකයේ තැන් දෙකක් සමඟ ගනුදෙනු කල නොහැක. ඌට ඇත්තේ සවල් හැසිරවිය හැකි දෑත් දෙකක් පමණක් වන අතර ඌට එකවර ජැටියේ (RAM) පෙට්ටි දෙකක් ඇර බඩු මාරු කල නොහැක.
එබැවින් අප මුලින්ම ජැටියේ එක් පෙට්ටියක ඇති බඩු අපේ ට්රක් රථයකට (රෙජිස්ටරයකට) පටවාගෙන, ඉන්පසු එම ට්රක් රථයේ සිට අනෙක් පෙට්ටියට දැමිය යුතුය:
mov rcx, [rbx] ; පළමුව RBX ලිපිනයේ ඇති දත්ත RCX ට්රක් රථයට පටවන්න
mov [rax], rcx ; දෙවනුව RCX ට්රක් රථයේ ඇති දත්ත RAX ලිපිනයට බාන්න
දැන් අප අපගේ සුප්රසිද්ධ කොළඹ නැව් තොටුපල (Colombo Jetty) උපමාව වෙත හැරෙමු.
පරිගණකයේ ඇති ප්රධාන මතකය (RAM - Random Access Memory) යනු කොළඹ නැව් තොටුපලයි. එහි එක පෙළට සකස් කරන ලද, අනුක්රමික අංක සහිත බිලියන ගණනක තොටුපල පෙට්ටි (Memory Locations / Addresses) ඇත.
සෑම පෙට්ටියකටම අනන්ය වූ ලිපිනයක් (Address) ඇත. මෙම ලිපිනය 0 සිට දක්වා වූ 64-bit අංකයකි.
අපේ රෙජිස්ටර (RAX, RBX ආදිය) යනු මෙම ජැටිය පුරා ධාවනය වන කුඩා දත්ත ප්රවාහන ට්රක් රථ වේ.
db (Define Byte): බයිට් 1ක (බිට් 8) කුඩා පෙට්ටියක්.dw (Define Word): බයිට් 2ක (බිට් 16) පෙට්ටියක්.dd (Define Doubleword): බයිට් 4ක (බිට් 32) පෙට්ටියක්.dq (Define Quadword): බයිට් 8ක (බිට් 64) විශාල පෙට්ටියක්.FASM හි දත්ත අංශය (Data Section) තුල අපට මේවා මෙසේ ප්රකාශ කල හැක:
section '.data' data readable writeable
sugar_box db 25 ; බයිට් 1ක සීනි පෙට්ටියක් (අගය 25)
rice_bag dw 1000 ; බයිට් 2ක සහල් මල්ලක් (අගය 1000)
gold_bar dd 500000 ; බයිට් 4ක රන් බාර් එකක් (අගය 500000)
ship_cargo dq 0x11223344556677 ; බයිට් 8ක විශාල නැව් බඩු පෙට්ටියක්
මෙම පෙට්ටි කියවීමට නම් බූරුවාට වරහන් [...] ආධාරයෙන් ජැටිය වෙත යාමට නියෝග කල යුතුය:
mov al, [sugar_box] ; sugar_box පෙට්ටියෙන් බයිට් 1ක දත්ත AL සාක්කුවට ගන්න
mov rbx, [ship_cargo] ; ship_cargo පෙට්ටියෙන් බයිට් 8ක දත්ත RBX ලාච්චුවට ගන්න
වින්ඩෝස් 64-bit රාජධානියේ (PE64) ඉතා වැදගත් ආරක්ෂක නීතියක් ක්රියාත්මක වේ. එය ASLR (Address Space Layout Randomization) ලෙස හඳුන්වයි.
මුහුදු කොල්ලකරුවන් (Hackers) අපගේ මතක තොටුපලට කඩා වැදී රන් බාර් සොරකම් කිරීම වැළැක්වීම සඳහා, වින්ඩෝස් රජු විසින් අපගේ ප්රෝග්රෑම් එක කොළඹ ජැටියේ ක්රියාත්මක වන සෑම වාරයකදීම ඌට කැමති අහඹු ස්ථානයක (Random base address) තැන්පත් කරයි. අද 0x1000 ලිපිනයේ තිබූ ප්රෝග්රෑම් එක හෙට 0x9000 ලිපිනයට මාරු විය හැක!
මෙවැනි තත්වයක් යටතේ, අපේ දත්ත පෙට්ටි තිබෙන නිශ්චිත ස්ථානය (Absolute Address) කල්තියා දැනගැනීමට ක්රමයක් නැත. බූරුවා ව්යාකූල වනු ඇත!
මේ සඳහා යොදාගන්නා අපූරු විසඳුම වන්නේ RIP-සම්බන්ධිත ලිපින ක්රමය (RIP-Relative Addressing) ය.
බූරුවාට තම කසය වන RIP (Instruction Pointer) සැමවිටම තමා ලඟ ඇති බැවින්, තමා වර්තමානයේ සිටින ස්ථානය ඌ දනී. ඌ තම දත්ත පෙට්ටි සොයා ගන්නේ තමා සිටින තැන සිට ඇති දුර (Displacement) ගණනය කිරීමෙනි.
උදාහරණයක් ලෙස:
"මම දැන් සිටින තැන (RIP) සිට හරියටම පියවර 500ක් ඉදිරියෙන් sugar_box පෙට්ටිය ඇත."
ප්රෝග්රෑම් එක ජැටියේ කොතැනක තැන්පත් කලද, කේතය (Instructions) සහ දත්ත (Data) අතර ඇති පරතරය වෙනස් නොවේ. එබැවින් බූරුවාට තම RIP කසය පදනම් කරගෙන පහසුවෙන්ම දත්ත සොයාගත හැක.
FASM හිදී අපට මෙම සංකීර්ණ ගණිතය කිරීමට අවශ්ය නැත. FASM විසින් මෙය ස්වයංක්රීයව සිදුකරයි:
mov eax, [sugar_box] ; FASM විසින් මෙය පසුබිමින් [rip + sugar_box] ලෙස සකසයි
මෙමගින් බූරුවා කිසිදු ව්යාකූලත්වයකින් තොරව, වින්ඩෝස් ජැටියේ ඕනෑම තැනක සිට තම රාජකාරිය සාර්ථකව නිම කරයි!
ආයුබෝවන් මිතුරනි! ඔන්න අපි පැමිණ සිටින්නේ අපේ පොතේ ඉතාමත්ම රසවත් සහ වැදගත් පරිච්ඡේදයකටයි. මතකද අපේ 2 වන වාදය: x64 මයික්රෝප්රොසෙසරය බූරුවෙකි (The x64 CPU is a Donkey)? මූට තනිවම හිතන්න බැහැ. හැබැයි මූ සංඛ්යා හපන්න උපන් හපනෙක්! (Number Cruncher). අපේ මේ සිලිකන් බූරුවාට එකතු කරන්න, අඩු කරන්න, ගුණ කරන්න සහ බෙදන්න පුළුවන්. හැබැයි ඌ ඒ හැමදේම කරන්නේ ඔබ ඌට දෙන නියෝග වලට අකුරටම අනුව විතරයි.
මේ පරිච්ඡේදයේදී අපි බූරුවාට ගණිතය උගන්වන්නේ කොහොමද, බෙදීමේදී තියෙන භයානක උගුල් වලින් බේරෙන්නේ කොහොමද, සහ ඌව එකම වටේ රවුම් ගස්වන්නේ (Loops) කොහොමද කියලා ඉතාමත් විස්තරාත්මකව බලමු. 1 වන වාදය සැමවිටම මතක තබාගන්න: මොළයට හානි සිදුකර ගැනීමෙන් වලකින්න!
x64 මහා බූරුවාට තම වැඩ බංකුවේ ඇති රෙජිස්ටර (Registers) සහ මතක ලිපින (Memory Addresses) වල ඇති අංක සමඟ සෙල්ලම් කිරීමට ප්රධාන ආයුධ කිහිපයක් තිබේ.
මේවා ඉතාමත් සරලයි. හරියට බූරුවාට කෑම බාල්දියක් දෙනවා වගෙයි.
ADD විධානය (Addition):
add destination, source
මෙහි ක්රියාකාරිත්වය වන්නේ destination = destination + source යන්නයි.
උදාහරණයක් ලෙස:
mov rax, 15 ; RAX ලාච්චුවට 15 දමන්න
add rax, 10 ; RAX = RAX + 10 (දැන් RAX හි 25 ඇත)
SUB විධානය (Subtraction):
sub destination, source
මෙහි ක්රියාකාරිත්වය වන්නේ destination = destination - source යන්නයි.
උදාහරණයක් ලෙස:
mov rbx, 50 ; RBX ලාච්චුවට 50 දමන්න
sub rbx, 20 ; RBX = RBX - 20 (දැන් RBX හි 30 ඇත)
බූරුවාගේ දෑත් දෙකේ රීතිය:
අපේ බූරුවාට එකවර වැඩ කල හැක්කේ එක මතක ලිපිනයක් (Memory Address) සමඟ පමණි. එනම්, ඔබට add [mem1], [mem2] ලෙස මතක ලිපින දෙකක් කෙලින්ම එකතු කළ නොහැක! ඌට මතකයෙන් මතකයට කෙලින්ම බඩු ඇදීමට ශක්තියක් නැත. ඔබ මුලින්ම එක් මතක අගයක් රෙජිස්ටරයකට පටවාගෙන, පසුව එය අනෙකට එකතු කල යුතුය.
ඔබට කිසියම් අගයක් 1කින් පමණක් වැඩි කිරීමට හෝ අඩු කිරීමට අවශ්ය වූ විට add rax, 1 හෝ sub rax, 1 ලිවීම මොළයට කරදරයකි (1 වන වාදය). ඒ සඳහා කෙටි ක්රම දෙකක් තිබේ:
inc rcx ; RCX = RCX + 1
dec rdx ; RDX = RDX - 1
සිලිකන් කොඩිවල රහස (RFLAGS Quirk):
INC සහ DEC විධාන මඟින් Carry Flag (CF) (කැරි කොඩිය) වෙනස් නොකරයි! මෙය එකතු කිරීමේ සහ අඩු කිරීමේ සාමාන්ය රීති වලට වඩා වෙනස්ය. බූරුවාගේ පැරණි මුතුන් මිත්තන්ගේ සිට පැමිණි මේ ක්රමය ලූප වලදී ඉතා ප්රයෝජනවත් වේ.
ගුණ කිරීමේදී x64 ප්රොසෙසරයට ක්රම කිහිපයක් ඇත. අප මෙහිදී භාවිතා කරන්නේ සයින්ඩ් (Signed - සලකුණක් සහිත, එනම් ධන/සෘණ) ගුණ කිරීම සඳහා වන IMUL විධානයයි.
FASM හි IMUL ආකාර තුනකින් ලිවිය හැක:
එක් පරාමිතියක් සහිත ආකාරය (Single-operand):
imul rbx ; RAX = RAX * RBX
මෙහිදී බූරුවා සිතන්නේ ඔබ ගුණ කරන්නේ RAX සමඟ කියාය. ගුණ කර ලැබෙන අවසාන අගය බිට් 64 සීමාව ඉක්මවා යා හැකි බැවින්, බූරුවා ප්රතිඵලය බිට් 128ක රෙජිස්ටර යුගලයක් වන RDX:RAX හි ගබඩා කරයි. (පහළ බිට් 64 RAX හිද, ඉහළ බිට් 64 RDX හිද ඇත).
පරාමිතීන් දෙකක් සහිත ආකාරය (Two-operand):
imul rax, rbx ; RAX = RAX * RBX
මෙහිදී ගුණ කර ලැබෙන අගය කෙලින්ම පළමු රෙජිස්ටරයට (RAX) ලියවේ. ඉහළ කොටස ඉවත දමනු ලැබේ.
පරාමිතීන් තුනක් සහිත ආකාරය (Three-operand):
imul rax, rbx, 5 ; RAX = RBX * 5
මෙහිදී RBX හි ඇති අගය 5න් ගුණ කර ප්රතිඵලය RAX තුල තැන්පත් කරයි.
මෙන්න අපේ බූරුවා බොහෝ දෙනෙකුට මොළයේ හානි (Brain Damage) සිදුකරන තැන! බෙදීම සඳහා භාවිතා කරන්නේ IDIV (Signed Division) විධානයයි. බූරුවාට බෙදීමක් කිරීමට නම් ඉතා දැඩි නීති රීති මාලාවක් අනුගමනය කල යුතුය.
බූරුවාගේ බෙදීමේ නීතිය:
ඔබ 64-bit අගයකින් බෙදන විට, බෙදීමට ලක්වන සංඛ්යාව (Dividend) බිට් 128ක් විය යුතුය! එම බිට් 128 තැන්පත් විය යුත්තේ RDX:RAX යන රෙජිස්ටර දෙක ඒකාබද්ධ කිරීමෙනි.
|-----------------------------------------------|
| RDX (ඉහළ බිට් 64) | RAX (පහළ බිට් 64) | = 128-bit Dividend
|-----------------------------------------------|
ඔබට බෙදීමට අවශ්ය වන්නේ RAX තුල ඇති 64-bit අගයක් පමණක් නම්, ඔබ බෙදීමට පෙර අනිවාර්යයෙන්ම RDX රෙජිස්ටරය නිවැරදිව සකස් කල යුතුය.
RDX හි ඇති කුණු දත්ත ඉවත් කිරීමට අවශ්ය නම්, xor rdx, rdx මඟින් RDX ශුන්ය කරන්න.RAX හි ඇති අගයේ සලකුණ (Sign bit - ධන ද සෘණ ද යන්න) RDX දක්වා දිගු කල යුතුය. ඒ සඳහා CQO (Convert Quadword to Octoword) නැමැති විශේෂ විධානය භාවිතා කරයි.බූරුවා බෙදා අවසන් වූ පසු ප්රතිඵල දෙකක් ලබා දෙයි:
RAX රෙජිස්ටරයට ලියවේ.RDX රෙජිස්ටරයට ලියවේ.වැඩ කරන ආකාරය පෙන්වන සම්පූර්ණ උදාහරණයක්:
mov rax, 100 ; බෙදීමට අවශ්ය අගය (100) RAX ට දමන්න
cqo ; RAX හි සලකුණ RDX දක්වා දිගු කරන්න (RDX:RAX = 100)
mov rbx, 7 ; බෙදන සංඛ්යාව (7) RBX ට දමන්න
idiv rbx ; RDX:RAX අගය RBX න් බෙදන්න (100 / 7)
; දැන්:
; RAX = 14 (බෙදුම් ඵලය - Quotient)
; RDX = 2 (ඉතිරිය - Remainder)
බෙදීමට පෙර RDX සකස් නොකළහොත්, RDX හි ඇති කුණු දත්ත නිසා බෙදුම් අගය අතිවිශාල වී බූරුවා කලබල වේ. එවිට Windows විසින් Integer Division by Zero හෝ Overflow Exception එකක් ලබාදී ඔබේ ප්රෝග්රෑම් එක බසයෙන් එලියට ඇද දමනු ඇත!
මතකද අපේ 6 වන වාදය: මයික්රෝප්රොසෙසරය ක්රමාණුකූලත්වයට හුරුවී ඇත (The CPU is addicted to order)? ඌ සැමවිටම ඊළඟ උපදෙස වෙත ක්රමාණුකූලව ගමන් කරයි. බූරුවාට වෙනත් පාරක යන්නට කිව යුත්තේ JUMP විධානයක් මගිනි.
අපට පැනීමට අවශ්ය ස්ථාන සලකුණු කිරීමට ලේබල් යොදා ගනී. FASM හි ලේබලයක් ලියන්නේ නම ලියා අගට කෝලන් : ලකුණ තැබීමෙනි.
my_destination: ; මෙය ලේබලයක්
FASM හි දේශීය ලේබල් (Local Labels):
ඔබ ප්රෝග්රෑම් එක පුරාම loop_start, exit වැනි නම් නැවත නැවත ලියන විට නම් එකිනෙක පැටලී මොළයට හානි විය හැක. FASM හි මෙයට කදිම විසඳුමක් ඇත. ලේබලයකට මුලින් තිතක් (.) තැබූ විට එය දේශීය ලේබලයක් (Local Label) බවට පත්වේ. එය වලංගු වන්නේ ඊට ඉහළින්ම ඇති සාමාන්ය ලේබලයට පමණි.
start:
.loop: ; start.loop ලෙස ක්රියා කරයි
dec rcx
jnz .loop
another_func:
.loop: ; another_func.loop ලෙස ක්රියා කරයි. කිසිම ගැටලුවක් නැත!
inc rbx
jmp .loop
කිසිදු ප්රශ්නයක්, කොන්දේසියක් නැතිව කෙලින්ම ලේබලයකට පැනීමට මෙය භාවිතා කරයි.
jmp my_destination
මෙහිදී බූරුවා පනින්නේ ඊට පෙර සිදුවූ ගණනය කිරීමකදී ඔසවන ලද "සිලිකන් කොඩි" (RFLAGS) දෙස බලා ඒවා සත්ය නම් පමණි. (කොඩි ගැන 5 වන පරිච්ඡේදයේදී වඩාත් විස්තරාත්මකව කතා කරමු).
නිතර භාවිතා වන කොන්දේසි සහිත පැනීම් කිහිපයක් මෙසේය:
පරිගණකයක ඇති ප්රධානම ප්රයෝජනයක් වන්නේ එකම වැඩේ නැවත නැවත ඉතා වේගයෙන් කිරීමයි. බූරුවා රවුමේ දිවවීමට (Loop) අප බහුලවම භාවිතා කරන්නේ RCX රෙජිස්ටරය ගණකය (Counter) ලෙස යොදාගැනීමයි.
අප සාමාන්යයෙන් කරන්නේ:
RCX ලාච්චුවට ලූපය දිවිය යුතු වාර ගණන දැමීම.dec rcx මඟින් ගණකය 1කින් අඩු කිරීම.jnz (Jump if Not Zero) මඟින් RCX තවමත් 0 වී නැත්නම් නැවත ලූපයේ ආරම්භයට පැනීම.1 සිට 10 දක්වා වූ සංඛ්යා ලූපයකින් එකතු කරන ආකාරය (1+2+3+...+10 = 55):
xor rax, rax ; RAX = 0 (එකතුව තබා ගන්නා ලාච්චුව)
mov rcx, 10 ; RCX = 10 (ගණකය 10 සිට ආරම්භ වේ)
.sum_loop:
add rax, rcx ; RAX = RAX + RCX (10, 9, 8... එකතු වේ)
dec rcx ; RCX = RCX - 1
jnz .sum_loop ; RCX තවමත් 0 නොවේ නම් .sum_loop වෙත ආපසු පනින්න
; ලූපයෙන් ඉවත් වන විට RAX හි 55 (Hex 0x37) අගය ඇත!
දැක්කා නේද? අපේ බූරුවා ඉතාමත් සතුටින් රවුමේ දුවමින්, තත්පරයෙන් බිලියනයකින් පංගුවකදී ගණනය කිරීම් අවසන් කරයි.
ඊළඟ පරිච්ඡේදයේදී අපි බූරුවා මේ තීරණ ගැනීමට බලන "සිලිකන් කොඩි" (Flags) සහ ඒවා පාලනය කරන්නේ කෙසේද කියා බලමු. සෙමෙන් ඉදිරියට යමු, මොළය පරිස්සම් කරගනිමින්!
පෙර පරිච්ඡේදයේදී අපි කොන්දේසි සහිතව පැනීම (Conditional Jumps) ගැන කෙටියෙන් ඉගෙන ගත්තෙමු. නමුත් බූරුවා මේ තීරණ ගන්නේ කෙසේද? ඌට ඇස් දෙකක් තිබේද? නැත! ඌට ඇත්තේ නාවිකයන්ගේ සිලිකන් කොඩි (RFLAGS Register) පමණි.
මෙම පරිච්ඡේදයේදී අපි බූරුවාගේ කොඩි කුටීරය හෙවත් RFLAGS රෙජිස්ටරය ක්රියාත්මක වන ආකාරයත්, CMP සහ TEST විධාන වලින් කොඩි හසුරුවන ආකාරයත්, JUMP භාවිතා නොකර බූරුවාට තීරණ ගැනීමට උගන්වන CMOVcc විධානයත් ඉතාමත් පැහැදිලිව ඉගෙන ගනිමු. මතක තබාගන්න 78 වන වාදය: බාල්දිය උතුරා ගියහොත් කොඩියක් ඔසවන්න! (When the bucket overflows, raise the red flag!).
අපේ x64 මහා බූරුවා ගණිතමය හෝ තාර්කික වැඩක් කල සැණින්, ඌ කල කාර්යයේ ප්රතිඵලය විස්තර කිරීමට කොඩි කිහිපයක් ඉහළට ඔසවයි. මෙම කොඩි සියල්ල එකතු කර තබා ගන්නා විශේෂ ලාච්චුවක් තිබේ. එය RFLAGS රෙජිස්ටරය ලෙස හඳුන්වයි.
අපට ඉතාමත් වැදගත් වන කොඩි 4ක් මෙන්න:
+-----------------------------------------------------------+
| RFLAGS |
+---------+---------+---------+---------+-------------------+
| OF (3) | SF (2) | ZF (1) | CF (0) | (අනෙකුත් කොඩි...) |
+---------+---------+---------+---------+-------------------+
ඔබ කල ගණනය කිරීමක අවසාන ප්රතිඵලය හරියටම බින්දුව (0) වුවහොත්, බූරුවා වහාම තම ශුන්ය කොඩිය ඔසවයි (ZF = 1). ප්රතිඵලය 0 නොවේ නම් කොඩිය පහත් කරයි (ZF = 0).
sub rax, rax (RAX හි අගය RAX න්ම අඩු කල විට ප්රතිඵලය 0 වේ, එවිට ZF = 1).අපේ බූරුවා නියමිත බිට් ප්රමාණය ඉක්මවා ගණන් හැදීමේදී අගයක් සීමාවෙන් එලියට පනින විට හෝ අඩු කිරීමකදී ණයට (Borrow) ගැනීමට සිදුවන විට මෙම කොඩිය ඔසවයි (CF = 1). මෙය අත්සනක් නැති සාමාන්ය අංක (Unsigned numbers) සඳහා සීමාව ඉක්මවා යාම සලකුණු කරයි.
අවසාන ප්රතිඵලය සෘණ අගයක් (Negative) වුවහොත් බූරුවා සයින් කොඩිය ඔසවයි (SF = 1). ප්රතිඵලයේ ඉහළම බිට් එක (Sign Bit) 1 වුවහොත් මූ කෙලින්ම මෙම කොඩිය ඔසවයි.
අත්සනක් සහිත සංඛ්යා (Signed numbers) එකතු කිරීමකදී හෝ අඩු කිරීමකදී බාල්දිය උතුරා ගියහොත් (එනම් ධන අගයන් දෙකක් එකතු කර සෘණ අගයක් ලැබීම වැනි දෙයක් සිදුවුවහොත්) බූරුවා ඕවර්ෆ්ලෝ කොඩිය ඔසවයි (OF = 1).
බොහෝ විට අපට අවශ්ය වන්නේ රෙජිස්ටරයක ඇති අගය වෙනස් නොකර, වෙනත් අගයක් සමඟ සසඳා බලා කොඩි පමණක් එසවීමටයි. ඒ සඳහා අපට විශේෂ විමර්ශකයෝ දෙදෙනෙක් සිටිති.
cmp destination, source
CMP විධානය ක්රියා කරන්නේ හරියට SUB (අඩු කිරීම) විධානය වගෙයි. ඌ destination අගයෙන් source අගය අඩු කරයි. හැබැයි ඌ ලැබෙන උත්තරය කොහේවත් ලියන්නේ නැත! ඌ කරන්නේ අඩු කර බලා ලැබෙන ප්රතිඵලයට අනුව RFLAGS හි කොඩි පමණක් වෙනස් කිරීමයි.
cmp rax, rbx කියා ලියූ විට:
rax - rbx = 0 වේ. එබැවින් ZF = 1 (Zero flag එසවේ).භාවිතා කරන ආකාරය:
mov rax, 50
cmp rax, 50 ; 50 සහ 50 සසඳන්න (50 - 50 = 0)
je .equal ; ZF = 1 බැවින් .equal වෙත පනින්න
test destination, source
TEST විධානය ක්රියා කරන්නේ තාර්කික AND (Logical AND) ක්රියාවලියක් ලෙසයි. ඌත් ප්රතිඵලය කොහේවත් ලියන්නේ නැත, කොඩි පමණක් වෙනස් කරයි.
බොහෝ විට ඇසෙම්බ්ලි වලදී කිසියම් රෙජිස්ටරයක් බින්දුව (0) ද කියා පරීක්ෂා කිරීමට TEST භාවිතා කරයි.
test rax, rax
රෙජිස්ටරයක් තමන් සමඟම AND කල විට ප්රතිඵලය වෙනස් නොවේ. හැබැයි RAX හි 0 තිබුණොත්, තාර්කිකව ලැබෙන ප්රතිඵලය 0 වන බැවින් ZF = 1 වේ.
භාවිතා කරන ආකාරය:
test rax, rax ; RAX බින්දුවද කියා බලන්න
jz .is_zero ; 0 නම් (ZF = 1) .is_zero වෙත පනින්න
අපේ x64 මහා බූරුවා වේගයට අතිශයින්ම ඇබ්බැහි වූ වැඩ පිස්සෙකි (3 සහ 4 වන වාදයන්). ඌ කොතරම් වේගයෙන් දුවනවාද කිවහොත්, කේතයේ JUMP එකක් හමුවූ විට, කොඩි පරීක්ෂා කර අවසන් වන තෙක් නොසිට, ඌට හිතෙන පාරක කලින්ම දුවන්නට පටන් ගනී! මෙයට ශාඛා පුරෝකථනය (Branch Prediction) යැයි කියනු ලැබේ.
නමුත් ඌ ගිය පාර වැරදි වුවහොත්, ඌට ආපසු හැරී පැමිණීමට සිදුවේ. එවිට ඌට හති වැටේ (වේගය බාල වේ). මෙය මඟ හැරීමට නවීන ප්රොසෙසර වලට කදිම උපක්රමයක් හඳුන්වා දී ඇත: CMOVcc (Conditional Move).
CMOVcc මඟින් කරන්නේ JUMP එකක් නොදා, කොඩියක් එසවී ඇත්නම් පමණක් දත්ත මාරු කිරීමයි.
cmovcc destination, source
මෙහි cc වෙනුවට කොන්දේසිය යෙදේ:
උදාහරණයක් ලෙස: අපට RAX සහ RBX අතරින් විශාලතම අංකය RCX තුලට ගැනීමට අවශ්ය යැයි සිතමු.
පැරණි ක්රමය (JUMP භාවිතා කර):
mov rcx, rax ; මුලින්ම RAX අගය RCX ට ගන්න
cmp rbx, rax ; RBX සහ RAX සසඳන්න
jle .done ; RBX කුඩා හෝ සමාන නම් .done වෙත පනින්න
mov rcx, rbx ; නැත්නම් RBX අගය RCX ට ගන්න
.done:
මෙහිදී බූරුවාට JUMP එකක් කිරීමට සිදුවේ.
නවීන ක්රමය (CMOVcc භාවිතා කර):
mov rcx, rax ; RCX = RAX
cmp rbx, rax ; RBX සහ RAX සසඳන්න
cmovg rcx, rbx ; RBX විශාල නම් (Greater) පමණක් RCX = RBX කරන්න
මෙතැන JUMP නැත! බූරුවා පාර මාරු නොකර එක දිගට දුවයි. මෙය ඉතාමත් වේගවත්ය.
සිලිකන් කොඩි කියන්නේ බූරුවාගේ ඇස් කන් වගෙයි. මේවා නිවැරදිව භාවිතා කිරීමෙන් ඔබට බූරුවා ලවා ඉතාමත් සංකීර්ණ තීරණ ගත හැක.
ඊළඟ පරිච්ඡේදයේදී අපි ඇසෙම්බ්ලි වල ඇති වැදගත්ම ගබඩාව වන කැෆටේරියා තට්ටුව (The Stack) සහ උපක්රමශීලී උපලේඛන (Procedures/Functions) ගැන ඉගෙන ගනිමු!
පරිගණක වැඩසටහනක් ධාවනය වන විට දත්ත තාවකාලිකව ගබඩා කර ගැනීමට සහ එක එක වැඩ කොටස් (Subroutines / Functions) වලට පැන ආපසු පැමිණීමට විශේෂ මතක ප්රදේශයක් අවශ්ය වේ. x64 ලෝකයේ මෙම ප්රදේශය හඳුන්වන්නේ තට්ටුව (Stack) කියාය.
මෙම පරිච්ඡේදයේදී අපි තට්ටුව ක්රියාත්මක වන ආකාරයත්, RSP රෙජිස්ටරය, PUSH සහ POP විධාන ක්රියා කරන ආකාරයත්, උපලේඛන ඇමතීම සඳහා වන CALL සහ RET ක්රියාවලියත් ඉතාමත් රසවත් ලෙස ඉගෙන ගනිමු. මතක තබාගන්න 120 වන වාදය: ඔබ කල කුණු කන්දල් ඔබම අස් කරන්න, නැතහොත් ඊළඟට එන කෙනා ලිස්සා වැටෙනු ඇත!
අපේ තට්ටුව (Stack) හරියට කැෆටේරියා එකක පිඟන් හෝ තැටි ගොඩ ගසා ඇති ස්ප්රින් (Spring) සහිත මේසයක් වැනිය.
| |
| [ Plate3 ] | <- Top of Stack (RSP)
| [ Plate2 ] |
| [ Plate1 ] |
+---------------+
මෙම ක්රමවේදයට තාක්ෂණිකව LIFO (Last-In, First-Out) හෙවත් "අවසානයට ඇතුල් වන දේ ප්රථමයෙන් පිටතට පැමිණීම" යැයි කියනු ලැබේ.
අපේ x64 මහා බූරුවාට තට්ටුවේ මුදුන කොතැනද කියා බලා ගැනීමට විශේෂ දර්ශකයක් ඇත. එය RSP (Stack Pointer) රෙජිස්ටරයයි.
පහළට දුවන තට්ටුව (Descending Stack):
x64 මතකයේදී තට්ටුව වර්ධනය වන්නේ ඉහළ මතක ලිපින වල සිට පහළ මතක ලිපින දෙසටය. එනම්, ඔබ තට්ටුවට දත්ත දමන විට RSP හි ලිපින අගය අඩු වේ (කැෆටේරියා රඳවනය පහළට යයි). දත්ත පිටතට ගන්නා විට RSP හි ලිපින අගය වැඩි වේ!
PUSH විධානය:
රෙජිස්ටරයක ඇති දත්තයක් තට්ටුව මත ගබඩා කිරීමට මෙය යොදා ගනී.
push rax ; RAX හි ඇති අගය තට්ටුව උඩින් තබන්න
මෙහිදී බූරුවා පසුබිමේදී:
RSP අගය 8කින් අඩු කරයි (බිට් 64ක ඉඩක් සඳහා): sub rsp, 8RSP පෙන්වන ලිපිනයට RAX හි අගය ලියයි: mov [rsp], raxPOP විධානය:
තට්ටුවේ මුදුනේ ඇති දත්තය ගෙන රෙජිස්ටරයකට දැමීමට මෙය යොදා ගනී.
pop rbx ; තට්ටුවේ මුදුනේ ඇති අගය ගෙන RBX ට දමන්න
මෙහිදී බූරුවා පසුබිමේදී:
RSP ලිපිනයේ ඇති දත්තය RBX ට ලියයි: mov rbx, [rsp]RSP අගය 8කින් වැඩි කරයි: add rsp, 8භාවිතය පෙන්වන සරල උදාහරණයක්:
mov rax, 100
mov rbx, 200
push rax ; තට්ටුවේ 100 ඇත
push rbx ; තට්ටුවේ 200 (උඩින්ම) සහ 100 ඇත
pop rax ; තට්ටුවෙන් 200 ඉවත් වී RAX ට යයි (දැන් RAX = 200)
pop rbx ; තට්ටුවෙන් 100 ඉවත් වී RBX ට යයි (දැන් RBX = 100)
දැක්කා නේද? අගයන් දෙක මාරු විය!
විශාල ප්රෝග්රෑම් එකක් ලියන විට හැමදේම එකම තැන ලිවීම 1 වන වාදයට පටහැනිය (මොළයට හානි සිදුවේ!). අපි පොදු වැඩ කොටස් වෙනම කේත කොටස් ලෙස ලියන්නෙමු. මේවා උපලේඛන (Subroutines / Functions) ලෙස හඳුන්වයි.
මෙම උපලේඛන වලට පැන, වැඩේ කර අවසන් වී නැවත ආපසු පැමිණීමට බූරුවාට ක්රමයක් අවශ්ය වේ. ඒ සඳහා ඌ තට්ටුව (Stack) භාවිතා කරයි.
call my_function
CALL විධානය ක්රියාත්මක වන විට බූරුවා:
my_function ලේබලය වෙත පනියි (JUMP කරයි).ret
උපලේඛනයේ අවසානයට RET හමුවූ විට බූරුවා:
RIP (Instruction Pointer) රෙජිස්ටරයට දමා කෙලින්ම එතැනට පනියි.ක්රියාකාරිත්වය පෙන්වන සම්පූර්ණ සැකසුම:
start:
call .add_numbers ; .add_numbers වෙත පනින්න, ආපසු පැමිණිය යුතු ලිපිනය තට්ටුවේ තබන්න
; (බූරුවා ආපසු පැමිණෙන්නේ මෙතැනටයි)
...
call [ExitProcess]
.add_numbers:
mov rax, 10
add rax, 20
ret ; තට්ටුවෙන් ලිපිනය POP කරගෙන නැවත start වෙත යන්න
මෙන්න 120 වන වාදය: ඔබේ කුණු කන්දල් ඔබම අස් කරන්න! (Clean up your own mess!).
උපලේඛනයක් තුලදී ඔබ යම් දත්තයක් තට්ටුවට දැමුවහොත් (PUSH), ඔබ උපලේඛනයෙන් ඉවත් වීමට (RET) පෙර අනිවාර්යයෙන්ම එය ඉවත් කල යුතුය (POP හෝ add rsp, 8).
වැරදි ක්රමය (කුණු තට්ටුව):
my_function:
push rbx ; RBX තට්ටුව මත තැබුවා (RSP අඩු විය)
; (මෙහිදී යම් වැඩක් කරන්න)
ret ; අපොයි! RBX තවම තට්ටුවේ ඇත!
මෙහිදී සිදුවන්නේ කුමක්ද? RET ක්රියාත්මක වන විට බූරුවා සිතන්නේ තට්ටුවේ මුදුනේ ඇති RBX හි අගය ආපසු පැමිණිය යුතු ලිපිනය කියාය! ඌ කෙලින්ම RBX හි තිබූ අංකය මතක ලිපිනයක් ලෙස ගෙන එතැනට පනින්නට උත්සාහ කරයි.
එතැන ඇත්තේ කිසිසේත්ම වලංගු ලිපිනයක් නොවන බැවින්, Windows විසින් වහාම ක්රියාත්මක වී ප්රෝග්රෑම් එක බිම ඇද දමා Access Violation (ක්රෑෂ්) දෝෂයක් ලබාදේ. බූරුවා අතරමං වේ!
නිවැරදි ක්රමය:
my_function:
push rbx ; RBX සුරැකීම
; (වැඩ කොටස කරන්න)
pop rbx ; තට්ටුව පිරිසිදු කිරීම (ආපසු මුල් තත්වයට ගැනීම)
ret ; දැන් තට්ටුවේ උඩින්ම ඇත්තේ නිවැරදි ආපසු යන ලිපිනයයි!
තට්ටුව යනු ඉතා වටිනා නමුත් අන්තරාදායක මෙවලමකි. එය පිරිසිදුව තබා ගැනීම ඔබ සතු වගකීමකි.
ඊළඟ පරිච්ඡේදයෙන් අපි වින්ඩෝස් රාජ සභාවේ ආචාරශීලී රීති (Windows x64 Calling Convention) සහ API භාවිතා කර වින්ඩෝස් වලට කතා කරන්නේ කෙසේද කියා ඉගෙන ගනිමු!
මිත්රවරුනි! අප මෙතෙක් වේලා කතා කළේ අපේ වැඩ බංකුව උඩ අපට ඕනෑ විදිහට, අපේම සෙල්ලම් දමන ආකාරයයි. එනම් රෙජිස්ටර (Registers) සහ තට්ටුව (Stack) සමඟ අපේම කුඩා වපසරියක වැඩ කළ ආකාරයයි. නමුත් ඔබ සදාකාලිකවම තනිවම ජීවත් වන හුදකලා දූපත් වැසියෙක් නොවේ. ඔබට පරිගණකයේ තිරය මත අකුරක් මුද්රණය කිරීමට, යතුරු පුවරුවෙන් අකුරක් කියවීමට, නැතහොත් දෘඩ තැටියට (Hard Disk) දත්ත ලියන්නට සිදු වේ.
මේ හැම වැඩක්ම කරන්නේ කවුද? ඔව්, වින්ඩෝස් මෙහෙයුම් පද්ධතිය (Windows Operating System) නමැති මහා රජතුමාය!
ඔබට කිසියම් බාහිර කාර්යයක් කර ගැනීමට අවශ්ය වූ විට, ඔබ කෙලින්ම රජ වාසල වෙත ගොස් එහි සිටින ඇමතිවරුන් සහ රාජකීය සේවකයින් (Windows API Functions) ඇමතිය යුතුය. නමුත් මතක තබා ගන්න: වින්ඩෝස් රජතුමා අතිශය රීතිගරුක, දැඩි, සහ ආඩම්බරකාරයෙකි!
ඔබ රාජ සභාවට ඇතුළු වී එහි සිටින ඇමතිවරුන් සමඟ කතා බස් කළ යුත්තේ රජතුමා විසින් පනවා ඇති විශේෂ රාජ සභා ආචාරශීලී රීති මාලාවට අනුකූලවය. මෙම රීති මාලාව පරිගණක විද්යාවේදී Microsoft x64 Calling Convention හෝ x64 ABI (Application Binary Interface) ලෙස හඳුන්වයි.
ඔබ මේ ආචාරශීලී රීති එකක් හෝ උල්ලංඝනය කළහොත් සිදු වන්නේ කුමක්ද? රජ වාසලේ සිටින දරුණු ආරක්ෂකයින් (Windows Crash Handler) පැමිණ ඔබේ ප්රෝග්රෑම් එක බෙල්ලෙන් අල්ලා රජ වාසලෙන් එලියට ඇද දමනු ඇත! (එනම්, Access Violation හෝ Crash වීමකි).
එසේ නම්, අපේ 73 වන වාදය මතකයට නඟා ගනිමු: "වැරදි කකුලෙන් නැඟී සිටියහොත් බසය මගහැරී යනු ඇත."
රාජ සභාවේදී අප පිළිපැදිය යුතු ප්රධාන ආචාරශීලී රීති 3 ක් ඇත. ඒවා පිළිවෙලින් අධ්යයනය කරමු.
පැරණි 32-bit (x86) වින්ඩෝස් ලෝකයේදී, යම් ශ්රිතයකට (Function) පරාමිතීන් (Arguments / Parameters) යැවීමට භාවිතා කළේ තට්ටුවයි (Stack). එනම් සියලුම බඩු තට්ටුව උඩ ගොඩ ගසා, ඇමතිවරයාට ඒවා එකින් එක බා ගන්න යැයි පැවසුවෙමු.
නමුත් x64 මහා බූරුවාට මේ ක්රමය ඉතා කම්මැලි සහ මන්දගාමී ක්රියාවකි. ඌට වේගයෙන් දත්ත සැපයිය යුතුය (3 වන වාදය). එබැවින් වින්ඩෝස් රජතුමා විසින් පළමු පරාමිතීන් හතර රැගෙන යාම සඳහා අතිශය වේගවත්, සිලිකන් අශ්වයින් පිට යන රාජකීය පණිවිඩකරුවන් සිව්දෙනෙකු (Registers) නම් කර ඇත.
ඕනෑම Windows API එකක් කැඳවීමේදී, ඔබ ලබා දෙන පළමු පරාමිතීන් 4 මෙම රෙජිස්ටර තුළටම දැමිය යුතුය:
RCX රෙජිස්ටරයRDX රෙජිස්ටරයR8 රෙජිස්ටරයR9 රෙජිස්ටරය ┌─────────────────────────────────────┐
│ Windows API Parameters │
└─────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ RCX │ │ RDX │ │ R8 │ │ R9 │
└─────┘ └─────┘ └─────┘ └─────┘
1st Arg 2nd Arg 3rd Arg 4th Arg
වැදගත් නිවේදනයක්: ඔබ ඇමතීමට යන API එකට ඇත්තේ පරාමිතීන් 2ක් පමණක් නම්, ඔබ
RCXසහRDXපමණක් භාවිතා කරයි.R8සහR9වලට ඕනෑම කුණු කන්දලක් තිබුණාට රජතුමාට ප්රශ්නයක් නැත.
රජ සභාවේ නීතියට අනුව, 5 වන පරාමිතියේ සිට ඉදිරියට ඇති සියල්ල තට්ටුව (Stack) හරහා යැවිය යුතුය.
උදාහරණයක් ලෙස පරාමිතීන් 6 ක් ඇති API එකක් සලකමු.
RCXRDXR8R9[RSP + 32] (තට්ටුව මත, සෙවනැලි ඉඩට කෙලින්ම ඉහළින්)[RSP + 40]අප මෙය ප්රායෝගිකව කරන අයුරු ඊළඟ පරිච්ඡේදයේදී WriteConsoleA භාවිතයෙන් විස්තරාත්මකව බලමු.
රාජ සභාවේ ඇති අමුතුම සහ බොහෝ නවකයන් අතරමං වන නීතිය මෙයයි.
ඔබ පරාමිතීන් 4ක් හෝ ඊට වඩා අඩු ගණනක් ඇති API එකක් කැඳවුවද, ඔබ අනිවාර්යයෙන්ම තට්ටුව (Stack) මත බයිට් 32ක ඉඩක් වෙන් කළ යුතුය! මෙම බයිට් 32 ක ඉඩ ප්රමාණය සෙවනැලි ඉඩ (Shadow Space) හෝ නිවාස ඉඩ (Home Space) ලෙස හඳුන්වයි.
sub rsp, 32 ; සෙවනැලි ඉඩ වෙන් කිරීම
call [SomeAPI]
add rsp, 32 ; සෙවනැලි ඉඩ නැවත පිරිසිදු කිරීම (120 වන වාදය!)
මහා බූරුවාගේ ක්රියාකාරිත්වය අනුව, API එක ඇතුලතදී RCX, RDX, R8, R9 යන රෙජිස්ටර වෙනත් වැඩ සඳහා ඉක්මනින් නිදහස් කර ගැනීමට සිදුවිය හැක. එවිට එම රෙජිස්ටර වල තිබූ අගයන් තාවකාලිකව ගබඩා කර තැබීමට API එක විසින් තට්ටුව මත අප වෙන්කළ බයිට් 32 ක ඉඩ භාවිතා කරයි.
RCX සඳහා බයිට් 8 ක්RDX සඳහා බයිට් 8 ක්R8 සඳහා බයිට් 8 ක්R9 සඳහා බයිට් 8 ක්රජතුමාගේ දැඩි නියෝගය: ඔබ අමතන API එකට කිසිදු පරාමිතියක් අවශ්ය නැති වුවද (උදාහරණයක් ලෙස පරාමිතීන් බිංදුවක් ඇති API එකක් වුවද), ඔබ මෙම බයිට් 32 ක සෙවනැලි ඉඩ තට්ටුව මත වෙන් කළ යුතුය! එසේ නොකළහොත් API එක තට්ටුවේ ඇති වෙනත් දත්ත (උදාහරණයක් ලෙස ආපසු යායුතු ලිපිනය - Return Address) මත තම දත්ත ලියා ඔබේ ප්රෝග්රෑම් එක සුණු විසුණු කර දමනු ඇත.
මෙය අපේ 73 වන වාදයට පණ දෙන කොටසයි. වින්ඩෝස් x64 මෙහෙයුම් පද්ධතියේ ඇති සියලුම API ශ්රිතයන් නිර්මාණය කර ඇත්තේ එක්තරා ස්ථීර උපකල්පනයක් මතය: "API එක CALL කිරීමට පෙර, තට්ටු දර්ශකය (RSP) බයිට් 16 කට සමාන්තරව (Aligned) තිබිය යුතුය."
බයිට් 16 කට සමාන්තරව තිබීම යන්නෙන් අදහස් කරන්නේ RSP රෙජිස්ටරයේ ඇති ලිපිනයේ අවසාන ඉලක්කම (Hexadecimal වලින්) 0 විය යුතු බවයි. (උදාහරණයක් ලෙස: 0x000000A2C3F4E560).
නමුත් මෙහි ඇති තාක්ෂණික ගැටලුව කුමක්ද?
අප ප්රෝග්රෑම් එකේදී CALL විධානය ක්රියාත්මක කරන විට, ප්රොසෙසරය විසින් ස්වයංක්රීයව ඊළඟට ක්රියාත්මක විය යුතු උපදෙස් ලිපිනය (Return Address) තට්ටුව මතට දමයි (PUSH කරයි). ලිපිනයක් යනු බයිට් 8 ක දත්තයකි.
එවිට සිදු වන්නේ කුමක්ද? බයිට් 16 ට සමාන්තරව තිබූ RSP අගය, තව බයිට් 8 ක් අඩු වී බයිට් 8 කින් අවසන් වන ලිපිනයක් බවට පත් වේ (එනම්, අග 8 කින් අවසන් වේ).
අපේ ප්රෝග්රෑම් එක ආරම්භයේදී (Entry Point) RSP පවතින්නේ බයිට් 8 ක අසමාන්තර තත්ත්වයකය. අප දැන් API එකක් ඇමතීමට සූදානම් වන්නේ නම්, අපට අවශ්ය දේ සලකා බලමු:
එබැවින්, 32 (Shadow Space) + 8 (Alignment) = 40 බයිට්!
අප ප්රෝග්රෑම් එක ආරම්භයේදීම තට්ටුවෙන් බයිට් 40 ක් අඩු කරන්නේ එබැවිනි:
sub rsp, 40 ; බයිට් 32 ක සෙවනැලි ඉඩ + බයිට් 8 ක පෙළගැස්ම
අපි මෙය ගණිතමය ලෙස තේරුම් ගනිමු.
ප්රෝග්රෑම් එක ආරම්භ වීමට පෙර RSP අගය 0x...00 (බයිට් 16 ට සමාන්තරව) යැයි සිතන්න.
CALL කරන විට:
RSP = 0x...F8 (දැන් බයිට් 8 කින් අසමාන්තරයි).sub rsp, 40 කළ විට:
RSP = 0x...F8 - 40 (Hex 0x28) = 0x...D0.CALL කරන විට:
120 වන වාදයේ මතක් කිරීම: ඔබ තට්ටුවෙන් බයිට් 40ක් අඩු කළේ නම්, ඔබේ කේතය අවසානයේදී
RETකිරීමට පෙර අනිවාර්යයෙන්ම එම බයිට් 40 නැවත එකතු කළ යුතුය (add rsp, 40). නැතහොත් මහා බූරුවාට ආපසු යායුතු ලිපිනය සොයාගත නොහැකිව මිය යනු ඇත!
පහත දැක්වෙන්නේ වින්ඩෝස් API ඇමතීමේදී භාවිතා කළ යුතු සම්මත ව්යුහයයි. මෙය ඔබේ මොළයට හානි සිදුවීම වළක්වා ගැනීමට හොඳම ආයුධයකි (1 වන වාදය).
format PE64 console
entry start
section '.code' code readable executable
start:
; 1. රාජ සභාවට ගරු කිරීම: බයිට් 40ක් වෙන් කර තට්ටුව පෙළගැස්වීම
sub rsp, 40
; 2. පරාමිතීන් සකස් කිරීම
mov rcx, 1 ; පළමු පරාමිතිය
mov rdx, 2 ; දෙවන පරාමිතිය
mov r8, 3 ; තෙවන පරාමිතිය
mov r9, 4 ; සිව්වන පරාමිතිය
; 3. රාජකීය ඇමතිවරයා ඇමතීම
; call [SomeWindowsAPI]
; 4. තට්ටුව පිරිසිදු කර පෙර තිබූ තත්ත්වයට පත් කිරීම
add rsp, 40
; 5. ප්රෝග්රෑම් එක වසා දැමීම
sub rsp, 40 ; ExitProcess සඳහා නැවත වෙන් කිරීම
xor ecx, ecx ; Exit code 0
call [ExitProcess]
මිත්රවරුනි, රාජ සභාවේ ආචාර විධි දැන් ඔබේ මොළයට ධාරණය වී ඇතැයි මම සිතමි. ඊළඟ පරිච්ඡේදයේදී අපි මෙම රීති ප්රායෝගිකව වින්ඩෝස් කොන්සෝලයේදී ක්රියාවට නංවමු!
මිත්රවරුනි! පසුගිය පරිච්ඡේදයේදී අපි වින්ඩෝස් රාජ සභාවේ ආචාර විධි (Calling Convention) ඉගෙන ගත්තෙමු. දැන් අපට ඇත්තේ එම රීති භාවිතා කර රජතුමාගේ ඇමතිවරුන් සමඟ සැබවින්ම කතාබස් කිරීමයි.
අපට පරිගණක තිරය (Console) සමඟ ගනුදෙනු කිරීමට අවශ්ය වූ විට, අපට පිහිටට එන්නේ KERNEL32.DLL නමැති රාජකීය දෙපාර්තමේන්තුවයි. මෙම දෙපාර්තමේන්තුවේ අපේ වැඩ සඳහා ඉන්නා ප්රධාන ඇමතිවරුන් (APIs) හතර දෙනෙකු අපට හමු වේ:
GetStdHandle (රාජකීය දොරටුපාලයා)WriteConsoleA (රාජකීය අණබෙරකරු)ReadConsoleA (රාජකීය ලේකම්වරයා)ExitProcess (රාජ සභාවෙන් සමුගැනීම)අපි මෙම ඇමතිවරුන් එකින් එක හඳුනාගෙන, ඔවුන්ට අණ දෙන ආකාරය බලමු.
FASM ඇසෙම්බ්ලර් එක තුල මෙම බාහිර ශ්රිතයන් (External Functions) භාවිතා කිරීමට නම්, අප ඒවා ආනයනය (Import) කළ යුතුය. FASM හි ඇති win64ax.inc නමැති උපකාරක ගොනුව භාවිතා කිරීමෙන් අපට මේ වැඩේ ඉතා පහසුවෙන් කරගත හැක.
include 'win64ax.inc'
මෙමගින් වින්ඩෝස් හි ඇති දහස් ගණනක් API වල ලිපින සහ නියතයන් (Constants) ස්වයංක්රීයව අපගේ ප්රෝග්රෑම් එකට එකතු කර දෙයි. 1 වන වාදය වන "මොළයට හානි සිදුකර ගැනීමෙන් වලකින්න!" යන්න ක්රියාත්මක වන තවත් අවස්ථාවක් වන්නේ මෙයයි. අපට හැම API එකකම ලිපිනයන් අතින් සෙවීමට අවශ්ය නැත, FASM විසින් එය අප වෙනුවෙන් සිදු කරයි.
මහා බූරුවාට කොන්සෝල් එකට යමක් ලියන්නට හෝ කියවන්නට පෙර, එම කොන්සෝලයට ඇතුල් වීමට විශේෂ අවසර පත්රයක් ලබාගත යුතුය. මෙම අවසර පත්රය පරිගණක විද්යාවේදී හසුරුව (Handle) ලෙස හඳුන්වයි. එය හරියට රජතුමා දෙන විශේෂ මුද්රාවක් සහිත ටිකට් පතක් වැනිය.
මේ සඳහා අප GetStdHandle ඇමතිවරයා භාවිතා කරමු.
STD_OUTPUT_HANDLE (නියත අගය: -11)STD_INPUT_HANDLE (නියත අගය: -10)RCX රෙජිස්ටරයට දැමිය යුතුය.RAX රෙජිස්ටරය තුළ තබා නැවත පැමිණේ.mov rcx, STD_OUTPUT_HANDLE ; RCX = -11
call [GetStdHandle] ; RAX = Output Handle
mov [stdout], rax ; අපගේ ලාච්චුවක සුරැකීම
තිරය මත අකුරු මුද්රණය කිරීමේ රාජකීය අණබෙරකරු මූය. මූට වැඩ කිරීමට පරාමිතීන් 5 ක් අවශ්ය වේ! ඔව්, පහක්ම!
RCX): අප කලින් ලබාගත් Output Handle එක.RDX): අප මුද්රණය කිරීමට බලාපොරොත්තු වන වැකිය (String) ගබඩා කර ඇති ස්ථානයේ මතක ලිපිනය (Memory Address).R8): වැකියේ ඇති අකුරු ප්රමාණය (දිග).R9): ඇත්තටම අකුරු කීයක් මුද්රණය කළාද කියා රජතුමාට ලිවීමට අප විසින් ලබා දෙන තාවකාලික විචල්යයක ලිපිනය (Pointer to Variable).[RSP + 32]): රජතුමා අනාගත වැඩකට වෙන්කර ඇති පරාමිතියකි. මෙයට සැමවිටම 0 (NULL) ලබා දිය යුතුය.; WriteConsoleA කැඳවීම
mov rcx, [stdout] ; 1 වන: Output Handle
lea rdx, [message] ; 2 වන: වැකියේ ලිපිනය
mov r8, message_len ; 3 වන: වැකියේ දිග
lea r9, [bytes_written] ; 4 වන: ලියූ ප්රමාණය සුරැකීමට විචල්යය
mov qword [rsp + 32], 0 ; 5 වන: (තට්ටුව මත සෙවනැලි ඉඩෙහි 32 වන බයිටයේ සිට)
call [WriteConsoleA]
5 වන පරාමිතියේ රහස: අප පසුගිය පරිච්ඡේදයේදී සෙවනැලි ඉඩ (Shadow Space) ලෙස තට්ටුව මත බයිට් 32 ක් වෙන් කළෙමු (
sub rsp, 40). 5 වන පරාමිතිය දැමිය යුත්තේ එම වෙන්කළ සෙවනැලි ඉඩට තට්ටුවේ ඉහලින්ම ඇති[rsp + 32]ලිපිනයටය!
යතුරු පුවරුවෙන් අකුරු කියවන රාජකීය ලේකම්වරයා මූය. මූටද පරාමිතීන් 5ක් අවශ්ය වේ.
RCX): Input Handle එක.RDX): පරිශීලකයා ලියන අකුරු ගබඩා කිරීමට අප වෙන්කළ හිස් බාල්දියේ ලිපිනය (Buffer Address).R8): අපට උපරිමයෙන් කියවීමට අවශ්ය අකුරු ප්රමාණය.R9): පරිශීලකයා ඇත්තටම අකුරු කීයක් යතුරු ලියනය කළාද කියා ලිවීමට අප ලබා දෙන විචල්යයේ ලිපිනය.[RSP + 32]): මෙයටද 0 ලබා දෙන්න.; ReadConsoleA කැඳවීම
mov rcx, [stdin] ; 1 වන: Input Handle
lea rdx, [input_buffer] ; 2 වන: බෆරයේ ලිපිනය
mov r8, 100 ; 3 වන: උපරිම අකුරු 100ක්
lea r9, [bytes_read] ; 4 වන: කියවූ ප්රමාණය සුරැකීමට විචල්යය
mov qword [rsp + 32], 0 ; 5 වන: තට්ටුව මත
call [ReadConsoleA]
වැඩ අවසන් වූ පසු මහා බූරුවා නිකම්ම අත්හැර දැමුවහොත් ඌ මතකයේ කෙළවරින් පැන විනාශ වනු ඇත (4 වන වාදය). එබැවින් ඉතා ගෞරවාන්විතව රජ වාසලෙන් පිටවීමට ExitProcess ඇමතිය යුතුය.
RCX): නික්මයාමේ කේතය (සාමාන්යයෙන් සාර්ථක නිමවීමක් සඳහා 0 ලබා දෙයි).xor ecx, ecx ; ECX = 0 (Exit Code)
call [ExitProcess]
දැන් අපි ඉහත ඉගෙන ගත් සියල්ල එකතු කර, සම්පූර්ණ ප්රෝග්රෑම් එකක් ලියමු. මෙම ප්රෝග්රෑම් එක මඟින් පරිශීලකයාගේ නම අසා, එය කියවා, නැවත "Hello, [නම]!" ලෙස මුද්රණය කරයි.
format PE64 console
entry start
include 'win64ax.inc'
section '.code' code readable executable
start:
; 1. රාජ සභා රීතිය: බයිට් 40ක් වෙන් කර තට්ටුව පෙළගැස්වීම (Alignment + Shadow)
sub rsp, 40
; 2. Output Handle එක ලබා ගැනීම
mov rcx, STD_OUTPUT_HANDLE
call [GetStdHandle]
mov [stdout], rax
; 3. Input Handle එක ලබා ගැනීම
mov rcx, STD_INPUT_HANDLE
call [GetStdHandle]
mov [stdin], rax
; 4. ප්රශ්නය මුද්රණය කිරීම: "Enter your name: "
mov rcx, [stdout] ; 1st Arg: Handle
lea rdx, [prompt] ; 2nd Arg: String Address
mov r8, prompt_len ; 3rd Arg: Length
lea r9, [bytes_written] ; 4th Arg: Pointer for output count
mov qword [rsp + 32], 0 ; 5th Arg: Stack (NULL)
call [WriteConsoleA]
; 5. පරිශීලකයාගේ පිළිතුර කියවීම
mov rcx, [stdin] ; 1st Arg: Handle
lea rdx, [input_buffer] ; 2nd Arg: Buffer Address
mov r8, 100 ; 3rd Arg: Max Length
lea r9, [bytes_read] ; 4th Arg: Pointer for read count
mov qword [rsp + 32], 0 ; 5th Arg: Stack (NULL)
call [ReadConsoleA]
; 6. සුභ පැතුම මුද්රණය කිරීම: "Hello, "
mov rcx, [stdout]
lea rdx, [greeting]
mov r8, greeting_len
lea r9, [bytes_written]
mov qword [rsp + 32], 0
call [WriteConsoleA]
; 7. කියවූ නම නැවත මුද්රණය කිරීම
mov rcx, [stdout]
lea rdx, [input_buffer] ; අප කියවූ දත්ත ඇති බෆරය
mov r8, [bytes_read] ; කියවූ අකුරු ප්රමාණය
lea r9, [bytes_written]
mov qword [rsp + 32], 0
call [WriteConsoleA]
; 8. තට්ටුව නැවත යථා තත්ත්වයට පත් කිරීම (120 වන වාදය!)
add rsp, 40
; 9. ගෞරවාන්විතව සමුගැනීම
xor ecx, ecx ; Exit Code = 0
call [ExitProcess]
section '.data' data readable writeable
prompt db 'Enter your name: ', 0
prompt_len = $ - prompt - 1
greeting db 'Hello, ', 0
greeting_len = $ - greeting - 1
stdout dq ?
stdin dq ?
bytes_written dq ?
bytes_read dq ?
input_buffer db 100 dup (?) ; අකුරු 100ක් තැබිය හැකි බාල්දියක්
sub rsp, 40: අප වින්ඩෝස් API ඇමතීමට සූදානම් වන නිසා තට්ටුව බයිට් 16 කට පෙළගස්වා සෙවනැලි ඉඩ ලබා දෙන්නෙමු.lea rdx, [prompt]: LEA (Load Effective Address) මඟින් prompt වැකියේ ආරම්භක මතක ලිපිනය RDX වෙත පටවයි.prompt_len = $ - prompt - 1: FASM හි ඇති විශේෂ ලක්ෂණයකි. $ මඟින් වර්තමාන මතක පිහිටීම පෙන්වයි. එයින් prompt හි ආරම්භක ස්ථානය සහ සෙමිකෝලන් හෝ NULL අකුරේ දිග අඩු කළ විට වැකියේ සැබෑ දිග ස්වයංක්රීයව ගණනය වේ. 1 වන වාදයට අනුව අපට අතින් අකුරු ගණන් කිරීමට අවශ්ය නැත!input_buffer db 100 dup (?): මතකයේ බයිට් 100ක ඉඩක් වෙන් කර ඒ සියල්ල හිස්ව තබයි (dup (?) යනු Duplicate Undefined යන්නයි). පරිශීලකයා යතුරු ලියනය කරන අකුරු මෙහි තැන්පත් වේ.මිත්රවරුනි, මෙම කේතය FASM මඟින් කොම්පයිල් කර ධාවනය කර බලන්න! ඔබ රාජ සභාවේ නීති අකුරටම පිළිපැද්ද නිසා, කිසිදු කරදරයකින් තොරව ඔබේ නම තිරය මත දෝංකාර දෙනු ඇත.
මිත්රවරුනි! අපේ x64 මහා බූරුවාට එකම විධානයන් නැවත නැවතත් දීමට සිදුවන විට, අපගේ ඇඟිලි රිදෙන්නටත්, මොළය තැවෙන්නටත් පටන් ගනී. උදාහරණයක් ලෙස, සෑම වින්ඩෝස් API එකක් ඇමතීමට පෙර sub rsp, 40 කිරීමත්, පරාමිතීන් RCX, RDX, R8, R9 වලට දැමීමත්, පසුව add rsp, 40 කිරීමත් ඉතාමත් වෙහෙසකර රාජකාරියකි.
අපේ 1 වන වාදය (მොළයට හානි සිදුවීමෙන් වලකින්න!) නැවත ක්රියාවට නැංවීමට කාලය පැමිණ ඇත.
මේ සඳහා FASM අපට ලබා දී ඇති රහස්ය මන්තරයක් බඳු මෙවලම වන්නේ පූර්ව-සකසනය (Preprocessor) සහ මැක්රෝ (Macros) ය. මැක්රෝ යනු සරලවම "කේතය ලියන කේතය" (Code that writes code) වේ.
මැක්රෝ එකක් යනු ඇසෙම්බ්ලි කේත කොටසකට අප විසින් දෙනු ලබන කෙටි නමකි. අප අපගේ සෝස් කේතය (Source Code) කොම්පයිල් (Compile) කිරීමට FASM වෙත ලබා දෙන විට, FASM විසින් අප ලියූ එම කෙටි නම වෙනුවට ඊට අදාළ සම්පූර්ණ කේතයම එතැනට ගෙනත් තබයි (Text Substitution).
මෙය ප්රොසෙසරය (මහා බූරුවා) දන්නේවත් නැත. මක්නිසාද යත්, බූරුවා ළඟට කෑම බාල්දිය යාමට පෙර FASM විසින් එම මැක්රෝ එක දිග හැර (Expand කර) සාමාන්ය කේතය බවට පත් කර අවසන් බැවිනි.
අපට රෙජිස්ටරයක් බිංදුව (0) කිරීමට නිතරම xor reg, reg ලියන්නට සිදු වේ. අපි මේ සඳහා clear නමින් මැක්රෝ එකක් ලියමු:
macro clear register {
xor register, register
}
දැන් අපට අපගේ කේතය තුළ මෙසේ ලිවිය හැක:
clear rax ; FASM විසින් මෙය xor rax, rax බවට හරවයි
clear rbx ; FASM විසින් මෙය xor rbx, rbx බවට හරවයි
බලන්න, කොතරම් පහසුද? අපේ ඇඟිලි වලට ලැබෙන සහනය මහත්ය!
වින්ඩෝස් API ඇමතීම පහසු කරවන මැක්රෝ එකක් නිර්මාණය කරමු. අපට අවශ්ය වන්නේ API එකේ නම දුන් විට ස්වයංක්රීයව තට්ටුව පෙළගස්වා එය ඇමතීමටයි:
macro call_api api_name {
sub rsp, 40
call [api_name]
add rsp, 40
}
දැන් අපට ExitProcess ඇමතීමට අවශ්ය වූ විට මෙසේ ලිවිය හැක:
xor ecx, ecx
call_api ExitProcess
ප්රෝග්රෑමිං ලෝකයේ නවකයන් නිතරම පටලවා ගන්නා තවත් දෙයක් නම් මැක්රෝ (Macros) සහ උපක්රම/ශ්රිත (Subroutines / Functions) අතර වෙනසයි.
| ලක්ෂණය | මැක්රෝ (Macro) | උපක්රමය (Subroutine / Function) |
|---|---|---|
| ක්රියාකාරිත්වය | FASM විසින් කේතය කෙලින්ම පිටපත් කර තබයි (Inlined). | CALL සහ RET විධාන මඟින් වෙනත් මතක ලිපිනයකට පනී. |
| වේගය | ඉතාමත් වේගවත්ය. පැනීම් (Jumps) හෝ තට්ටු හැසිරවීම් නැත. | පැනීම් නිසා සහ තට්ටුවේ වැඩ නිසා සුළු වේලාවක් ගතවේ. |
| ප්රමාණය | කේතය නැවත නැවත පිටපත් වන නිසා ෆයිල් එකේ ප්රමාණය (Executable Size) විශාල වේ. | කේතය ඇත්තේ එකම තැනක බැවින් ප්රමාණය ඉතා කුඩා වේ. |
| භාවිතය | කුඩා, නිතර භාවිතා වන වේගවත් වැඩ සඳහා සුදුසුය. | විශාල, සංකීර්ණ සහ දිගු කේත කොටස් සඳහා සුදුසුය. |
මතකයේ ඇති දත්ත සමූහයක් ක්රමානුකූලව පෙළගස්වා තැබීමට FASM හි ව්යුහයන් (Structs) භාවිතා කළ හැක.
උදාහරණයක් ලෙස, තිරය මත ඇති ලක්ෂ්යයක් (Point) සලකමු. එහි ඛණ්ඩාංකය සඳහා බයිට් 8 ක් සහ ඛණ්ඩාංකය සඳහා බයිට් 8 ක් ඇත. අපට මෙය ව්යුහයක් ලෙස මෙසේ අර්ථ දැක්විය හැක:
struc POINT {
.x dq ?
.y dq ?
}
දැන් අපට .data සෙක්ෂන් එක තුළ මෙවැනි ලක්ෂ්යයන් සෑදිය හැක:
section '.data' data readable writeable
my_point POINT
මෙම ලක්ෂ්යයේ අගය වෙනස් කිරීමට අවශ්ය නම් අපට මෙසේ කළ හැක:
mov rax, 150
mov [my_point.x], rax ; my_point හි X අගයට 150 දැමීම
මෙලෙස ව්යුහයන් භාවිතා කිරීමෙන් අපේ මතකයේ ඇති දත්ත වල ලිපිනයන් (Offsets) අතින් ගණනය කිරීමට සිදු නොවේ. මතක තබා ගන්න: "අතින් ගණනය කරන්නට ගියහොත් 178 වන වාදය (කිසිවෙකු පරිපූර්ණ නැත) ක්රියාත්මක වී වැරදි සිදුවනු ඇත!"
කේතය තුළ එකම අංකය නැවත නැවත ලියනවා වෙනුවට ඒවා නියතයන් ලෙස අර්ථ දැක්වීමෙන් කේතය කියවීමේ හැකියාව වැඩි වේ. FASM හි නියතයක් අර්ථ දක්වන්නේ = හෝ define මඟින්ය.
BUFFER_SIZE = 100
STD_OUTPUT_HANDLE = -11
මිත්රවරුනි! මෙම පරිච්ඡේදයෙන් අපේ බූරුවාට ඉතා පහසුවෙන් සහ අපගේ මොළය වෙහෙස නොකර වැඩ ගන්නා අයුරු ඔබ ඉගෙන ගත්තේය. මීළඟ පරිච්ඡේදය වන්නේ අප මෙතෙක් ඉගෙන ගත් සියල්ලේ සාරාංශය සහ ඔබේ පණ බේරාගැනීමේ Survival Guide එක අඩංගු ඇමුණුමයි (Annex)!
මිත්රවරුනි! අපේ ගමනේ අවසාන නැවතුමට අපි පැමිණ සිටිමු. මෙම පරිච්ඡේදය ලියා ඇත්තේ ඔබ යුධ පිටියේ (Coding) තනිවම සටන් කරන විට, මහා බූරුවා කලබල වී ඔබේ ප්රෝග්රෑම් එක කුඩුපට්ටම් කර දමන මොහොතක, ඔබේ පණ බේරාගැනීම සඳහාම පමණි.
මෙය නිතරම ළඟ තබාගෙන කියවිය හැකි කෙටි සටහන් එකතුවකි (Cheat Sheet).
x64 බූරුවාගේ වැඩ බංකුවේ සිටින ප්රධාන සේවකයින් 16 දෙනා සහ ඔවුන්ගේ සම්ප්රදායික රාජකාරි මෙසේය:
| රෙජිස්ටරය | ප්රමාණය | සම්ප්රදායික කාර්යය (Traditional Role) | API කැඳවීමේදී (Microsoft ABI Status) |
|---|---|---|---|
RAX |
බිට් 64 | ගණනය කිරීම් සහ ශ්රිතයන්ගේ ප්රතිඵලය (Return Value) | වෙනස් වේ (Volatile) |
RBX |
බිට් 64 | දත්ත තබාගන්නා පදනම (Base Register) | සුරැකිය යුතුය (Non-Volatile) |
RCX |
බිට් 64 | ලූප ගණකය (Counter) / 1 වන පරාමිතිය | වෙනස් වේ (Volatile) |
RDX |
බිට් 64 | දත්ත (Data) / බෙදීමේදී ඉතිරිය / 2 වන පරාමිතිය | වෙනස් වේ (Volatile) |
RSI |
බිට් 64 | ප්රභව දර්ශකය (Source Index) | සුරැකිය යුතුය (Non-Volatile) |
RDI |
බිට් 64 | ගමනාන්ත දර්ශකය (Destination Index) | සුරැකිය යුතුය (Non-Volatile) |
RBP |
බිට් 64 | තට්ටුවේ පදනම (Base Pointer) | සුරැකිය යුතුය (Non-Volatile) |
RSP |
බිට් 64 | තට්ටු දර්ශකය (Stack Pointer) - ඉහලම පිඟාන | සුරැකිය යුතුය (Non-Volatile) |
R8 |
බිට් 64 | සාමාන්ය කාර්යයන් / 3 වන පරාමිතිය | වෙනස් වේ (Volatile) |
R9 |
බිට් 64 | සාමාන්ය කාර්යයන් / 4 වන පරාමිතිය | වෙනස් වේ (Volatile) |
R10 - R11 |
බිට් 64 | තාවකාලික ගබඩා (Scratch registers) | වෙනස් වේ (Volatile) |
R12 - R15 |
බිට් 64 | සාමාන්ය කාර්යයන් | සුරැකිය යුතුය (Non-Volatile) |
වැදගත් රහසක් (Volatile vs Non-Volatile):
- Volatile (වෙනස් වන): ඔබ වින්ඩෝස් API එකක් ඇමතූ සැනින්, රජතුමා විසින්
RAX,RCX,RDX,R8,R9,R10,R11යන රෙජිස්ටර වල ඇති දත්ත විනාශ කර දමනු ඇත! එබැවින්, API එකක් කැඳවීමට පෙර මේවායේ ඇති වටිනා දත්ත වෙනත් තැනක සඟවා තබන්න.- Non-Volatile (සුරැකෙන): API එක ක්රියාත්මක වී අවසන් වුවද
RBX,RSI,RDI,RBP,RSP,R12-R15යන රෙජිස්ටර වල දත්ත වලට හානියක් නොවේ. ඒවා සුරක්ෂිතව පවතී.
mov dest, src : src හි ඇති දත්ත dest වෙත පිටපත් කරයි. (මතක තබා ගන්න: මතකයේ සිට මතකයට සවලෙන් කෙලින්ම දැමිය නොහැක!)add dest, src : dest = dest + srcsub dest, src : dest = dest - srcimul dest, src : සංගුණක වැඩි කිරීම (Signed Multiplication).idiv reg : බෙදීම. බෙදීමට පෙර අනිවාර්යයෙන්ම xor rdx, rdx කර තිබිය යුතුය. RAX හි ප්රතිඵලයද, RDX හි ඉතිරියද ඉතුරු වේ.cmp op1, op2 : දෙක සංසන්දනය කර RFLAGS රෙජිස්ටරයේ සිලිකන් කොඩි ඔසවයි (CF, ZF, SF, OF).test op1, op2 : Bitwise AND එකක් කර ප්රතිඵලය ඉවත් කර කොඩි පමණක් වෙනස් කරයි. රෙජිස්ටරයක් බිංදුවදැයි බැලීමට test rax, rax බහුලව යොදයි.jmp label : කොන්දේසි විරහිතව label වෙත පනියි.je / jne : සමාන නම් පනින්න (Jump if Equal) / සමාන නැත්නම් පනින්න (Jump if Not Equal).push reg : reg හි ඇති දත්ත තට්ටුව (Stack) උඩින් තබයි. RSP අගය බයිට් 8කින් අඩු වේ.pop reg : තට්ටුව මුදුනේ ඇති දත්ත ගෙන reg වෙත දමයි. RSP අගය බයිට් 8කින් වැඩි වේ.call func : ආපසු පැමිණිය යුතු ලිපිනය තට්ටුවට දමා func වෙත පනියි.ret : තට්ටුව මුදුනේ ඇති ලිපිනය ගෙන නැවත ප්රධාන කේතයට පනියි (Return).අපි API එකක් ඇමතීමට පෙර sub rsp, 40 කළ පසු, තට්ටුව මත දත්ත ගොඩගැසී ඇති ආකාරය (ASCII Art ඇසුරින්) මෙන්න:
RSP ලිපිනය තට්ටුවේ අන්තර්ගතය (Stack Contents)
┌────────────────────────────────────────┐
[RSP + 0] │ Shadow Space for RCX (බයිට් 8) │ ◄─── RSP පෙන්වන ස්ථානය
├────────────────────────────────────────┤
[RSP + 8] │ Shadow Space for RDX (බයිට් 8) │
├────────────────────────────────────────┤
[RSP + 16] │ Shadow Space for R8 (බයිට් 8) │
├────────────────────────────────────────┤
[RSP + 24] │ Shadow Space for R9 (බයිට් 8) │
├────────────────────────────────────────┤
[RSP + 32] │ 5 වන පරාමිතිය (5th Argument) / Padding │ ◄─── WriteConsole හි 5 වන පරාමිතිය
├────────────────────────────────────────┤
[RSP + 40] │ Return Address (ප්රධාන කේතයට ආපසු පාර) │
└────────────────────────────────────────┘
sub rsp, 40 (හෝ නිසි ප්රමාණයක්) කර නොමැත.mov rax, [0]).sub rsp, 40 තිබේදැයි සහ අවසානයේ add rsp, 40 තිබේදැයි පරීක්ෂා කරන්න.RCX) අඩු කිරීමට අමතක වීම (dec rcx) හෝ පැනීමේ කොන්දේසිය වැරදීම.RCX, RDX, R8, R9 ආදිය) වල දත්ත තබාගෙන API එකක් ඇමතීම.RBX වැනි සුරැකෙන රෙජිස්ටරයකට මාරු කරන්න, නැතහොත් තට්ටුවට PUSH කර පසුව POP කරන්න.format PE64 console සහ entry start ලියා තිබේද?win64ax.inc ගොනුව ඇතුලත් කර තිබේද?sub rsp, 40 කර තට්ටුව පෙළගස්වා තිබේද?RCX, RDX, R8, R9 යන අනුපිළිවෙලින් දමා තිබේද?mov qword [rsp + 32], Value ලෙස ලබා දී තිබේද?add rsp, 40 කර තට්ටුව පිරිසිදු කර තිබේද? (120 වන වාදය)ExitProcess කැඳවා ප්රෝග්රෑම් එක වසා දමා තිබේද?මිත්රවරුනි! දැන් ඔබ සතුව අවශ්ය සියලුම ආයුධ සහ දැනුම ඇත. මහා බූරුවා ඔබට අවශ්ය පරිදි මෙල්ල කර ගැනීමට මෙම පොත ඔබට මහත් පිටුවහලක් වනු ඇතැයි මම විශ්වාස කරමි.