什麼是堆棧?
把“棧”想成一摞只能放最上面、只能拿最上面的盤子——這就是逆向學習法:先記住“最後放的最先拿”,再反推它為什麼長這樣、怎麼用、坑在哪。下面用“小白語言”帶你三步逆序吃透。
五個步驟:
第①步:先背口訣(10 秒記住)
“後放先拿,先放壓底;拿只拿頂,不能插隊。”
→ 這就是 LIFO,後面所有故事都圍着它轉。
第②步:逆向拆三個生活場景(看完就懂 80 %)
場景 A——撤銷鍵(Ctrl+Z)
你依次打了“我”“愛”“中國”:
- 棧裏依次壓入:【我】→【愛】→【中國】
- 按一次撤銷:先出來的是最後輸入的“中國”
- 再按:出來“愛”
結論:編輯器的撤銷棧 = 純純的 LIFO,後放先拿。
場景 B——函數套娃
void a(){ b(); }
void b(){ c(); }
void c(){ printf("到這裏"); }
調用順序:a → b → c
可返回順序必須倒着來:c 先返 → b 返 → a 返
結論:電腦把“返回地址”壓進系統棧,才能保證後調的先返。
場景 C——括號匹配
表達式:{ [ ( ) ] }
- 讀左邊括號就壓棧:{ → [ → (
- 讀到右括號就跟“棧頂”比,對上就彈出
- 如果最後棧空 → 全部匹配
結論:右邊的括號必須先找到離它最近的左邊,符合 LIFO。
第③步:逆向總結“它到底解決了啥問題”
問題:
“我要倒序處理、回溯處理、最近相關處理,怎麼辦?”
答案:用棧。
- 倒序:撤銷、後綴表達式
- 回溯:函數返回、遞歸
- 最近相關:括號、標籤匹配
第④步:最小代碼(30 行以內,能跑)
#include <stdio.h>
int stack[100], top = -1; // 順序棧,盤子數組
void push(int x) { stack[++top] = x; } // 放盤子
int pop() { return stack[top--]; } // 拿盤子
int empty() { return top == -1; }
int main(){
push(1); // 後放
push(2);
push(3);
while(!empty())
printf("%d ", pop()); // 先拿 3 2 1
return 0;
}
運行結果:3 2 1 ← 肉眼看見“倒序”。
第⑤步:逆向看“常見翻車點”
- 棧溢出 = 盤子太多摞塌了(無限遞歸)
解決:少套娃,或把ulimit -s調大 - 空棧彈數據 = 沒盤子硬拿,會 SEGFAULT
解決:每次 pop 前先if(!empty()) - 拿錯位置 = 想從中間抽盤子,違反規則
解決:記住“只能動頂”
一句話逆向總結
“棧”就是電腦世界裏的一把時光倒流尺:
誰最後來,誰就最先被處理;
只要你記住這句,所有括號、撤銷、遞歸、DFS 的代碼,都能一眼看穿它在幹嘛。
小練習
這是一個訓練用的exe,我們打卡od
我用的是集成好的od
將exe拖入到od中
1.push壓棧
看這個00401000,程序在這裏停住了。
這一條對應的彙編語言是push 0x100
第二條為pop eax
這張圖裏只有 3 行指令,卻把所有“棧”的動作一次性擺在你面前。
用“盤子比喻”一句話就能看懂:
push 0x100把數值 0x100 這個“盤子”放到最上面,同時棧頂指針 ESP 自動-4(32 位棧向下增長)。pop eax把最上面那隻盤子(0x100)拿下來,送進寄存器 EAX;ESP 自動+4。nop什麼都不做,佔個位置,方便以後調試或補丁。
完整時間線(假設 ESP 初始 = 0012FFC0)
時刻 指令 棧頂地址 棧頂內容 ESP 值 EAX 值 0 — 0012FFC0 舊數據 0012FFC0 ? 1 push 0x100 0012FFBC 0x100 0012FFBC ? 2 pop eax 0012FFC0 舊數據 0012FFC0 0x100
兩句話總結
- push = 放盤子 + 棧指針向下走一步;
- pop = 拿盤子 + 棧指針向上回原位。
這兩條指令合起來,就是“把立即數 0x100 裝進 EAX”的最短路徑,順便示範了棧的 LIFO 規則。
此時棧頂是ESP 0019FF78
od的右下角是棧的窗口
返回到 kernel #kernel是內核裏的東西,先不用管。找個意思就是對應的內核地址。
也可以右鍵-在堆棧窗口跟隨
這個是單步步入,意思就是也可以按F7.點擊後這行彙編代碼就會執行掉。
我們點擊一下,發現跳到了第二行,地址跳到了00401005.
第一行代碼已經執行了,就是向棧裏壓入了一個0x100
現在看右邊窗口,esp寄存器這個地址變成了0019FF74
棧頂指針的值為0019FF78在代碼窗口區域,執行F7單步調試,進行下一步,執行00401000 >/$ 68 00010000 push 0x100處的push命令。
此時,如下圖所示,ESP的值變為0019FF74
這個位置可以看到棧裏面,我們制定的數值位置已經壓進去了。
2.pop出棧
壓棧完成後,我們點點擊單步步入或是F7。0019FF78
此時我們看,右邊的寄存器FPU裏的esp棧頂位置又回到了0019FF78
而此時右下角的棧窗口中顯示0019FF78 74D05D49 返回到 kernel32.74D05D49
也可以説明執行了出棧的操作。
總結
向棧壓入數據時,棧頂指針減小,向低地址移動,從棧中彈出數據時,棧頂指針增加,向高地址移動。
把棧想成倒着放的糖葫蘆串:
- 串的頭在最上面,叫“棧頂指針”。
- 每插一根新糖葫蘆(push),你得往下找空位,所以指針往低地址走(數字變小)。
- 拔掉一根(pop),指針又往回上移(數字變大),剛才的位置就作廢了。
一句話:
“往下插,指針減;往上拔,指針加。”