什麼是堆棧?

把“棧”想成一摞只能放最上面、只能拿最上面的盤子——這就是逆向學習法:先記住“最後放的最先拿”,再反推它為什麼長這樣、怎麼用、坑在哪。下面用“小白語言”帶你三步逆序吃透。

五個步驟:

第①步:先背口訣(10 秒記住)

“後放先拿,先放壓底;拿只拿頂,不能插隊。”

→ 這就是 LIFO,後面所有故事都圍着它轉。

第②步:逆向拆三個生活場景(看完就懂 80 %)

場景 A——撤銷鍵(Ctrl+Z)

你依次打了“我”“愛”“中國”:

  1. 棧裏依次壓入:【我】→【愛】→【中國】
  2. 按一次撤銷:先出來的是最後輸入的“中國”
  3. 再按:出來“愛”

結論:編輯器的撤銷棧 = 純純的 LIFO,後放先拿。

場景 B——函數套娃

void a(){ b(); }
void b(){ c(); }
void c(){ printf("到這裏"); }

調用順序:a → b → c

可返回順序必須倒着來:c 先返 → b 返 → a 返

結論:電腦把“返回地址”壓進系統棧,才能保證後調的先返。

場景 C——括號匹配

表達式:{ [ ( ) ] }

  1. 讀左邊括號就壓棧:{ → [ → (
  2. 讀到右括號就跟“棧頂”比,對上就彈出
  3. 如果最後棧空 → 全部匹配

結論:右邊的括號必須先找到離它最近的左邊,符合 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 ← 肉眼看見“倒序”。

第⑤步:逆向看“常見翻車點”

  1. 棧溢出 = 盤子太多摞塌了(無限遞歸)
    解決:少套娃,或把 ulimit -s 調大
  2. 空棧彈數據 = 沒盤子硬拿,會 SEGFAULT
    解決:每次 pop 前先 if(!empty())
  3. 拿錯位置 = 想從中間抽盤子,違反規則
    解決:記住“只能動頂”

一句話逆向總結

“棧”就是電腦世界裏的一把時光倒流尺:

誰最後來,誰就最先被處理;

只要你記住這句,所有括號、撤銷、遞歸、DFS 的代碼,都能一眼看穿它在幹嘛。

小練習

pwn學習4堆棧(筆記)_倒序

這是一個訓練用的exe,我們打卡od

pwn學習4堆棧(筆記)_倒序_02

我用的是集成好的od

pwn學習4堆棧(筆記)_寄存器_03

將exe拖入到od中

1.push壓棧

pwn學習4堆棧(筆記)_寄存器_04

看這個00401000,程序在這裏停住了。

這一條對應的彙編語言是push 0x100

第二條為pop eax

這張圖裏只有 3 行指令,卻把所有“棧”的動作一次性擺在你面前。

用“盤子比喻”一句話就能看懂:

  1. push 0x100把數值 0x100 這個“盤子”放到最上面,同時棧頂指針 ESP 自動-4(32 位棧向下增長)。
  2. pop eax把最上面那隻盤子(0x100)拿下來,送進寄存器 EAX;ESP 自動+4。
  3. 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 規則。

pwn學習4堆棧(筆記)_遞歸_05

此時棧頂是ESP  0019FF78

pwn學習4堆棧(筆記)_倒序_06

od的右下角是棧的窗口

pwn學習4堆棧(筆記)_倒序_07

返回到 kernel  #kernel是內核裏的東西,先不用管。找個意思就是對應的內核地址。

也可以右鍵-在堆棧窗口跟隨

pwn學習4堆棧(筆記)_遞歸_08

這個是單步步入,意思就是也可以按F7.點擊後這行彙編代碼就會執行掉。

pwn學習4堆棧(筆記)_遞歸_09

我們點擊一下,發現跳到了第二行,地址跳到了00401005.

第一行代碼已經執行了,就是向棧裏壓入了一個0x100

pwn學習4堆棧(筆記)_遞歸_10

現在看右邊窗口,esp寄存器這個地址變成了0019FF74

棧頂指針的值為0019FF78在代碼窗口區域,執行F7單步調試,進行下一步,執行00401000 >/$  68 00010000          push 0x100處的push命令。

此時,如下圖所示,ESP的值變為0019FF74

pwn學習4堆棧(筆記)_倒序_11

這個位置可以看到棧裏面,我們制定的數值位置已經壓進去了。

2.pop出棧

pwn學習4堆棧(筆記)_遞歸_12

壓棧完成後,我們點點擊單步步入或是F7。0019FF78

pwn學習4堆棧(筆記)_倒序_13

此時我們看,右邊的寄存器FPU裏的esp棧頂位置又回到了0019FF78

而此時右下角的棧窗口中顯示0019FF78   74D05D49  返回到 kernel32.74D05D49

也可以説明執行了出棧的操作。

總結

向棧壓入數據時,棧頂指針減小,向低地址移動,從棧中彈出數據時,棧頂指針增加,向高地址移動。

把棧想成倒着放的糖葫蘆串:

  • 串的頭在最上面,叫“棧頂指針”。
  • 每插一根新糖葫蘆(push),你得往下找空位,所以指針往低地址走(數字變小)。
  • 拔掉一根(pop),指針又往回上移(數字變大),剛才的位置就作廢了。

一句話:

“往下插,指針減;往上拔,指針加。”