कॉलस्टैक कैसे ठीक काम करता है?


103

मैं यह समझने की कोशिश कर रहा हूं कि प्रोग्रामिंग भाषाओं के निम्न स्तर के संचालन कैसे काम करते हैं और विशेष रूप से वे ओएस / सीपीयू के साथ कैसे बातचीत करते हैं। मैंने शायद स्टैक ओवरफ्लो पर यहां हर स्टैक / हीप संबंधित धागे में हर उत्तर पढ़ा है, और वे सभी शानदार हैं। लेकिन अभी भी एक बात है जो मुझे अभी तक पूरी तरह से समझ नहीं आई है।

छद्म कोड में इस फ़ंक्शन पर विचार करें जो मान्य रस्ट कोड हो ;-)

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(a, b);
    doAnotherThing(c, d);
}

इस तरह से मैं लाइन एक्स की तरह दिखने के लिए स्टैक मान लेता हूं:

Stack

a +-------------+
  | 1           | 
b +-------------+     
  | 2           |  
c +-------------+
  | 3           | 
d +-------------+     
  | 4           | 
  +-------------+ 

अब, सब कुछ मैंने पढ़ा है कि स्टैक कैसे काम करता है, यह सख्ती से LIFO नियमों का पालन करता है (अंतिम में, पहले बाहर)। .NET, जावा या किसी अन्य प्रोग्रामिंग भाषा में स्टैक डेटाटाइप की तरह।

लेकिन अगर ऐसा है, तो लाइन X के बाद क्या होगा? क्योंकि जाहिर है, अगली बात हम जरूरत के साथ काम करने के लिए है aऔर b, लेकिन इसका मतलब यह होगा कि कि OS / सीपीयू (?) पॉप आउट करना है dऔर cपहले करने के लिए वापस पाने के लिए aऔर b। लेकिन तब यह खुद को पैर में गोली मार लेता था, क्योंकि इसकी जरूरत होती है cऔर dअगली पंक्ति में।

तो, मुझे आश्चर्य है कि वास्तव में पर्दे के पीछे क्या होता है?

एक और संबंधित प्रश्न। विचार करें कि हम इस तरह के अन्य कार्यों में से एक का संदर्भ देते हैं:

fn foo() {
    let a = 1;
    let b = 2;
    let c = 3;
    let d = 4;

    // line X

    doSomething(&a, &b);
    doAnotherThing(c, d);
}

मैं चीजों को कैसे समझता हूं, इसका मतलब यह होगा कि पैरामीटर doSomethingअनिवार्य रूप से उसी मेमोरी पते की ओर इशारा कर रहे हैं जैसे कि aऔर bअंदर foo। लेकिन तब फिर से इसका मतलब है कि स्टैक तकab कोई पॉप नहीं है, जब तक हम नहीं होते हैं और हो रहे हैं।

उन दो मामलों से मुझे लगता है कि मैंने पूरी तरह से समझा नहीं है कि स्टैक कैसे काम करता है और यह कैसे पूरी तरह से LIFO नियमों का पालन करता है।


14
LIFO केवल स्टैक पर जगह को बचाने के लिए मायने रखता है। आप हमेशा किसी भी चर का उपयोग कर सकते हैं जो कम से कम आपके स्टैक फ्रेम (फ़ंक्शन के अंदर घोषित) पर है, भले ही वह बहुत सारे अन्य चर के तहत हो
VoidStar

2
दूसरे शब्दों में, LIFOइसका मतलब है कि आप केवल स्टैक के अंत में तत्वों को जोड़ या हटा सकते हैं, और आप किसी भी तत्व को हमेशा पढ़ / बदल सकते हैं।
पवित्रब्लैकट

12
-O0 के संकलन के बाद आप एक साधारण कार्य क्यों नहीं करते हैं और उत्पन्न निर्देशों को देखें? यह सुंदर, अच्छी तरह से, शिक्षाप्रद ;-) है। आप पाएंगे कि कोड रैम के आर हिस्से का अच्छा उपयोग करता है; यह सीधे वसीयत में पते तक पहुंचता है। आप एक चर नाम के लिए एक पते रजिस्टर (स्टैक पॉइंटर) के ऑफसेट के रूप में सोच सकते हैं। जैसा कि दूसरों ने कहा, स्टैकिंग के संबंध में स्टैक सिर्फ एलआईएफओ है (पुनरावर्तन के लिए अच्छा है)। इसे एक्सेस करने के संबंध में यह LIFO नहीं है। पहुंच पूरी तरह से यादृच्छिक है।
पीटर -

6
आप एक सरणी का उपयोग करके अपनी खुद की स्टैक डेटा संरचना बना सकते हैं, और बस शीर्ष तत्व के सूचकांक को स्टोर कर सकते हैं, जब आप धक्का देते हैं तो इसे बढ़ाते हैं, जब आप पॉप करते हैं तो इसे घटाते हैं। यदि आपने ऐसा किया है, तब भी आप किसी भी समय किसी भी व्यक्तिगत तत्व को किसी भी समय पुश या पॉपिंग के बिना एक्सेस कर पाएंगे, ठीक वैसे ही जैसे आप हमेशा एरेज़ के साथ कर सकते हैं। लगभग यही हाल यहाँ हो रहा है।
क्रोमन

