हाथ से लिखी असेंबली में हर जगह दोहराव होता है। निर्देशों का वही छोटा सिलसिला, थोड़े-बहुत फर्क के साथ, बार-बार आता है, और दोहराए जाने वाले कोड को घटाने के लिए बहुत कम एब्स्ट्रैक्शन मिलते हैं। सिर्फ इतना ही नहीं, असेंबली में कोडिंग करते समय ऐसी वैल्यूओं को सँभालना पड़ता है जिनका कोई साफ अर्थ नहीं होता। यहाँ नाम वाले पैरामीटर नहीं होते, और भीतर चलने वाली मशीनरी की ज़्यादातर बारीकियाँ प्रोग्रामर को खुद सँभालनी पड़ती हैं।
इस ट्रैक में इस्तेमाल होने वाला असेंबलर, NASM, कुछ ऐसे औज़ार देता है जो प्रोग्रामर की इन दोनों दिक्कतों में मदद कर सकते हैं। ये औज़ार असेंबल समय पर चलते हैं, यानी इससे पहले कि कोई भी निर्देश प्रोसेसर तक पहुँचे। ये असेंबलर को बताते हैं कि सोर्स टेक्स्ट को कैसे फिर से लिखना है, इससे पहले कि वह मशीन कोड में बदले जाए।
इनमें सबसे ज़रूरी औज़ार हैं मैक्रो, जिन्हें असेंबलर के लिए एक तरह का नुस्खा माना जा सकता है। जहाँ इनका इस्तेमाल होता है, वहीं इनका विस्तार कर दिया जाता है, जैसे वह कोड वहाँ हाथ से लिखा गया हो।
मैक्रो कई शाब्दिक टोकन पैरामीटर के तौर पर ले सकते हैं, जिससे इन्हें अलग-अलग जगहों पर दोबारा इस्तेमाल किया जा सकता है।
लेकिन ये फंक्शन नहीं हैं: इनमें न call होता है, न ret, और न ही कोड में किसी दूसरे बिंदु तक नियंत्रण का हस्तांतरण।
सबसे आसान तरीका है %define की मदद से किसी वैल्यू या छोटे एक्सप्रेशन को एक नाम देना:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
असेंबली से पहले WIDTH जहाँ-जहाँ भी आगे आता है, वहाँ उसकी जगह 8 रख दिया जाता है।
यह बदलाव टेक्स्ट के स्तर पर होता है, यानी यह नाम जहाँ भी दिखे, वहीं उसकी जगह लगा दिया जाता है, चाहे वह किसी ऑपरेंड में हो या किसी दूसरी परिभाषा के अंदर।
संख्यात्मक कॉन्स्टेंट के लिए %define की जगह equ इस्तेमाल करना बेहतर है।
%define से बनाया गया एक-पंक्ति वाला मैक्रो सिर्फ इस्तेमाल होने पर ही फैलता है, और उसे दोबारा असाइन किया जा सकता है या हटाया जा सकता है।
दूसरी ओर, equ कॉन्स्टेंट परिभाषा के समय ही तय हो जाता है और उसे कभी बदला नहीं जाता।
%define नाम वाले पैरामीटर भी ले सकता है, और तब वह एक छोटे इनलाइन फंक्शन की तरह काम करता है:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
पूरी बॉडी को और हर पैरामीटर को गोल ब्रैकेट में लपेटिए।
%define square(x) x*x के साथ square(1+1) कॉल फैलकर 1+1*1+1 बन जाता है, जो 3 है।
अगर इसे %define square(x) ((x)*(x)) लिखा जाए, तो वही कॉल फैलकर ((1+1)*(1+1)) बन जाता है, जो 4 है।
%define से बनाए गए नामों में छोटे और बड़े अक्षर अलग-अलग माने जाते हैं।
अगर ज़रूरत हो, तो %idefine वाला रूप नाम में छोटे-बड़े अक्षरों का यह फर्क मिटा देता है।
कोड के अलग-अलग हिस्सों को अर्थ देने के लिए एक-पंक्ति वाले मैक्रो काम आते हैं। अगर इन्हें सोच-समझकर इस्तेमाल किया जाए, तो इससे कोड पढ़ने में आसानी होती है और गलत टाइप होने की गुंजाइश घटती है। उदाहरण के लिए, किसी फंक्शन के अंदर किसी खास काम के लिए इस्तेमाल होने वाले रजिस्टर को उसी काम के नाम से परिभाषित किया जा सकता है:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
कोड के पूरे सिलसिले को, चाहे उसमें कितनी भी पंक्तियाँ हों, %macro से पकड़ा जाता है और %endmacro से बंद किया जाता है।
नाम के बाद लिखी संख्या बताती है कि मैक्रो कितने आर्गुमेंट लेता है, जो शून्य भी हो सकते हैं।
आर्गुमेंट पास करते समय उन्हें कॉमा (,) से अलग किया जाता है:
%macro example_macro 0
...
%endmacro
बहु-पंक्ति वाले मैक्रो में आर्गुमेंट के नाम नहीं होते।
उन्हें जिस क्रम में पास किया जाता है, उसी क्रम में %1, %2 और इसी तरह से पुकारा जाता है:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
ध्यान रखिए कि आर्गुमेंट उन कामों के लिए सही सिंटैक्स में होने चाहिए जो मैक्रो की बॉडी में किए जाते हैं।
यहाँ mult_two eax, ecx और mult_two r8, r9 दोनों असेंबल हो जाते हैं, लेकिन mult_two eax, cl नहीं होगा, क्योंकि imul eax, cl परिभाषित नहीं है।
जो मैक्रो कोई साधारण लेबल बनाता है, वह दूसरी बार इस्तेमाल होते ही टूट जाता है, क्योंकि तब वह लेबल भी दो बार बन जाएगा।
ऐसे मैक्रो के अंदर लेबल इस्तेमाल करने के लिए, जिसे एक से ज़्यादा बार कॉल किया जा सकता है, ऐसे लेबल से पहले %% लगाना ज़रूरी है।
इससे असेंबलर को निर्देश मिलता है कि हर बार फैलने पर इस लेबल को एक अलग, अनोखा नाम दे:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
times की मदद से एक पंक्ति के कोड को दोहराया जा सकता है।
यह खास तौर पर तब काम आता है जब मेमोरी में एक जैसी वैल्यूएँ घोषित करनी हों:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
कई पंक्तियों वाले कोड को दोहराने के लिए हम %rep इस्तेमाल कर सकते हैं:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
ध्यान दीजिए कि दोहराव की संख्या %rep के ठीक बाद लिखी जाती है, और दोहराए गए ब्लॉक को बंद करने के लिए %endrep होता है।
%rep ब्लॉक के अंदर असेंबल समय का काउंटर इस्तेमाल करना आम बात है, ठीक वैसे ही जैसे रनटाइम लूप में काउंटर काम आता है।
असेंबल समय के पूर्णांक कॉन्स्टेंट को equ से घोषित किया जा सकता है, लेकिन असेंबल समय के पूर्णांक वेरिएबलो के लिए हम %assign इस्तेमाल करते हैं।
%define के उलट, %assign का मान तुरंत किसी संख्या में बदल दिया जाता है और उसे दोबारा असाइन किया जा सकता है:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
प्रीप्रोसेसर के चलने के बाद लूप गायब हो जाता है और चार add निर्देश बचते हैं, जिनमें 0, 1, 2 और 3 इमीडिएट पहले से भरे होते हैं।
यह असेंबलर का किया हुआ लूप अनरोलिंग है: न कोई काउंटर रजिस्टर, न कोई ब्रांच, बस सीधी-सीधी चलने वाला कोड।
प्रीप्रोसेसर के पास कई तरह की शर्तें उपलब्ध हैं। ये कई अलग-अलग रास्तों में से एक चुनती हैं और अपनी बॉडी को वहीं खोल देती हैं, लेकिन सिर्फ तब जब उनकी शर्त सही हो।
%if ... %elif ... %else ... %endif किसी संख्यात्मक जाँच के आधार पर एक ब्रांच चुनते हैं:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
मैक्रो अक्सर अपनी अलग फाइल में रखे जाते हैं और जिस सोर्स को इनकी ज़रूरत होती है, उसमें %include से जोड़ लिए जाते हैं:
%include "patterns.mac"
बताई गई फाइल उसी जगह, असेंबली से पहले, ज्यों की त्यों जोड़ दी जाती है, बिल्कुल ऐसे जैसे उसका सारा सामान वहीं टाइप किया गया हो। इससे परिभाषाएँ एक ही जगह रहती हैं और कई सोर्स फाइलें उन्हें आपस में बाँट सकती हैं।
अगर मैक्रो फाइल को अकेले भी असेंबल किया जा सकता है, तो उसका एक्सटेंशन .asm होना चाहिए।
ध्यान रखिए कि मैक्रो का कोई लिंकेज नहीं होता।
मैक्रो फाइल चाहे असेंबल हो भी जाए, उसके मैक्रो सिर्फ उसी दूसरी फाइल को दिखते हैं जो उसे %include करती है।
ऐसा न हो तो सिर्फ %include करने के लिए बनी फाइल का एक्सटेंशन कुछ भी हो सकता है।
.asm या .mac में से किसी एक को इस्तेमाल करना आम परंपरा है।
आप उस मैक्रो लाइब्रेरी को बनाए रखते हैं जिस पर एक पत्रिका का कंपोज़िशन इंजन टिका है। इंजन खुद, यानी वे फंक्शन जो हर पेज को सजाते हैं, तय है और उसे आप नहीं लिखते। आप उस मशीनरी को लिखते हैं जो असेंबल के समय इस इंजन को बनाती है।
इस अभ्यास में आपको कोई फंक्शन लिखने की ज़रूरत नहीं है, सिर्फ मैक्रो लिखने हैं। इन मैक्रो को फिर किसी दूसरी फाइल में पहले से तय फंक्शनों में शामिल करके कॉल किया जाता है।
आपके पास चार काम हैं।
हर मैक्रो को ठीक उसी नाम और उसी आर्गुमेंट क्रम से कॉल किया जाता है जो उसका काम बताता है, इसलिए इन्हें बिल्कुल वैसा ही लिखिए। वरना फाइल असेंबल नहीं होगी।
पत्रिका का फॉर्मेट कुछ गिनी-चुनी तय संख्याओं से बना है, और बाकी लेआउट उन्हें सिर्फ नाम से पुकारता है। आपको असेंबल के समय हर एक को पढ़ने लायक नाम देना है:
COLUMN_COUNT की वैल्यू 4 हैCOLUMN_STRIDE की वैल्यू 18 हैSPACE की वैल्यू 0x20 है, यानी खाली बाइटRULE की वैल्यू 0x3D है, यानी बाइट =
RULE_WIDTH की वैल्यू 4 हैPLAIN की वैल्यू 0x2E है, यानी बाइट .
RULED की वैल्यू 0x2D है, यानी बाइट -
BOXED की वैल्यू 0x23 है, यानी बाइट #
इंजन इन्हें ऊपर दिए क्रम में बताता है।
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run मैक्रो लिखिएइंजन गैली को खाली करता है और मास्टहेड रूल बनाता है, और इसके लिए वह एक ही बाइट की एक पंक्ति छापता है। उस चरण को एक मल्टी-लाइन मैक्रो के रूप में लिखिए, जिसे इंजन जहाँ भी कोई पंक्ति भरनी हो वहाँ कॉल कर सके।
सबसे पहले रजिस्टर rcx के लिए पढ़ने लायक नाम COUNTER तय कीजिए।
फिर fill_run मैक्रो बनाइए, जो एक ऐरे को किसी बाइट की N प्रतियों से भर दे।
यह तीन आर्गुमेंट लेता है, इस क्रम में:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
fill_run मैक्रो COUNTER को जितना चाहे बदल सकता है, लेकिन किसी और रजिस्टर को नहीं।
ध्यान दीजिए कि बाइटों की संख्या 64-बिट जनरल-पर्पस रजिस्टर में पास की जा सकती है, जैसे rax या rsi, या इमीडिएट के रूप में, जैसे 10 या 4।
आप मान सकते हैं कि यह संख्या कभी ऋणात्मक नहीं होती।
यह मैक्रो दो अलग-अलग फंक्शनों में, कुल तीन बार कॉल किया जाता है।
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns मैक्रो लिखिएइस फॉर्मेट की ग्रिड तय है, इसलिए कॉलमों के शुरुआती एड्रेस असेंबल के समय ही निकाले जा सकते हैं।
lay_columns मैक्रो बनाइए, जो COLUMN_COUNT ऑफसेट लिखता है, हर एक 64-बिट पूर्णांक।
यह दो आर्गुमेंट लेता है, इस क्रम में:
lay_columns rsi, rax
गंतव्य एड्रेस पर लिखे जाने वाले ऑफसेटों की संख्या तय है और COLUMN_COUNT के बराबर है।
हर ऑफसेट एक 64-बिट वैल्यू है, जो offset[i] = margin + i * COLUMN_STRIDE से निकाली जाती है, जहाँ i ऑफसेट का इंडेक्स है जो 0 से शुरू होता है, और margin दूसरा आर्गुमेंट है।
यह मैक्रो अपने दोनों रजिस्टर आर्गुमेंट बदल सकता है, लेकिन और कोई नहीं। इसे एक बार कॉल किया जाता है।
margin = 3
out = {3, 21, 39, 57}
set_header मैक्रो लिखिएतीनों स्थायी सेक्शनों में से हर एक का हेडर स्टाइल फॉर्मेट से तय है, इसलिए बाइट असेंबल के समय ही पता होता है।
set_header बनाइए, जो दो आर्गुमेंट लेता है, इस क्रम में:
0 सादा है, 1 रेखांकित है, और 2 बॉक्स वाला है।गंतव्य एड्रेस एक प्रभावी मेमोरी एड्रेस का एक्सप्रेशन है।
इसमें एक बेस रजिस्टर होता है, जिसे 1, 2, 4 या 8 से स्केल किए गए इंडेक्स रजिस्टर के साथ जोड़ा जा सकता है, और एक इमीडिएट:
set_header rdx+8*rcx+2, 1
हेडर बाइट (PLAIN, RULED या BOXED) चुनने के लिए रनटाइम ब्रांच की जगह कंडीशनल असेंबली (%if / %elif / %else) इस्तेमाल कीजिए।
इस मैक्रो को कोई भी रजिस्टर नहीं बदलना चाहिए। इसे एक ही फंक्शन में तीन बार कॉल किया जाता है।
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Exercism पर साइन अप कीजिए और x86-64 Assembly को 22 कॉन्सेप्ट130 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।