फ़ंक्शन्स और स्टैक

फ़ंक्शन्स और स्टैक

एक फ़ंक्शन निर्देशों का एक नामित ब्लॉक है जिसे आप एक से अधिक बार उपयोग कर सकते हैं। आप इसमें जाते हैं, यह अपना कार्य करता है, और नियंत्रण उस स्थान पर वापस कर देता है जहाँ से आपने इसे कॉल किया था। यह आपको एक तर्क को एक बार लिखने और अपने पूरे प्रोग्राम में समान पंक्तियों को दोहराने के बजाय कई स्थानों से उपयोग करने की सुविधा देता है।

किसी फ़ंक्शन को कॉल करने के लिए का उपयोग करें: यह लेबल पर जाता है और वापसी स्थान को ra में संग्रहीत करता है। फ़ंक्शन के साथ समाप्त होता है, जो ra में दिए गए पते पर वापस जाता है। और स्यूडो-निर्देश हैं: सुविधाजनक नाम जिन्हें असेंबलर वास्तविक निर्देशों, और , में विस्तारित करता है।

आर्गुमेंट a0, a1, और उसके बाद के रजिस्टरों में रखे जाते हैं। वापसी मान a0 में रखा जाता है। यह व्यवस्था कॉलिंग कन्वेंशन है, वह साझा समझौता जो अलग-अलग कोड के टुकड़ों को एक साथ काम करने देता है।

एक square() फ़ंक्शन
li a0, 6 # आर्गुमेंट
call square # कॉल करें, और ra याद रखता है कि कहाँ वापस आना है
li a7, 1 # square अपना परिणाम a0 में छोड़ता है
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # ra पर वापस जाता है

कॉल का अनुसरण करें। आर्गुमेंट 6 को a0 में रखता है। square लेबल पर जाता है और वापसी पता ra में संग्रहीत करता है। फ़ंक्शन के अंदर, 6 * 6 = 36 की गणना करता है और परिणाम को a0 में छोड़ देता है, जो वापसी मानों के लिए उपयोग किया जाने वाला रजिस्टर है। कॉल के ठीक बाद वाले निर्देश पर वापस जाता है, जहाँ और 36 प्रिंट करते हैं, और फिर प्रोग्राम समाप्त हो जाता है।

फ़ंक्शन कॉल और रिटर्न
कॉलर
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

कॉल करने वाला A को कॉल करने वाला है।

हाइलाइट की गई लाइन को पूरा राउंड ट्रिप करते हुए देखें। caller निष्पादित करता है, जो स्वचालित रूप से रिटर्न पॉइंट को ra रजिस्टर में caller पर सहेजता है। इससे पहले कि A निष्पादित कर सके, उसे एक समस्या का सामना करना पड़ता है: निर्देश ra को एक नए रिटर्न एड्रेस से अधिलेखित करने वाला है, जिससे वह एड्रेस नष्ट हो जाएगा जिसकी A को वापस घर लौटने के लिए आवश्यकता है। इसे ठीक करने के लिए, A स्टैक पर जगह बनाता है () और मूल ra को वहाँ सहेजता है ()। अब करना सुरक्षित है। B चलता है और एक साधारण के साथ लौटता है, वापस A के अंदर पहुँचता है। अंत में, A स्टैक से मूल ra को पुनर्स्थापित करता है (), स्टैक पॉइंटर को साफ करता है (), और इसका अपना उस पुनर्स्थापित मान का उपयोग करके सफलतापूर्वक caller पर वापस कूदता है। यदि A इस चरण को छोड़ देता, तो निर्देश ने ra को स्थायी रूप से अधिलेखित कर दिया होता, और A का अंतिम घर लौटने के बजाय गलत स्थान पर कूद जाता।

स्टैक

केवल कुछ ही रजिस्टर मौजूद हैं, इसलिए जो फ़ंक्शन अन्य फ़ंक्शन को कॉल करते हैं उन्हें स्क्रैच स्पेस की आवश्यकता होती है। स्टैक वह मेमोरी है जिसे आप स्टैक पॉइंटर sp के माध्यम से उधार लेते हैं: जगह बनाने के लिए घटाएँ, जो कुछ आपको रखना है उसे स्टोर करें, और से पहले इसे पुनर्स्थापित करें।

स्टैक एक लास्ट-इन, फर्स्ट-आउट (LIFO) संरचना है: सबसे हाल ही में संग्रहीत मान पहला होता है जिसे आप प्राप्त करते हैं। RISC-V में, स्टैक मेमोरी के माध्यम से नीचे की ओर बढ़ता है। sp से घटाना नई जगह आरक्षित करता है जिसका उपयोग करना सुरक्षित है। जब आप मानों को वापस निकालते हैं, तो आपको उन्हें उसी उल्टे क्रम में करना होगा जिस क्रम में आपने उन्हें संग्रहीत किया था, अन्यथा प्रोग्राम का निष्पादन प्रवाह खराब हो जाएगा।

एक कॉल के दौरान ra को सहेजें और पुनर्स्थापित करें
addi sp, sp, -4 # एक वर्ड के लिए जगह बनाता है
sw ra, 0(sp) # वापसी का पता सहेजता है
# ... यहाँ अन्य फ़ंक्शन कॉल करें ...
lw ra, 0(sp) # उसे पुनर्स्थापित करता है
addi sp, sp, 4 # वह जगह वापस देता है
ret

प्रत्येक / ra को अधिलेखित करता है, इसलिए यदि आपका फ़ंक्शन किसी अन्य फ़ंक्शन को कॉल करता है, तो आंतरिक कॉल आपके वापसी पते को नष्ट कर देता है और आपका स्वयं का गलत स्थान पर जाता है। इसे रोकने के लिए, स्टैक पर 4 बाइट आरक्षित करता है, और ra को वहाँ सहेजता है। आंतरिक कॉल समाप्त होने के बाद, वापसी पते को पुनर्स्थापित करता है, और से पहले जगह वापस दे देता है।

⚠
यदि आपने कुछ भी कॉल किया है, तो से पहले ra को पुनर्स्थापित करें, और हमेशा sp को उस मान पर लौटाएँ जो शुरुआत में था। एक स्टैक जो अपनी प्रारंभिक स्थिति से मेल नहीं खाता, असेंबली प्रोग्रामों में क्रैश का सामान्य कारण है।