3
मूल रूप से, स्टैक / हीप का नामकरण दुर्भाग्यपूर्ण है। वे डेटा संरचनाओं की शब्दावली में ढेर और ढेर करने के लिए बहुत कम समानता रखते हैं, इसलिए उन्हें एक ही कॉल करना बहुत भ्रामक है।
सियुआन रेन

जवाबों:


117

कॉल स्टैक को फ़्रेम स्टैक भी कहा जा सकता है। LIFO सिद्धांत के बाद
जो चीजें स्टैक्ड होती हैं, वे स्थानीय वैरिएबल नहीं होते हैं, बल्कि फंक्शंस के पूरे स्टैक फ्रेम ("कॉल") होते हैं । स्थानीय चर क्रमश : तथाकथित फ़ंक्शन प्रोलॉग और उपसंहार में उन फ़्रेमों के साथ एक साथ धकेल दिए जाते हैं।

फ़्रेम के अंदर चर का क्रम पूरी तरह से अनिर्दिष्ट है; कंपाइलर एक फ्रेम के अंदर स्थानीय चरों की स्थिति को " संरेखित" करते हैं, ताकि उनके संरेखण को अनुकूलित किया जा सके ताकि प्रोसेसर उन्हें जल्दी से जल्दी प्राप्त कर सके। महत्वपूर्ण तथ्य यह है कि कुछ निश्चित पते के सापेक्ष चरों की ऑफसेट पूरे जीवनकाल के दौरान स्थिर रहती है - इसलिए यह एक लंगर का पता लेने के लिए पर्याप्त है, कहते हैं, फ़्रेम का पता, और उस पते के ऑफसेट के साथ काम करना चर। ऐसा लंगर पता वास्तव में तथाकथित आधार या फ़्रेम पॉइंटर में निहित हैजिसे EBP रजिस्टर में संग्रहीत किया जाता है। दूसरी ओर, ऑफसेट को स्पष्ट रूप से संकलन समय पर जाना जाता है और इसलिए इसे मशीन कोड में हार्डकोड किया जाता है।

विकिपीडिया का यह ग्राफिक दिखाता है कि विशिष्ट कॉल स्टैक 1 की तरह संरचित है :

एक ढेर की तस्वीर

एक चर की ऑफसेट जोड़ें जो हम फ्रेम पॉइंटर में निहित पते तक पहुंचना चाहते हैं और हमें अपने चर का पता मिलता है। इसलिए जल्द ही कहा गया, कोड बस आधार सूचक से निरंतर संकलन-समय के ऑफसेट के माध्यम से सीधे उन तक पहुंचता है; यह सरल सूचक अंकगणित है।

उदाहरण

#include <iostream>

int main()
{
    char c = std::cin.get();
    std::cout << c;
}

gcc.godbolt.org हमें देता है

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp

    movl    std::cin, %edi
    call    std::basic_istream<char, std::char_traits<char> >::get()
    movb    %al, -1(%rbp)
    movsbl  -1(%rbp), %eax
    movl    %eax, %esi
    movl    std::cout, %edi
    call    [... the insertion operator for char, long thing... ]

    movl    $0, %eax
    leave
    ret

.. के लिए main। मैंने कोड को तीन उपखंडों में विभाजित किया है। फ़ंक्शन प्रस्तावना में पहले तीन ऑपरेशन शामिल हैं:

  • बेस पॉइंटर को स्टैक पर धकेल दिया जाता है।
  • स्टैक पॉइंटर को बेस पॉइंटर में सेव किया जाता है
  • स्थानीय चर के लिए जगह बनाने के लिए स्टैक पॉइंटर को घटाया जाता है।

फिर cinईडीआई रजिस्टर 2 में स्थानांतरित किया getजाता है और कहा जाता है; वापसी मूल्य EAX में है।

अब तक सब ठीक है। अब दिलचस्प बात यह है:

EAX का निम्न-क्रम बाइट, जिसे 8-बिट रजिस्टर AL द्वारा निर्दिष्ट किया जाता है, को आधार सूचक के ठीक बाद बाइट में लिया और संग्रहीत किया जाता है : अर्थात -1(%rbp), आधार सूचक की ऑफसेट है -1यह बाइट हमारा परिवर्तनशील हैc । ऑफसेट नकारात्मक है क्योंकि स्टैक x86 पर नीचे की ओर बढ़ता है। अगले आपरेशन भंडार cEAX में: EAX ईएसआई के लिए ले जाया जाता है, coutईडीआई में ले जाया गया और साथ तो प्रविष्टि ऑपरेटर कहा जाता है coutऔर cतर्क किया जा रहा है।

आखिरकार,

  • का रिटर्न मान mainEAX में संग्रहीत किया जाता है: 0. यह निहित returnकथन के कारण है। xorl rax raxइसके बजाय आप भी देख सकते हैं movl
  • कॉल साइट पर छोड़ें और वापस लौटें। leaveइस उपसंहार और संक्षेप में संक्षिप्त है
    • बेस पॉइंटर के साथ स्टैक पॉइंटर को बदल देता है और
    • बेस पॉइंटर को पॉप करता है।

