आमतौर पर ऑपरेटिंग सिस्टम (OS) किसी प्रोग्राम के लिए मेमोरी का एक सामान्य लेआउट तय करता है:
| एड्रेस | मेमोरी क्षेत्र |
|---|---|
| उच्च | स्टैक |
| ... | |
| हीप | |
| पढ़ने-लिखने योग्य सेगमेंट | |
| कोड/केवल पढ़ने योग्य सेगमेंट | |
| निम्न | आरक्षित |
सेगमेंट में बँटी मेमोरी को अलग-अलग अनुमतियों वाले सेक्शन में व्यवस्थित किया जाता है।
अब तक हमने जो फंक्शन बनाए हैं, वे सभी section .text में थे। इस सेक्शन में केवल पढ़ने योग्य एक्ज़ीक्यूटेबल डेटा रहता है। दूसरे सेक्शन डेटा वेरिएबल घोषित करने के लिए इस्तेमाल होते हैं, जो केवल पढ़ने योग्य या पढ़ने-लिखने योग्य हो सकते हैं, लेकिन एक्ज़ीक्यूटेबल नहीं होते।
इनिशियलाइज़ किया गया डेटा section .data में घोषित किया जाता है।
NASM (The Netwide Assembler, यानी इस ट्रैक में इस्तेमाल होने वाला असेंबलर) में इनिशियलाइज़ किए गए वेरिएबल का एक नाम होता है, एक डायरेक्टिव होता है जो डेटा का साइज़ बताता है, और कॉमा से अलग की गई वैल्यू की एक सूची होती है।
इनमें से हर एक को दूसरे से एक स्पेस से अलग किया जाता है, और लेबल के बाद वैकल्पिक रूप से : भी आ सकता है।
मुख्य डायरेक्टिव और उनसे जुड़े डेटा साइज़ ये हैं:
| डायरेक्टिव | साइज़ |
|---|---|
| db | 1 बाइट |
| dw | 2 बाइट |
| dd | 4 बाइट |
| dq | 8 बाइट |
उदाहरण के लिए, यह space नाम का एक बाइट वेरिएबल घोषित करता है जिसकी वैल्यू 10 है:
section .data
space db 10
section .data में घोषित वेरिएबल परिवर्तनीय होते हैं, यानी वे पढ़ने-लिखने योग्य होते हैं।
इनकी स्टोरेज अवधि भी static होती है, यानी प्रोग्राम के पूरे रनटाइम तक ये बने रहते हैं।
section .rodata, section .data जैसा ही होता है।
दोनों सेक्शन में इनिशियलाइज़ किया गया डेटा रहता है, जिसे एक ही तरीके से घोषित किया जाता है और जिसकी स्टोरेज अवधि भी एक जैसी होती है।
इनमें मुख्य अंतर यह है कि section .rodata का डेटा अपरिवर्तनीय होता है, यानी केवल पढ़ने योग्य।
equ से परिभाषित कॉन्स्टेंट, section .rodata में परिभाषित कॉन्स्टेंट से अलग होते हैं।
equ से परिभाषित कॉन्स्टेंट मेमोरी में जगह नहीं घेरता, बल्कि असेंबलर सीधे उसकी जगह उसकी वैल्यू रख देता है।
वास्तव में यह उस वैल्यू के लिए एक प्लेसहोल्डर है।
दूसरी ओर, section .rodata में परिभाषित कॉन्स्टेंट वास्तव में मेमोरी में संग्रहीत होते हैं और उनका एक एड्रेस होता है।
घोषित डेटा से जुड़ा एक नाम होना ज़रूरी है। इस नाम को लेबल कहते हैं।
लेबल एक सिंबल होता है जो मेमोरी में डेटा के विशिष्ट एड्रेस को दर्शाता है। x86-64 में एड्रेस 64-बिट वैल्यू होते हैं।
NASM में डेटा तक सीधे उसके लेबल से पहुँचने की कोशिश करने पर आवंटित मेमोरी नहीं मिलती, बल्कि उसका एड्रेस मिलता है:
section .data
example dq 27 ; this declares a 8-byte variable initialized with 27
section .text
fn:
mov rax, example ; this stores the address of the declared variable in rax, not its contents
...
किसी मेमोरी एड्रेस की सामग्री तक पहुँचने के लिए उसे डीरेफ़रेंस करना ज़रूरी होता है। इसे इनडायरेक्शन कहते हैं।
NASM में यह [] से किया जाता है:
section .data
example dq -27 ; this declares a 8-byte variable initialized with -27
section .text
fn:
mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
...
लेकिन कुछ स्थितियाँ ऐसी होती हैं जहाँ डीरेफ़रेंस की गई मेमोरी के साइज़ को लेकर संदेह हो सकता है। ऐसी स्थितियों में इस साइज़ को बताने वाला एक प्रीफ़िक्स इस्तेमाल करना ज़रूरी होता है।
आमतौर पर एक x86-64 प्रोग्राम में सबसे महत्वपूर्ण प्रीफ़िक्स और उनके साइज़ ये हैं:
| प्रीफ़िक्स | साइज़ |
|---|---|
| byte | 1 बाइट |
| word | 2 बाइट |
| dword | 4 बाइट |
| qword | 8 बाइट |
यही लोड साइज़ को स्पष्ट रूप से लिखकर भी किया जा सकता है:
mov rax, qword [example] ; same dereference, size stated explicitly
मेमोरी को डीरेफ़रेंस करते समय हमेशा प्रीफ़िक्स इस्तेमाल करना अच्छा अभ्यास है।
मेमोरी में लिखना भी उसी तरह, किसी एड्रेस को डीरेफ़रेंस करके किया जाता है:
section .data
example1 db 10 ; example1 is a 1-byte memory location initialized with value 10
example2 dq -456 ; example2 is a 8-byte memory location initialized with value -456
example3 dd 54 ; example3 is a 4-byte memory location initialized with value 54
section .text
fn:
mov byte [example1], 20 ; example1 now has value 20
mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
mov dword [example3], eax ; example3 now has value equal to the contents in eax
ध्यान दीजिए कि ज़्यादातर इंस्ट्रक्शन में आप मेमोरी ऑपरेंड का इस्तेमाल इससे पहले कि सामग्री को किसी रजिस्टर में लोड करें, कर सकते हैं। लेकिन आमतौर पर उन्हें सोर्स और डेस्टिनेशन, दोनों ऑपरेंड में इस्तेमाल नहीं किया जा सकता, सिर्फ़ दोनों में से किसी एक में:
section .data
example4 dw 4
example5 dq -8
example6 dd 15
section .text
fn:
add word [example4], 5 ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
imul rax, qword [example5] ; rax = rax * (-8)
; this is not possible -> sub dword [example6], dword [example6]
हालाँकि किसी वेरिएबल का एड्रेस रजिस्टर में रखने के लिए mov का इस्तेमाल किया जा सकता है, लेकिन इसी खास काम के लिए एक इंस्ट्रक्शन मौजूद है: lea।
यह इंस्ट्रक्शन मेमोरी-फ़ॉर्म ऑपरेंड इस्तेमाल करता है, लेकिन मेमोरी को पढ़ता नहीं है। इसके बजाय यह इफ़ेक्टिव एड्रेस एक्सप्रेशन की गणना करता है और नतीजा डेस्टिनेशन ऑपरेंड में लिख देता है:
lea rax, [example] ; this stores the address of 'example' in rax
मेमोरी एड्रेस की गणना करके उन्हें रजिस्टर में रखने के लिए lea का इस्तेमाल करना ज़्यादा स्वाभाविक माना जाता है।
मेमोरी लोकेशन तक पहुँचते समय NASM का डिफ़ॉल्ट व्यवहार एब्सोल्यूट एड्रेस, यानी तय मेमोरी एड्रेस, बनाना होता है।
सुरक्षा कारणों से एक्ज़ीक्यूटेबल अक्सर PIE (Position Independent Executable) के रूप में बनाए जाते हैं, जिसमें मेमोरी क्षेत्रों को बेतरतीब जगहों पर रखा जाता है।
PIE में किसी वेरिएबल का अंतिम एड्रेस लिंक के समय पता नहीं होता।
इसलिए कोड एड्रेस की गणना rip नाम के एक खास रजिस्टर की वैल्यू से ऑफ़सेट के रूप में करता है, जो चलाए जाने वाले अगले इंस्ट्रक्शन की ओर इशारा करता है।
इसे आमतौर पर RIP-रिलेटिव एड्रेसिंग कहा जाता है।
NASM में आप rel ऑपरेटर से RIP-रिलेटिव एक्सेस माँग सकते हैं:
mov rax, qword [rel variable]
किसी सोर्स फ़ाइल के लिए सबसे ऊपर default rel लिखकर रिलेटिव एड्रेसिंग को डिफ़ॉल्ट भी बनाया जा सकता है।
इस ट्रैक के सभी अभ्यास PIE के रूप में कंपाइल और लिंक किए जाते हैं, इसलिए रिलेटिव एड्रेस बनाने के लिए rel का इस्तेमाल करना चाहिए।
किसी भी सेक्शन (जैसे .text, .data, .rodata) में परिभाषित लेबल (फंक्शन और डेटा) उसी सोर्स फ़ाइल के अंदर दिखाई देते हैं।
अगर उन्हें global घोषित किया जाए, तो वे दूसरी सोर्स फ़ाइलों को भी दिखाई देते हैं।
इसके उलट, दूसरी सोर्स फ़ाइलों में परिभाषित लेबल मौजूदा सोर्स फ़ाइल को तब दिखाई देते हैं जब उन्हें extern घोषित किया गया हो।
ऐसे में असेंबली में डेटा के साइज़ का कोई संकेत नहीं होता, यह पहले से पता होना चाहिए।
default rel
section .data
global number1 ; 'number1' is a variable visible to other source files
number1 db 200
extern number2 ; 'number2' is a variable visible to the current source file, but defined in another
section .text
extern sum ; sum is a function visible to the current source file, but defined in another
fn:
mov dil, byte [number1]
mov sil, byte [number2]
call sum
...
आपके दोस्त José एक स्थानीय स्कूल में शिक्षक हैं। उनके मन में कुछ मज़ेदार प्रयोग करने का विचार आया। इन प्रयोगों से दिखाया जा सकता है कि अलग-अलग रंगों को मिलाकर नए रंग कैसे बनाए जाते हैं।
उन्होंने इन प्रयोगों के लिए आपसे मदद माँगी।
इस अभ्यास में हर रंग को एक 32-बिट (4-बाइट) संख्या से दर्शाया जाता है, जो उसकी RGB वैल्यू एनकोड करती है।
एक RGB वैल्यू में 3 चैनल होते हैं, Red, Green और Blue, और हर चैनल 8 बिट (1 बाइट) की जगह लेता है।
चौथा बाइट आम तौर पर Alpha चैनल के लिए आरक्षित रहता है, लेकिन इस अभ्यास में उसकी वैल्यू खाली (0) रहेगी।
हर रंग की वैल्यू पहले से एक टेबल में संग्रहीत है, जो किसी दूसरी सोर्स फाइल में दी गई है। इस टेबल में हर रंग का अपना एक अलग एड्रेस होता है।
एक फंक्शन get_color_value बनाइए जो किसी रंग की 32-बिट वैल्यू लौटाता है।
यह फंक्शन पैरामीटर के रूप में कलर टेबल में इस रंग का मान्य एड्रेस लेता है।
get_color_value(black)
// => 0
संकेत - 32 बिट, 4 बाइट के बराबर होते हैं।
अलग-अलग रंगों को मिलाने के लिए José पहले एक बेस रंग तय करेंगे और उसके साथ मिलाए जाने वाले सेकंडरी रंग को ही बदलेंगे।
एक फंक्शन add_base_color बनाइए जो किसी रंग की 32-बिट वैल्यू को वेरिएबल base_color में संग्रहीत करता है, ताकि उसका इस्तेमाल बाद में किया जा सके।
यह फंक्शन कोई वैल्यू नहीं लौटाता और पैरामीटर के रूप में कलर टेबल में उस रंग का एड्रेस लेता है।
वेरिएबल base_color आपको खुद बनाना है, और वह ऐसा होना चाहिए जिसे दूसरी सोर्स फाइलें भी इस्तेमाल कर सकें।
एक ही समय में एक से ज़्यादा बेस रंग नहीं रहेगा। अगर नया बेस रंग जोड़ा जाता है, तो पुराना हटा दिया जाता है।
प्रोग्राम के शुरू में, डिफ़ॉल्ट रूप से base_color को white की 32-बिट वैल्यू से इनिशियलाइज़ करना चाहिए, जो 0xFFFFFF00 है।
संकेत - NASM 0x से शुरू होने वाली हेक्साडेसिमल संख्याएँ स्वीकार करता है, जैसे 0xFFFFFF00।
José प्राथमिक रंगों से कई संयोजन बनाने वाले हैं, इसलिए वे चाहते हैं कि इन रंगों को तेज़ी से इस्तेमाल के लिए अलग रखा जाए।
चूँकि वे रंगों को दर्शाने के लिए RGB का इस्तेमाल कर रहे हैं, प्राथमिक रंग ये हैं:
RED, जिसकी वैल्यू 0xFF000000 है।GREEN, जिसकी वैल्यू 0x00FF0000 है।BLUE, जिसकी वैल्यू 0x0000FF00 है।इनमें से हर रंग के लिए एक कॉन्स्टेंट बनाइए। इन कॉन्स्टेंट को दूसरी सोर्स फाइलें भी इस्तेमाल कर सकें, यह ज़रूरी है।
रंगों को उस combining_function के अनुसार मिलाया जाना चाहिए जो किसी दूसरी सोर्स फाइल में परिभाषित है।
यह फंक्शन पैरामीटर के रूप में base_color की और उसके साथ मिलाए जाने वाले सेकंडरी रंग की 32-बिट वैल्यू लेता है।
यह मिले हुए रंग की 32-बिट वैल्यू लौटाता है।
एक फंक्शन make_color_combination बनाइए जो दो रंगों को मिलाता है और परिणाम को मेमोरी में संग्रहीत करता है।
यह फंक्शन कोई वैल्यू नहीं लौटाता और पैरामीटर के रूप में, इसी क्रम में, ये लेता है:
ध्यान दीजिए कि combining_function उन रजिस्टरों की वैल्यू बदल सकता है जिनका इस्तेमाल आप कर रहे हैं।
फंक्शन को कॉल करने से पहले अपने काम का हर वेरिएबल मेमोरी में सहेज लें, यह पक्का कीजिए।
Exercism पर साइन अप कीजिए और x86-64 Assembly को 22 कॉन्सेप्ट130 अभ्यास तथा असली इंसानों से मिलने वाली मेंटरिंग के साथ सीखिए और उसमें महारत हासिल कीजिए, वह भी बिल्कुल मुफ्त।