棧內行為分析
一、源碼分析
我們以以下簡單的 C 程序為例,通過 GDB 動態調試分析函數調用過程中的棧內布局變化:
#include <stdio.h>
int add(){int a = 10;int b = 20;return (a + b);
}int main() {add();return 0;
}
編譯為 32 位程序:
gcc -m32 test.c -o test
gdb ./test
動態調試
依次在對應位置打上斷點
(gdb) b *main
Breakpoint 1 at 0x11d9
(gdb) b *add
Breakpoint 2 at 0x11ad
(gdb) b *add+42
Breakpoint 3 at 0x11d7
(gdb) r
Starting program: /root/test
Breakpoint 1, 0x565561d9 in main ()
(gdb) layout regs
補充: esp 永遠指向棧頂,ebp永遠指向棧底 先記住這句話
然后我們繼續運行c
進入add
函數在下面這個圖我們還沒進入add
函數,eip
指向的地址就是add
函數的入口地址
程序進入 main()
函數后,還未調用 add()
函數之前,EIP
指向的是 add()
函數的入口地址。此時:
- ESP 指向當前棧頂
- EBP 尚未參與本次函數調用幀的構造
重點部分解釋
push %ebp ;這就是我們經常說的壓棧 將調用者(main)的 ebp 存入當前棧頂,用于函數返回后恢復上下文
mov %esp, %ebp ;設置新的棧基址,構造當前函數的棧幀
sub $0x10, %esp ;壓棧 留出棧空間(0x10 字節)用于局部變量(如 a、b)
當我們進入add
函數到這一步我們會發現 此時的esp
=ebp
可以觀察到:
程序剛剛進入函數,ESP == EBP
,棧幀尚未展開。
類似于“空水桶”,當前的棧頂和棧底都指向相同位置。
這一步體現了函數調用剛發生、棧幀尚未初始化的狀態。
但是接下來,我們繼續si
幾步我們會發現esp
在不停變化代碼比較簡單 但是能看出esp
是不斷變化的
ESP
向低地址移動(因為棧向下生長)
為局部變量 a
與 b
分配空間
函數內部指令執行期間不斷使用和調整 ESP
這一過程中,ESP
表示當前操作的頂部位置,而 EBP
固定在該棧幀的底部,作為局部變量的偏移基準。
直到走到我們第三個斷點 add+42
我們繼續si
esp
和ebp
又繼續相等了,同時我們回到了main
函數當中
ESP
與 EBP
再次恢復為相同值,意味著當前棧幀已被銷毀
程序執行流程回到 main()
函數,繼續往下執行
匯編解釋
leave ; 出棧 實際上等價于:mov %ebp, %esp(還原 esp)→ pop %ebp(恢復上層 ebp)
ret ; 跳轉 彈出棧頂的返回地址(調用者 call 指令之后的地址),跳轉回主函數
-------------------分割線---------------
leave 是一個復合指令,相當于做了這兩件事:
mov %ebp, %esp ; 清理當前棧幀(還原棧頂)
pop %ebp ; 恢復調用者的 ebp
-------------------分割線---------------
ret 的作用
ret 會做這件事:
pop %eip ; 從棧頂取出返回地址并跳轉執行