इस ऑपरेशन के बाद और retप्रदर्शन किया गया है, फ्रेम प्रभावी रूप से पॉप हो गया है, हालांकि कॉलर को अभी भी तर्कों को साफ करना है क्योंकि हम cdecl कॉलिंग कन्वेंशन का उपयोग कर रहे हैं। अन्य सम्मेलनों, जैसे stdcall, को शांत करने के लिए कैली की आवश्यकता होती है, उदाहरण के लिए बाइट्स की मात्रा को पार करके ret

फ़्रेम सूचक प्रवेश

आधार / फ्रेम पॉइंटर से बल्कि स्टैक पॉइंटर (ईएसबी) से ऑफसेट का उपयोग नहीं करना भी संभव है। यह ईबीपी-रजिस्टर बनाता है जिसमें अन्यथा मनमाना उपयोग के लिए फ्रेम पॉइंटर मूल्य उपलब्ध होगा - लेकिन यह कुछ मशीनों पर डिबगिंग को असंभव बना सकता है , और कुछ कार्यों के लिए इसे बंद कर दिया जाएगा । यह विशेष रूप से उपयोगी है जब केवल कुछ रजिस्टरों के साथ प्रोसेसर के लिए संकलन होता है, जिसमें x86 भी शामिल है।

इस अनुकूलन को एफपीओ (फ्रेम सूचक चूक) के रूप में जाना जाता है और इसे -fomit-frame-pointerजीसीसी और -Oyक्लैंग में स्थापित किया जाता है ; ध्यान दें कि यह अनुमानित रूप से प्रत्येक अनुकूलन स्तर> 0 से होता है और यदि केवल डिबगिंग अभी भी संभव है, क्योंकि इसके अलावा कोई लागत नहीं है। अधिक जानकारी के लिए यहां और यहां देखें ।


1 जैसा कि टिप्पणियों में कहा गया है, फ़्रेम पॉइंटर संभवतः रिटर्न एड्रेस के बाद पते को इंगित करने के लिए है।

2 ध्यान दें कि आर के साथ शुरू होने वाले रजिस्टर ई के साथ शुरू होने वाले 64-बिट प्रतिरूप हैं। EAX RAX के चार कम-क्रम बाइट्स को नामित करता है। मैंने स्पष्टता के लिए 32-बिट रजिस्टर के नामों का उपयोग किया।


1
बहुत बढ़िया जवाब। ऑफसेट द्वारा डेटा को संबोधित करने वाली बात मेरे लिए गायब थी :)
क्रिस्टोफ

1
मुझे लगता है कि ड्राइंग में एक छोटी सी गलती है। फ़्रेम पॉइंटर को रिटर्न एड्रेस के दूसरी तरफ होना चाहिए। एक फ़ंक्शन को छोड़ना आमतौर पर निम्नानुसार किया जाता है: स्टैक पॉइंटर को फ़्रेम पॉइंटर पर ले जाएं, स्टैक से कॉलर्स फ़्रेम पॉइंटर को पॉप करें, रिटर्न (यानी स्टैक से कॉलर प्रोग्राम काउंटर / इंस्ट्रक्शन पॉइंटर पॉप करें।)
कैस्पर

कैस्परल्ड बिल्कुल सही है। आप या तो फ्रेम पॉइंटर का उपयोग बिल्कुल नहीं करते हैं (मान्य अनुकूलन और विशेष रूप से रजिस्टर-स्टार्चर्ड आर्किटेक्चर जैसे कि x86 बेहद उपयोगी) के लिए या आप इसका उपयोग करते हैं और स्टैक पर पिछले एक को स्टोर करते हैं - आमतौर पर वापसी पते के ठीक बाद। फ्रेम को कैसे स्थापित किया जाता है और हटाया जाता है यह वास्तुकला और एबीआई पर बहुत हद तक निर्भर करता है। काफी कुछ आर्किटेक्चर (हैलो इटेनियम) हैं जहां पूरी बात है .. और अधिक दिलचस्प (और आकार तर्क सूची जैसी चीजें हैं!)
वू

3
@Christoph मुझे लगता है कि आप इसे वैचारिक दृष्टिकोण से देख रहे हैं। यहां एक टिप्पणी है जो इसे स्पष्ट रूप से स्पष्ट करेगी - आरटीएस, या रनटाइम स्टैक, अन्य स्टैक से थोड़ा अलग है, जिसमें यह "गंदी स्टैक" है - वास्तव में कुछ भी नहीं है जो आपको उस मूल्य को देखने से रोक रहा है जो isn ' शीर्ष पर टी। ध्यान दें कि आरेख में, हरे विधि के लिए "वापसी पता" - जो नीले विधि द्वारा आवश्यक है! मापदंडों के बाद है। पिछले फ्रेम के पॉप होने के बाद नीली पद्धति को रिटर्न वैल्यू कैसे मिलती है? ठीक है, यह एक गंदा ढेर है, इसलिए यह बस इसमें पहुंच सकता है और इसे पकड़ सकता है।
6

