( यदि आप इसके साथ खेलना चाहते हैं तो मैंने इस उत्तर में सभी कोड का एक जिप बनाया है)
मैंने 2003 में अपने CS101 कोर्स के दौरान केवल कभी-कभी सबसे बुनियादी काम किया था। और मुझे वास्तव में "यह" नहीं मिला कि कैसे आस और स्टैक काम करते हैं जब तक कि मुझे एहसास नहीं हो गया कि यह C या C ++ में प्रोग्रामिंग की तरह सभी आधारभूत है ... लेकिन स्थानीय चर, मापदंडों और कार्यों के बिना। शायद अभी तक आसान नहीं लगता है :) मुझे आपको दिखाने के लिए ( इंटेल सिंटैक्स के साथ x86 asm के लिए )।
1. स्टैक क्या है
स्टैक आमतौर पर स्मृति का एक सन्निहित हिस्सा होता है जिसे वे शुरू करने से पहले हर धागे के लिए आवंटित करते हैं। आप जो चाहें वहां स्टोर कर सकते हैं। C ++ शब्दों में ( कोड स्निपेट # 1 ):
const int STACK_CAPACITY = 1000;
thread_local int stack[STACK_CAPACITY];
2. स्टैक के ऊपर और नीचे
सिद्धांत रूप में, आप stack
सरणी के यादृच्छिक सेल ( स्निपेट # 2.1 ) में मान संग्रहीत कर सकते हैं :
stack[333] = 123;
stack[517] = 456;
stack[555] = stack[333] + stack[517];
लेकिन कल्पना करें कि यह याद रखना कितना मुश्किल होगा कि कौन-सी कोशिकाएँ stack
पहले से ही उपयोग में हैं और जो "स्वतंत्र" हैं। इसलिए हम एक दूसरे के बगल में ढेर पर नए मूल्यों को संग्रहीत करते हैं।
(X86) asm के स्टैक के बारे में एक अजीब बात यह है कि आप वहां चीजें जोड़ते हैं जो आखिरी इंडेक्स से शुरू होती है और निचले इंडेक्स में जाती है: स्टैक [999], फिर स्टैक [998] और इसी तरह ( स्निपेट # 2.2 ):
stack[999] = 123;
stack[998] = 456;
stack[997] = stack[999] + stack[998];
और फिर भी के लिए "आधिकारिक" नाम (सतर्कता, अब आप भ्रमित होने वाले हैं) stack[999]
है ढेर के नीचे ।
अंतिम प्रयुक्त सेल ( stack[997]
ऊपर उदाहरण में) को स्टैक के ऊपर कहा जाता है (देखें कि स्टैक का शीर्ष x86 पर कहां है )।
3. स्टैक पॉइंटर (SP)
इस चर्चा के उद्देश्य से मान लें कि CPU रजिस्टर को वैश्विक चर के रूप में दर्शाया गया है ( सामान्य प्रयोजन रजिस्टर देखें )।
int AX, BX, SP, BP, ...;
int main(){...}
विशेष सीपीयू रजिस्टर (एसपी) है जो स्टैक के शीर्ष को ट्रैक करता है। SP एक पॉइंटर है (0xAAAABBCC जैसा मेमोरी एड्रेस रखता है)। लेकिन इस पोस्ट के प्रयोजनों के लिए मैं इसे एक सरणी सूचकांक (0, 1, 2, ...) के रूप में उपयोग करूंगा।
जब एक थ्रेड शुरू होता है, SP == STACK_CAPACITY
और फिर प्रोग्राम और ओएस इसे आवश्यकतानुसार संशोधित करते हैं। नियम यह है कि आप स्टैक के ऊपर और किसी भी इंडेक्स से अधिक स्टैक सेल को नहीं लिख सकते हैं, तब एसपी अमान्य और असुरक्षित है ( सिस्टम में रुकावट के कारण ), इसलिए आप
पहले एसपी को घटाते हैं और फिर नए आवंटित सेल के लिए एक मान लिखते हैं।
जब आप एक पंक्ति में ढेर में कई मूल्यों को धक्का देना चाहते हैं, तो आप उन सभी के लिए स्थान आरक्षित कर सकते हैं ( स्निपेट # 3 ):
SP -= 3;
stack[999] = 12;
stack[998] = 34;
stack[997] = stack[999] + stack[998];
ध्यान दें। अब आप देख सकते हैं कि स्टैक पर आवंटन इतनी तेजी से क्यों है - यह सिर्फ एक रजिस्टर में वृद्धि है।
4. स्थानीय चर
आइए एक नज़र डालते हैं इस सरलीकृत समारोह ( स्निपेट # 4.1 ) पर:
int triple(int a) {
int result = a * 3;
return result;
}
और स्थानीय चर ( स्निपेट # 4.2 ) का उपयोग किए बिना इसे फिर से लिखना :
int triple_noLocals(int a) {
SP -= 1;
stack[SP] = a * 3;
return stack[SP];
}
और देखें कि इसे कैसे कहा जा रहा है ( स्निपेट # 4.3 ):
someVar = triple_noLocals(11);
SP += 1;
5. पुश / पॉप
स्टैक के शीर्ष पर एक नए तत्व का जोड़ एक ऐसा लगातार ऑपरेशन है, जो कि सीपीयू के लिए एक विशेष निर्देश है push
। हम इसे इस तरह से निकालेंगे ( स्निपेट 5.1 ):
void push(int value) {
--SP;
stack[SP] = value;
}
इसी तरह, ढेर के शीर्ष तत्व को लेना ( स्निपेट 5.2 ):
void pop(int& result) {
result = stack[SP];
++SP;
}
पुश / पॉप के लिए सामान्य उपयोग पैटर्न अस्थायी रूप से कुछ मूल्य बचा रहा है। कहें, हमारे पास चर में कुछ उपयोगी है myVar
और किसी कारण से हमें गणना करने की आवश्यकता है जो इसे अधिलेखित कर देगा ( स्निपेट 5.3 ):
int myVar = ...;
push(myVar);
myVar += 10;
...
pop(myVar);
6. फंक्शन पैरामीटर
अब स्टैक का उपयोग करते हुए पैरामीटर पास करें ( स्निपेट # 6 ):
int triple_noL_noParams() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
return stack[SP];
}
int main(){
push(11);
assert(triple(11) == triple_noL_noParams());
SP += 2;
}
7. return
कथन
AX रजिस्टर में मान लौटाएं ( स्निपेट # 7 ):
void triple_noL_noP_noReturn() {
SP -= 1;
stack[SP] = stack[SP + 1] * 3;
AX = stack[SP];
SP += 1;
}
void main(){
...
push(AX);
push(11);
triple_noL_noP_noReturn();
assert(triple(11) == AX);
SP += 1;
pop(AX);
...
}
8. स्टैक बेस पॉइंटर (BP) (जिसे फ्रेम पॉइंटर भी कहा जाता है ) और स्टैक फ्रेम
अधिक "उन्नत" फंक्शन लें और इसे हमारे asm जैसे C ++ ( स्निपेट # 8.1 ) में फिर से लिखें :
int myAlgo(int a, int b) {
int t1 = a * 3;
int t2 = b * 3;
return t1 - t2;
}
void myAlgo_noLPR() {
SP -= 2;
stack[SP + 1] = stack[SP + 2] * 3;
stack[SP] = stack[SP + 3] * 3;
AX = stack[SP + 1] - stack[SP];
SP += 2;
}
int main(){
push(AX);
push(22);
push(11);
myAlgo_noLPR();
assert(myAlgo(11, 22) == AX);
SP += 2;
pop(AX);
}
अब कल्पना करें कि हमने लौटने से पहले परिणाम को स्टोर करने के लिए नए स्थानीय वैरिएबल को पेश करने का फैसला किया, जैसा कि हम tripple
(स्निपेट # 4.1) में करते हैं। समारोह का शरीर होगा ( स्निपेट # 8.2 ):
SP -= 3;
stack[SP + 2] = stack[SP + 3] * 3;
stack[SP + 1] = stack[SP + 4] * 3;
stack[SP] = stack[SP + 2] - stack[SP + 1];
AX = stack[SP];
SP += 3;
आप देखते हैं, हमें फ़ंक्शन पैरामीटर और स्थानीय चर के लिए हर एक संदर्भ को अपडेट करना था। उससे बचने के लिए, हमें एक एंकर इंडेक्स की आवश्यकता होती है, जो स्टैक बढ़ने पर बदलता नहीं है।
हम बीपी रजिस्टर में वर्तमान शीर्ष (एसपी के मूल्य) को बचाकर फ़ंक्शन एंट्री (स्थानीय लोगों के लिए स्थान आवंटित करने से पहले) पर लंगर का अधिकार बनाएंगे। स्निपेट # 8.3 :
void myAlgo_noLPR_withAnchor() {
push(BP);
BP = SP;
SP -= 2;
stack[BP - 1] = stack[BP + 1] * 3;
stack[BP - 2] = stack[BP + 2] * 3;
AX = stack[BP - 1] - stack[BP - 2];
SP = BP;
pop(BP);
}
स्टैक का स्लाइस, जो कि फंक्शन के पूर्ण नियंत्रण से संबंधित है और फ़ंक्शन का स्टैक फ्रेम कहलाता है । ईजी myAlgo_noLPR_withAnchor
का स्टैक फ्रेम है stack[996 .. 994]
(दोनों आइडैक्स सम्मिलित)।
फ़्रेम फ़ंक्शन बीपी पर शुरू होता है (बाद में हमने इसे फ़ंक्शन के अंदर अपडेट किया है) और अगले स्टैक फ्रेम तक रहता है। तो स्टैक पर पैरामीटर कॉलर के स्टैक फ्रेम का हिस्सा हैं (नोट 8 ए देखें)।
नोट:
8 ए। विकिपीडिया मापदंडों के बारे में अन्यथा कहता है , लेकिन यहां मैं इंटेल सॉफ्टवेयर डेवलपर के मैनुअल का पालन करता हूं , वॉल्यूम देखें। 1, खंड 6.2.4.1 स्टैक-फ़्रेम बेस पॉइंटर और चित्रा 6-2 खंड 6.3.2 सुदूर कॉल और आरईटी ऑपरेशन में । फ़ंक्शन के पैरामीटर और स्टैक फ्रेम फ़ंक्शन के सक्रियण रिकॉर्ड का हिस्सा हैं ( फ़ंक्शन पेरिलॉग्स पर जीन देखें )।
8b। बीपी से पॉजिटिव ऑफ़सेट फंक्शन पैरामीटर और नेगेटिव ऑफ़सेट स्थानीय वेरिएबल्स की ओर इशारा करते हैं। यह
8c डिबगिंग के लिए बहुत आसान है । stack[BP]
पिछले स्टैक फ्रेम के पते को संग्रहीत करता है,stack[stack[BP]]
पूर्व-पूर्व स्टैक फ्रेम और इतने पर स्टोर करता है। इस श्रृंखला के बाद, आप प्रोग्राम में सभी कार्यों के फ्रेम की खोज कर सकते हैं, जो अभी तक वापस नहीं आया था। यह कैसे डिबगर दिखाते हैं कि आप स्टैक
8d कहते हैं । के पहले 3 निर्देश myAlgo_noLPR_withAnchor
, जहाँ हम फ्रेम को सेटअप करते हैं (पुराने बीपी को बचाएं, बीपी अपडेट करें, स्थानीय लोगों के लिए आरक्षित स्थान) को फ़ंक्शन प्रस्तावना कहा जाता है
9. सम्मेलन बुलाना
स्निपेट 8.1 में हमने myAlgo
दाएं से बाएं और फिर से परिणाम के लिए मापदंडों को आगे बढ़ाया है AX
। हम दायीं ओर के बाएं पारामों को भी पास कर सकते हैं और वापस लौट सकते हैं BX
। या बीएक्स और सीएक्स में पैरामेट्स उत्तीर्ण करें और एएक्स में लौटें। जाहिर है, फोन करने वाले ( main()
) और फंक्शन को इस बात से सहमत होना चाहिए कि यह सारा सामान कहां और किस क्रम में रखा गया है।
कॉलिंग कन्वेंशन नियमों का एक सेट है कि कैसे मापदंडों को पारित किया जाता है और परिणाम वापस किया जाता है।
उपरोक्त कोड में हमने cdecl कॉलिंग कन्वेंशन का उपयोग किया है :
- पैरामीटर को स्टैक पर पास किया जाता है, कॉल के समय स्टैक पर सबसे कम पते पर पहला तर्क (अंतिम <...) धक्का दिया जाता है। कॉल के बाद स्टैक से वापस मापदंडों को पॉप करने के लिए कॉलर जिम्मेदार है।
- वापसी मान को AX में रखा गया है
- ईबीपी और ईएसपी को कैली (
myAlgo_noLPR_withAnchor
हमारे मामले में कार्य) द्वारा संरक्षित किया जाना चाहिए , जैसे कि कॉलर ( main
फ़ंक्शन) उन रजिस्टरों पर भरोसा कर सकता है जिन्हें कॉल द्वारा नहीं बदला गया है।
- अन्य सभी रजिस्टर (EAX, <...>) कैली द्वारा स्वतंत्र रूप से संशोधित किया जा सकता है; यदि कोई कॉल फ़ंक्शन कॉल के पहले और बाद में किसी मान को संरक्षित करना चाहता है, तो उसे कहीं और मान को सहेजना होगा (हम इसे AX के साथ करते हैं)
(स्रोत: से स्टैक ओवरफ़्लो प्रलेखन उदाहरण "32-बिट cdecl"; द्वारा कॉपीराइट 2016 icktoofay और पीटर Cordes ।; 3.0 सीसी BY-SA एक के अंतर्गत लाइसेंस प्राप्त पूर्ण स्टैक ओवरफ़्लो प्रलेखन सामग्री के संग्रह archive.org में पाया जा सकता है, जिसमें यह उदाहरण विषय आईडी 3261 और उदाहरण आईडी 11196 द्वारा अनुक्रमित है।)
10. फंक्शन कॉल
अब सबसे दिलचस्प हिस्सा। डेटा की तरह, निष्पादन योग्य कोड भी मेमोरी में संग्रहीत किया जाता है (स्टैक के लिए मेमोरी से पूरी तरह असंबंधित) और प्रत्येक निर्देश में एक पता होता है।
जब अन्यथा कमांड नहीं किया जाता है, तो सीपीयू एक के बाद एक निर्देशों को निष्पादित करता है, जिस क्रम में वे मेमोरी में संग्रहीत होते हैं। लेकिन हम CPU को मेमोरी में दूसरे स्थान पर "जंप" करने और वहां से निर्देशों को निष्पादित करने के लिए कमांड कर सकते हैं। Asm में यह कोई भी पता हो सकता है, और C ++ जैसी अधिक उच्च-स्तरीय भाषाओं में आप केवल लेबल द्वारा चिह्नित पते पर जा सकते हैं ( वर्कअराउंड हैं, लेकिन वे कम से कम कहने के लिए सुंदर नहीं हैं)।
आइए इस फ़ंक्शन ( स्निपेट # 10.1 ) को लें:
int myAlgo_withCalls(int a, int b) {
int t1 = triple(a);
int t2 = triple(b);
return t1 - t2;
}
और tripple
C ++ तरीके से कॉल करने के बजाय , निम्नलिखित करें:
- शरीर
tripple
की शुरुआत के लिए कोड की प्रतिलिपि बनाएँmyAlgo
- पर
myAlgo
प्रवेश के ऊपर से छलांग tripple
के साथ के कोडgoto
- जब हमें
tripple
कोड को निष्पादित करने की आवश्यकता होती है tripple
, तो कॉल करने के बाद कोड लाइन के स्टैक पते पर सहेजें , इसलिए हम बाद में यहां लौट सकते हैं और निष्पादन जारी रख सकते हैं ( PUSH_ADDRESS
नीचे मैक्रो)
- पहली पंक्ति (
tripple
फ़ंक्शन) के पते पर जाएं और इसे अंत तक निष्पादित करें (3. और 4. साथ में CALL
मैक्रो हैं)
- के अंत में
tripple
(हम स्थानीय लोगों को साफ करने के बाद), स्टैक के शीर्ष से वापसी का पता लें और वहां कूदें ( RET
मैक्रो)
चूँकि C ++ में विशेष कोड एड्रेस पर कूदने का कोई आसान तरीका नहीं है, हम लेबल का उपयोग जम्प के स्थानों को चिह्नित करने के लिए करेंगे। मैं विस्तार से नहीं बताऊंगा कि मैक्रोज़ नीचे काम कैसे करते हैं, बस मुझे विश्वास है कि वे वही करते हैं जो मैं कहता हूँ कि वे करते हैं ( स्निपेट # 10.2 ):
#define PUSH_ADDRESS(labelName) { \
void* tmpPointer; \
__asm{ mov [tmpPointer], offset labelName } \
push(reinterpret_cast<int>(tmpPointer)); \
}
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#define LABEL_NAME(num) TOKENPASTE2(lbl_, num)
#define CALL_IMPL(funcLabelName, callId) \
PUSH_ADDRESS(LABEL_NAME(callId)); \
goto funcLabelName; \
LABEL_NAME(callId) :
#define CALL(funcLabelName) CALL_IMPL(funcLabelName, __LINE__)
#define RET() { \
int tmpInt; \
pop(tmpInt); \
void* tmpPointer = reinterpret_cast<void*>(tmpInt); \
__asm{ jmp tmpPointer } \
}
void myAlgo_asm() {
goto my_algo_start;
triple_label:
push(BP);
BP = SP;
SP -= 1;
stack[BP - 1] = stack[BP + 2] * 3;
AX = stack[BP - 1];
SP = BP;
pop(BP);
RET();
my_algo_start:
push(BP);
BP = SP;
SP -= 2;
push(AX);
push(stack[BP + 2]);
CALL(triple_label);
stack[BP - 1] = AX;
SP -= 1;
pop(AX);
push(AX);
push(stack[BP + 3]);
CALL(triple_label);
stack[BP - 2] = AX;
SP -= 1;
pop(AX);
AX = stack[BP - 1] - stack[BP - 2];
SP = BP;
pop(BP);
}
int main() {
push(AX);
push(22);
push(11);
push(7777);
myAlgo_asm();
assert(myAlgo_withCalls(11, 22) == AX);
SP += 1;
SP += 2;
pop(AX);
}
नोट्स:
10 ए। क्योंकि रिटर्न पता स्टैक पर संग्रहीत है, सिद्धांत रूप में हम इसे बदल सकते हैं। इस तरह से स्टैक स्मैशिंग हमला
10 बी काम करता है । triple_label
(क्लीनअप लोकल, पुराने बीपी को पुनर्स्थापित करें, वापसी) के अंतिम 3 निर्देशों को फ़ंक्शन का उपसंहार कहा जाता है
11. असेम्बली
अब के लिए वास्तविक asm को देखते हैं myAlgo_withCalls
। दृश्य स्टूडियो में ऐसा करने के लिए:
- x86 के लिए बिल्ड प्लेटफ़ॉर्म सेट करें ( x86_64 नहीं )
- बिल्ड प्रकार: डीबग
- myAlgo_withCalls के अंदर कहीं और ब्रेक पॉइंट सेट करें
- रन, और जब निष्पादन ब्रेक पॉइंट पर रुक जाता है Ctrl + Alt + D दबाएं
हमारे asm- जैसे C ++ के साथ एक अंतर यह है कि asm का स्टैक ints के बजाय बाइट्स पर काम करता है। तो एक के लिए स्थान आरक्षित करने के लिए int
, एसपी को 4 बाइट्स से घटाया जाएगा।
यहाँ हम जाते हैं ( स्निपेट # 11.1 , टिप्पणियों में लाइन नंबर, जीस्ट से हैं ):
; 114: int myAlgo_withCalls(int a, int b) {
push ebp ; create stack frame
mov ebp,esp
; return address at (ebp + 4), `a` at (ebp + 8), `b` at (ebp + 12)
sub esp,0D8h ; reserve space for locals. Compiler can reserve more bytes then needed. 0D8h is hexadecimal == 216 decimal
push ebx ; cdecl requires to save all these registers
push esi
push edi
; fill all the space for local variables (from (ebp-0D8h) to (ebp)) with value 0CCCCCCCCh repeated 36h times (36h * 4 == 0D8h)
; see https://stackoverflow.com/q/3818856/264047
; I guess that's for ease of debugging, so that stack is filled with recognizable values
; 0CCCCCCCCh in binary is 110011001100...
lea edi,[ebp-0D8h]
mov ecx,36h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
; 115: int t1 = triple(a);
mov eax,dword ptr [ebp+8] ; push parameter `a` on the stack
push eax
call triple (01A13E8h)
add esp,4 ; clean up param
mov dword ptr [ebp-8],eax ; copy result from eax to `t1`
; 116: int t2 = triple(b);
mov eax,dword ptr [ebp+0Ch] ; push `b` (0Ch == 12)
push eax
call triple (01A13E8h)
add esp,4
mov dword ptr [ebp-14h],eax ; t2 = eax
mov eax,dword ptr [ebp-8] ; calculate and store result in eax
sub eax,dword ptr [ebp-14h]
pop edi ; restore registers
pop esi
pop ebx
add esp,0D8h ; check we didn't mess up esp or ebp. this is only for debug builds
cmp ebp,esp
call __RTC_CheckEsp (01A116Dh)
mov esp,ebp ; destroy frame
pop ebp
ret
और इसके लिए tripple
( स्निपेट # 11.2 ):
push ebp
mov ebp,esp
sub esp,0CCh
push ebx
push esi
push edi
lea edi,[ebp-0CCh]
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
imul eax,dword ptr [ebp+8],3
mov dword ptr [ebp-8],eax
mov eax,dword ptr [ebp-8]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
आशा है, इस पोस्ट को पढ़ने के बाद, विधानसभा पहले की तरह गूढ़ नहीं दिखेगी :)
यहाँ पोस्ट के शरीर से लिंक और कुछ आगे पढ़ने हैं:
- एली बेंडस्की , जहाँ ढेर का शीर्ष x86 पर है - ऊपर / नीचे, धक्का / पॉप, सपा, स्टैक फ्रेम, कॉलिंग कन्वेंशन
- एली बेंडस्की , x86-64 पर स्टैक फ्रेम लेआउट - x64, स्टैक फ्रेम, रेड ज़ोन पर गुजरने वाले आर्ग्स
- स्टैण्ड को समझना, स्टैक कॉन्सेप्ट्स - स्टैक कॉन्सेप्ट्स के लिए एक बहुत अच्छी तरह से लिखा गया परिचय। (यह MIPS (x86 नहीं) और GAS सिंटैक्स में है, लेकिन यह विषय के लिए महत्वहीन है)। यदि रुचि हो तो MIPS ISA प्रोग्रामिंग पर अन्य नोट देखें ।
- x86 असिम विकिबूक, जनरल-पर्पस रजिस्टर
- x86 डिस्सेम्फ़र विकाइबूक, द स्टैक
- x86 Disassembly wikibook, फ़ंक्शंस और स्टैक फ्रेम्स
- इंटेल सॉफ्टवेयर डेवलपर के मैनुअल - मुझे उम्मीद थी कि यह वास्तव में कट्टर होगा, लेकिन आश्चर्यजनक रूप से यह बहुत आसान पढ़ा गया है (हालांकि जानकारी की मात्रा बहुत अधिक है)
- जोनाथन डी बोयेन पोलार्ड, द फंक्शन ऑन जीन पेरिलॉग्स - प्रस्तावना / उपसंहार, स्टैक फ्रेम / सक्रियण, रेड ज़ोन