1
फ़्रेम पॉइंटर की वास्तव में आवश्यकता नहीं है, क्योंकि स्टैक पॉइंटर से हमेशा ऑफसेट का उपयोग किया जा सकता है। जीसीसी डिफ़ॉल्ट रूप से x64 आर्किटेक्चर को लक्षित करता है, स्टैक पॉइंटर का उपयोग करता है, और rbpअन्य काम करने के लिए मुक्त करता है।
सियुआन रेन

27

क्योंकि जाहिर है, अगली चीज़ जो हमें चाहिए वह है a और b के साथ काम करना लेकिन इसका मतलब यह होगा कि OS / CPU (?) को d और c को पहले पॉप करना होगा और a और b में वापस लाना होगा। लेकिन तब यह अपने आप पैर में गोली मार लेता था क्योंकि इसे अगली पंक्ति में c और d की आवश्यकता होती थी।

संक्षेप में:

तर्कों को पॉप करने की आवश्यकता नहीं है। fooफ़ंक्शन के लिए कॉलर द्वारा पास किए गए तर्क doSomethingऔर स्थानीय चर doSomething सभी को आधार सूचक से ऑफसेट के रूप में संदर्भित किया जा सकता है
इसलिए,

  • जब कोई फ़ंक्शन कॉल किया जाता है, तो फ़ंक्शन के तर्क स्टैक पर PUSHed होते हैं। इन तर्कों को आधार सूचक द्वारा संदर्भित किया जाता है।
  • जब फ़ंक्शन अपने कॉलर पर लौटता है, तो LIFO विधि का उपयोग करके स्टैक से रिटर्निंग फ़ंक्शन के तर्क प्रस्तुत किए जाते हैं।

विस्तार से:

नियम यह है कि प्रत्येक फ़ंक्शन कॉल स्टैक फ्रेम के निर्माण में होता है (न्यूनतम पते पर लौटने के लिए)। तो, अगर funcAकॉल funcBऔर funcBकॉल funcC, तीन स्टैक फ्रेम एक दूसरे के ऊपर स्थापित किए जाते हैं। जब कोई फ़ंक्शन वापस आता है, तो उसका फ्रेम अमान्य हो जाता है । एक अच्छी तरह से व्यवहार किया गया फ़ंक्शन केवल अपने स्वयं के स्टैक फ्रेम पर कार्य करता है और किसी अन्य पर अतिचार नहीं करता है। दूसरे शब्दों में POPing शीर्ष पर स्टैक फ्रेम के लिए किया जाता है (जब फ़ंक्शन से लौटते हुए)।

यहां छवि विवरण दर्ज करें

आपके प्रश्न में स्टैक कॉलर द्वारा सेटअप किया गया है foo। जब doSomethingऔर doAnotherThingकहा जाता है, तो वे अपने स्वयं के ढेर को सेटअप करते हैं। आंकड़ा आपको यह समझने में मदद कर सकता है:

यहां छवि विवरण दर्ज करें

ध्यान दें कि, तर्कों का उपयोग करने के लिए, फ़ंक्शन बॉडी को उस स्थान से नीचे (उच्च पते) पर लौटना होगा, जहां से वापसी पता संग्रहीत है, और स्थानीय चर तक पहुंचने के लिए, फ़ंक्शन बॉडी को स्टैक को कम करना होगा (निचले पते ) उस स्थान के सापेक्ष जहां रिटर्न पता संग्रहीत है। वास्तव में, फ़ंक्शन के लिए विशिष्ट संकलक उत्पन्न कोड ठीक यही करेगा। संकलक इस (बेस पॉइंटर) के लिए EBP नामक एक रजिस्टर समर्पित करता है। उसी का एक और नाम फ्रेम पॉइंटर है। कंपाइलर आमतौर पर, फ़ंक्शन बॉडी के लिए पहली चीज़ के रूप में, मौजूदा EBP मान को स्टैक पर धकेलता है और EBP को वर्तमान ESP पर सेट करता है। इसका मतलब है, एक बार ऐसा करने के बाद, फ़ंक्शन कोड के किसी भी भाग में, तर्क 1 EBP + 8 दूर है (कॉलर के प्रत्येक EBP और वापसी पते के लिए 4 बाइट्स), तर्क 2 EBP + 12 (दशमलव) दूर है, स्थानीय चर EBP-4n दूर हैं।

.
.
.
[ebp - 4]  (1st local variable)
[ebp]      (old ebp value)
[ebp + 4]  (return address)
[ebp + 8]  (1st argument)
[ebp + 12] (2nd argument)
[ebp + 16] (3rd function argument) 

फ़ंक्शन के स्टैक फ्रेम के गठन के लिए निम्नलिखित सी कोड पर एक नज़र डालें:

void MyFunction(int x, int y, int z)
{
     int a, int b, int c;
     ...
}

जब फोन करने वाला इसे बुलाता है

MyFunction(10, 5, 2);  

निम्नलिखित कोड उत्पन्न किया जाएगा

^
| call _MyFunction  ; Equivalent to: 
|                   ; push eip + 2
|                   ; jmp _MyFunction
| push 2            ; Push first argument  
| push 5            ; Push second argument  
| push 10           ; Push third argument  

और समारोह के लिए विधानसभा कोड होगा (लौटने से पहले कैली द्वारा सेट अप)

^
| _MyFunction:
|  sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
|  ;x = [ebp + 8], y = [ebp + 12], z = [ebp + 16]
|  ;a = [ebp - 4] = [esp + 8], b = [ebp - 8] = [esp + 4], c = [ebp - 12] =   [esp]
|  mov ebp, esp
|  push ebp
 

संदर्भ:


1
आपके उत्तर के लिए धन्यवाद। इसके अलावा लिंक वास्तव में शांत हैं और मुझे कंप्यूटर को वास्तव में कैसे काम करते हैं, इस सवाल के कभी खत्म न होने के लिए और अधिक प्रकाश डालने में मदद करते हैं :)
क्रिस्टोफ

आपको क्या मतलब है "स्टैक पर वर्तमान ईबीपी मान को धक्का देता है" और स्टैक पॉइंटर को रजिस्टर में संग्रहीत किया जाता है या वह भी स्टैक में स्थिति रखता है ... मैं थोड़ा भ्रमित हूं
सूरज जैन

और यह नहीं होना चाहिए * [ebp + 8] नहीं [ebp + 8]।]
सूरज जैन

@ सूरज जैन; क्या आप जानते हैं क्या है EBPऔर ESP?
हॉक

esp स्टैक पॉइंटर है और ईबप बेस पॉइंटर है। अगर मुझे कुछ जानकारी नहीं है, तो कृपया इसे ठीक करें।
सूरज जैन

19

अन्य लोगों की तरह, जब तक वे दायरे से बाहर नहीं जाते, तब तक पॉप पैरामीटर की आवश्यकता नहीं होती है।

मैं निक पॉर्लेंट द्वारा "पॉइंटर्स एंड मेमोरी" से कुछ उदाहरण पेस्ट करूंगा। मुझे लगता है कि आपके द्वारा कल्पना किए जाने से स्थिति थोड़ी अधिक सरल है।

यहाँ कोड है:

void X() 
{
  int a = 1;
  int b = 2;

  // T1
  Y(a);

  // T3
  Y(b);

  // T5
}

void Y(int p) 
{
  int q;
  q = p + 2;
  // T2 (first time through), T4 (second time through)
}

समय में अंक T1, T2, etc। उस समय कोड और मेमोरी की स्थिति को ड्राइंग में दिखाया गया है:

यहां छवि विवरण दर्ज करें


2
महान दृश्य व्याख्या। मैंने गुगली की और यहाँ कागज पाया: cslibrary.stanford.edu/102/PointersAndMemory.pdf वास्तव में उपयोगी कागज!
क्रिस्टोफ

7

विभिन्न प्रोसेसर और भाषाएं कुछ अलग स्टैक डिज़ाइन का उपयोग करती हैं। 8x86 और 68000 दोनों पर दो पारंपरिक पैटर्न को पास्कल कॉलिंग कन्वेंशन और सी कॉलिंग कन्वेंशन कहा जाता है; प्रत्येक कन्वेंशन को रजिस्टरों के नाम को छोड़कर दोनों प्रोसेसरों में उसी तरह से हैंडल किया जाता है। प्रत्येक स्टैक और संबंधित चर का प्रबंधन करने के लिए दो रजिस्टरों का उपयोग करता है, जिसे स्टैक पॉइंटर (एसपी या ए 7) और फ्रेम पॉइंटर (बीपी या ए 6) कहा जाता है।

जब या तो कन्वेंशन का उपयोग करके सबरूटीन को कॉल किया जाता है, तो किसी भी पैरामीटर को रूटीन पर कॉल करने से पहले स्टैक पर धकेल दिया जाता है। दिनचर्या का कोड तब स्टैक पर फ़्रेम पॉइंटर के वर्तमान मूल्य को धक्का देता है, स्टैक पॉइंटर के वर्तमान मूल्य को फ़्रेम पॉइंटर पर कॉपी करता है, और स्टैक पॉइंटर से घटाकर स्थानीय चर [यदि कोई हो] का उपयोग किया जाता है। एक बार जो किया जाता है, भले ही अतिरिक्त डेटा को स्टैक पर धकेल दिया जाता है, सभी स्थानीय चर स्टैक पॉइंटर से निरंतर नकारात्मक विस्थापन के साथ चर पर संग्रहीत किए जाएंगे, और कॉलर द्वारा स्टैक पर धकेल दिए गए सभी मापदंडों को एक्सेस किया जा सकता है। फ़्रेम पॉइंटर से निरंतर सकारात्मक विस्थापन।

दो सम्मेलनों के बीच का अंतर उस तरह से निहित है जैसे वे सबरूटीन से बाहर निकलने से संभालते हैं। सी कन्वेंशन में, रिटर्निंग फंक्शन फ्रेम पॉइंटर को स्टैक पॉइंटर पर कॉपी करता है [पुराने फ्रेम पॉइंटर को पुश करने के ठीक बाद इसे वैल्यू पर रीस्टोर करता है], पुराने फ्रेम पॉइंटर वैल्यू को पॉप करता है, और रिटर्न करता है। कॉल करने से पहले कॉल करने वाले किसी भी पैरामीटर को स्टैक पर धकेल दिया था। पास्कल कन्वेंशन में, पुराने फ्रेम पॉइंटर को पॉप करने के बाद, प्रोसेसर फंक्शन रिटर्न एड्रेस को पॉप करता है, स्टैक पॉइंटर को कॉल करने वाले द्वारा पुश किए गए मापदंडों के बाइट्स की संख्या में जोड़ता है, और फिर पॉप्डेड रिटर्न एड्रेस पर जाता है। मूल 68000 पर कॉलर के मापदंडों को हटाने के लिए 3-अनुदेश अनुक्रम का उपयोग करना आवश्यक था; मूल के बाद 8x86 और सभी 680x0 प्रोसेसर में "रिट एन" शामिल था।

पास्कल कन्वेंशन में कॉलर साइड पर थोड़ा सा कोड सेव करने का फायदा है, क्योंकि कॉल करने वाले को फ़ंक्शन कॉल के बाद स्टैक पॉइंटर को अपडेट नहीं करना पड़ता है। हालांकि, यह आवश्यक है कि कॉल किया जाने वाला फ़ंक्शन ठीक-ठीक जानता हो कि कॉल करने वाले के पास कितने बाइट्स के कितने पुर्जे हैं जो स्टैक पर रखे जा रहे हैं। एक फ़ंक्शन को कॉल करने से पहले स्टैक पर मापदंडों की उचित संख्या को पुश करने में विफल होना जो पास्कल सम्मेलन का उपयोग करता है लगभग एक दुर्घटना का कारण बनने की गारंटी है। यह ऑफसेट है, हालांकि, इस तथ्य से कि प्रत्येक नामक विधि के भीतर थोड़ा अतिरिक्त कोड उन स्थानों पर कोड को बचाएगा जहां विधि कहा जाता है। इस कारण से, अधिकांश मूल Macintosh टूलबॉक्स दिनचर्या ने पास्कल कॉलिंग कन्वेंशन का उपयोग किया।

सी कॉलिंग कन्वेंशन में रूटीन को मापदंडों की एक परिवर्तनीय संख्या को स्वीकार करने की अनुमति देने का लाभ है, और मजबूत होने के बावजूद अगर कोई रूट पास किए गए सभी मापदंडों का उपयोग नहीं करता है (कॉल करने वाले को पता चल जाएगा कि कितने बाइट्स मापदंडों के लायक हैं, और इस प्रकार उन्हें साफ करने में सक्षम हो जाएगा)। इसके अलावा, हर फ़ंक्शन कॉल के बाद स्टैक क्लीनअप करना आवश्यक नहीं है। यदि कोई रूटीन चार कार्यों को क्रम में बुलाता है, जिनमें से प्रत्येक चार बाइट्स के मापदंडों का उपयोग करता है, तो यह हो सकता है - ADD SP,4प्रत्येक कॉल के बाद उपयोग करने के बजाय , ADD SP,16सभी चार कॉल से मापदंडों को साफ करने के लिए अंतिम कॉल के बाद एक का उपयोग करें।

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


1
वाह! क्या मैं एक हफ्ते के लिए आपके दिमाग को उधार ले सकता हूं। कुछ नटखट किरकिरा सामान निकालने की जरूरत है! बहुत बढ़िया जवाब!
क्रिस्टोफ

फ्रेम और स्टैक पॉइंटर को स्टैक में ही या कहीं और कहाँ संग्रहीत किया जाता है?
सूरज जैन

@ सुरजजैन: आमतौर पर, फ़्रेम पॉइंटर की प्रत्येक सहेजी गई प्रतिलिपि नए फ़्रेम पॉइंटर मान के सापेक्ष एक निश्चित विस्थापन पर संग्रहीत की जाएगी।
सुपरकैट

महोदय, मुझे यह संदेह लंबे समय से हो रहा है। अगर मेरे फंक्शन में मैं लिखता हूं (g==4)तो अगर मैं इनपुट लेता हूं int d = 3और उसके बाद मैं दूसरे वेरिएबल को परिभाषित करता हूं । अब, संकलक अब स्टैक में जगह कैसे देता है । ऑफ़सेट कैसे किया जाता है क्योंकि यदि नहीं है , तो स्टैक में d के लिए कोई मेमोरी नहीं होगी और बस ऑफ़सेट दिया जाएगा और यदि ऑफ़सेट पहले g के लिए होगा और फिर के लिए । कंपाइलर कंपाइल समय पर कैसे करता है, यह हमारे इनपुट के लिए नहीं पता हैgscanfint h = 5d = 3g4hg == 4hg
सूरज जैन

@ सूरजजैन: सी के शुरुआती संस्करणों के लिए आवश्यक है कि किसी भी निष्पादन योग्य विवरणों से पहले एक फ़ंक्शन के भीतर सभी स्वचालित चर दिखाई दें। उस जटिल संकलन को थोड़ा आराम करना, लेकिन एक दृष्टिकोण एक फ़ंक्शन की शुरुआत में कोड उत्पन्न करना है जो एसपी से एक आगे घोषित लेबल के मूल्य को घटाता है। फ़ंक्शन के भीतर, कोड में प्रत्येक बिंदु पर कंपाइलर ट्रैक कर सकता है कि स्थानीय लोगों के कितने बाइट्स अभी भी स्कोप में हैं और साथ ही उन बाइट्स की अधिकतम संख्या को भी ट्रैक करें जो कभी स्कोप में हों। फ़ंक्शन के अंत में, यह पहले के लिए मूल्य की आपूर्ति कर सकता है ...
सुपरकैट

5

यहां पहले से ही कुछ बहुत अच्छे जवाब हैं। हालांकि, यदि आप अभी भी स्टैक के LIFO व्यवहार के बारे में चिंतित हैं, तो इसे वैरिएबल के ढेर के बजाय फ़्रेम के ढेर के रूप में सोचें। मेरा सुझाव देने का मतलब यह है कि यद्यपि एक फ़ंक्शन चर के शीर्ष पर नहीं है, जो चर तक पहुंच सकता है, यह अभी भी केवल स्टैक के शीर्ष पर आइटम पर काम कर रहा है: एकल स्टैक फ्रेम।

बेशक, इसके अपवाद हैं। संपूर्ण कॉल श्रृंखला के स्थानीय चर अभी भी आवंटित और उपलब्ध हैं। लेकिन वे सीधे पहुँचा नहीं जा सकेगा। इसके बजाय, उन्हें संदर्भ (या सूचक द्वारा, जो वास्तव में केवल शब्दार्थ से भिन्न है) द्वारा पारित किया जाता है। इस स्थिति में स्टैक फ्रेम के एक स्थानीय चर को और नीचे तक पहुँचा जा सकता है। लेकिन इस मामले में भी, वर्तमान में निष्पादित कार्य अभी भी केवल अपने स्थानीय डेटा पर काम कर रहा है। यह अपने स्वयं के स्टैक फ्रेम में संग्रहीत एक संदर्भ को एक्सेस कर रहा है, जो ढेर पर किसी चीज का संदर्भ हो सकता है, स्थिर मेमोरी में, या स्टैक के नीचे।

यह स्टैक एब्स्ट्रैक्शन का हिस्सा है जो किसी भी क्रम में कार्यों को कॉल करने योग्य बनाता है, और पुनरावृत्ति की अनुमति देता है। शीर्ष स्टैक फ़्रेम एकमात्र ऑब्जेक्ट है जो सीधे कोड द्वारा एक्सेस किया जाता है। कुछ भी अप्रत्यक्ष रूप से एक्सेस किया जाता है (एक पॉइंटर के माध्यम से जो शीर्ष स्टैक फ्रेम में रहता है)।

यह आपके छोटे कार्यक्रम की विधानसभा को देखने के लिए शिक्षाप्रद हो सकता है, खासकर यदि आप अनुकूलन के बिना संकलन करते हैं। मुझे लगता है कि आप देखेंगे कि आपके फ़ंक्शन में सभी मेमोरी एक्सेस स्टैक फ्रेम पॉइंटर से ऑफसेट के माध्यम से होती है, जो कि कंपाइलर द्वारा फ़ंक्शन के लिए कोड कैसे लिखा जाएगा। संदर्भ द्वारा पास के मामले में, आप एक सूचक के माध्यम से अप्रत्यक्ष मेमोरी एक्सेस निर्देश देखेंगे जो स्टैक पॉइंटर पॉइंटर से कुछ ऑफसेट पर संग्रहीत होता है।


4

कॉल स्टैक वास्तव में स्टैक डेटा संरचना नहीं है। पर्दे के पीछे, हमारे द्वारा उपयोग किए जाने वाले कंप्यूटर यादृच्छिक एक्सेस मशीन वास्तुकला के कार्यान्वयन हैं। तो, ए और बी को सीधे एक्सेस किया जा सकता है।

पर्दे के पीछे, मशीन करती है:

  • स्टैक टॉप के नीचे चौथे तत्व के मूल्य को पढ़ने के लिए "ए" बराबर है।
  • "b" स्टैक टॉप के नीचे तीसरे तत्व के मूल्य को पढ़ने के बराबर है।

http://en.wikipedia.org/wiki/Random-access_machine


1

यहाँ एक आरेख है जिसे मैंने C के कॉल स्टैक के लिए बनाया है। यह Google छवि संस्करणों की तुलना में अधिक सटीक और समकालीन है

यहां छवि विवरण दर्ज करें

और ऊपर दिए गए आरेख की सटीक संरचना के अनुसार, यहां विंडोज 7 पर notepad.exe x64 का डिबग है।

यहां छवि विवरण दर्ज करें

निम्न पतों और उच्च पतों की अदला-बदली की जाती है इसलिए स्टैक इस आरेख में ऊपर की ओर चढ़ रहा है। लाल ठीक उसी तरह इंगित करता है जैसे पहले आरेख में (जो लाल और काले रंग का उपयोग करता था, लेकिन काला अब वापस कर दिया गया है); ब्लैक होम स्पेस है; नीला रिटर्न पता है, जो कॉल करने के बाद निर्देश के लिए कॉलर फ़ंक्शन में एक ऑफसेट है; नारंगी संरेखण है और गुलाबी है जहां निर्देश सूचक कॉल के ठीक बाद और पहले निर्देश से पहले इंगित कर रहा है। होमस्पेस + रिटर्न वैल्यू खिड़कियों पर सबसे छोटी अनुमत फ्रेम है और 16 बाइट आरपीएस संरेखण के रूप में जिसे सही कहा जाता है, की शुरुआत में बनाए रखा जाना चाहिए, इसमें हमेशा 8 बाइट संरेखण भी शामिल है।BaseThreadInitThunk और इसी तरह।

लाल फ़ंक्शन फ़्रेम यह रेखांकित करता है कि कैली फ़ंक्शन क्या तार्किक रूप से 'का मालिक है' + पढ़ता है / संशोधित करता है (यह स्टैक पर पारित एक पैरामीटर को संशोधित कर सकता है जो एक रजिस्टर में पास करने के लिए बहुत बड़ा था)। हरे रंग की लाइनें उस फ़ंक्शन को सीमांकित करती हैं जो फ़ंक्शन को फ़ंक्शन के प्रारंभ से अंत तक आवंटित करता है।


यदि आप डीबग मोड में संकलित करते हैं, तो आरडीआई और अन्य रजिस्टर आर्गन्स केवल स्टैक को दिए जाते हैं, और कोई भी गारंटी नहीं देता है कि एक संकलन उस आदेश को चुनता है। इसके अलावा, स्टैक आर्ग्स को सबसे पुराने फ़ंक्शन कॉल के लिए आरेख के शीर्ष पर क्यों नहीं दिखाया गया है? आपके आरेख में स्पष्ट सीमांकन नहीं है कि कौन सा फ्रेम "डेटा" का मालिक है। (एक कैली अपने स्टैक आर्ग्स का मालिक है)। आरेख के शीर्ष से स्टैक आर्गन को छोड़ना यह देखने के लिए और भी कठिन हो जाता है कि "पैरामीटर जो रजिस्टरों में पारित नहीं हो सकते हैं" हमेशा हर फ़ंक्शन के रिटर्न पते के ठीक ऊपर होते हैं।
पीटर कॉर्डेस

@PeterCordes Goldbolt asm आउटपुट क्लैग और जीसीसी कैलेल्स को एक रजिस्टर में दिए गए पैरामीटर को डिफ़ॉल्ट व्यवहार के रूप में धकेलता है, इसलिए इसका एक पता है। registerपैरामीटर के पीछे gcc के उपयोग से यह अनुकूलन हो जाता है, लेकिन आपको लगता है कि इसे वैसे भी अनुकूलित किया जाएगा, जब तक कि फ़ंक्शन के भीतर पता कभी नहीं लिया गया हो। मैं शीर्ष फ्रेम को ठीक कर दूँगा; माना जाता है कि मुझे दीर्घवृत्त को एक अलग खाली फ्रेम में रखना चाहिए। 'एक कैली अपने स्टैक आर्ग्स का मालिक है', यदि कॉलर्स को रजिस्टरों में पारित नहीं किया जा सकता है, तो क्या शामिल है?
लुईस केल्सी

हाँ, यदि आप अनुकूलन अक्षमता के साथ संकलित करते हैं, तो कैली इसे कहीं न कहीं बिखेर देगा। लेकिन स्टैक आर्ग्स (और यकीनन सहेजे गए-आरबीपी) की स्थिति के विपरीत, कुछ भी मानकीकृत नहीं है कि कहां है। रे: केली अपने स्टैक आर्ग्स का मालिक है: हाँ, फ़ंक्शंस को उनके आने वाले आर्ग को संशोधित करने की अनुमति है। Reg args यह फैलता है खुद args ढेर नहीं हैं। कंपाइलर कभी-कभी ऐसा करते हैं, लेकिन IIRC अक्सर वे रिटर्न एड्रेस के नीचे स्पेस का उपयोग करके स्टैक स्पेस को बर्बाद कर देते हैं, भले ही वे कभी भी arg-re-read न करें। यदि कोई कॉलगर्ल एक ही आर्ग के साथ एक और कॉल करना चाहती है, तो सुरक्षित होने के लिए उन्हें दोहराने से पहले एक और कॉपी स्टोर करनी होगीcall
पीटर कॉर्ड्स

@PeterCordes ठीक है, मैंने तर्कों को कॉलर स्टैक का हिस्सा बनाया क्योंकि मैं जहाँ rbp पॉइंट्स के आधार पर स्टैक फ़्रेमों का सीमांकन कर रहा था। कुछ आरेख इसे कैली स्टैक के हिस्से के रूप में दिखाते हैं (जैसा कि इस प्रश्न पर पहला आरेख करता है) और कुछ इसे कॉलर स्टैक के हिस्से के रूप में दिखाते हैं, लेकिन शायद यह पैरामीटर पैरामीटर के रूप में देखकर उन्हें कैली स्टैक का हिस्सा बनाने के लिए समझ में आता है। उच्च स्तर के कोड में कॉलर के लिए सुलभ नहीं है। हाँ, ऐसा लगता है registerऔर constअनुकूलन केवल -0 पर फर्क करते हैं।
लुईस केल्सी

@PeterCordes मैंने इसे बदल दिया है। मैं इसे फिर से बदल सकता हूं
लेविस केल्सी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.