iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

Day07|Stack:Function 執行時資料都放去哪裡?

昨天我們介紹了 Function Call,包含:

  • call
  • ret
  • Return Address
  • Calling Convention

也知道當 CPU 執行:

call foo

時,除了跳到 foo 之外,還會把 Return Address 保存起來。

但是問題來了:

Return Address 到底被放在哪裡?

答案就是今天的主角:

Stack。


0x00 Stack 是什麼?

Stack 是 Process Memory 中的一塊記憶體區域。

它最重要的特性是:

LIFO(Last In, First Out)

也就是:

最後放進去的東西,最先被拿出來。

可以想像成疊盤子:

       ┌───────────┐
       │    C      │ ← 最後放進來
       ├───────────┤
       │    B      │
       ├───────────┤
       │    A      │
       └───────────┘

如果現在要拿東西:

C → B → A

這就是 Stack。

在 Binary 裡,Stack 常被拿來保存:

  • Return Address
  • Local Variable
  • Saved Register
  • Function 執行時需要的暫存資料

等等。

而這些東西也是之後學 Pwn 時非常重要的基礎。


0x01 Stack 往哪裡長?

這邊有一個很容易搞混的地方。

在 x86-64 中,Stack 是:

從高位址往低位址成長

假設目前:

High Address
0x7fffffffe000
0x7fffffffdff8
0x7fffffffdff0
0x7fffffffdfe8
0x7fffffffdfe0
Low Address

Stack 新增資料時,會往下面長:

High Address
┌─────────────────┐
│                 │
├─────────────────┤
│ Return Address  │
├─────────────────┤
│ Saved RBP       │
├─────────────────┤
│ Local Variable  │
├─────────────────┤
│ Local Variable  │ ← Stack 繼續往下
└─────────────────┘
Low Address

也就是:

Stack grows downward
        ↓

所以當 Stack 裡增加資料時,位址反而會變小。


0x02 RSP

講到 Stack,就一定要先認識:

RSP

也就是:

Stack Pointer

rsp 會指向目前 Stack 的頂端。

例如:

High Address

0x7fffffffe000
0x7fffffffdff8
0x7fffffffdff0  ← RSP

Low Address

如果 Stack 又放進去一個 8 Bytes 的資料:

High Address

0x7fffffffe000
0x7fffffffdff8
0x7fffffffdff0
0x7fffffffdfe8  ← RSP

Low Address

可以看到:

RSP
0x7fffffffdff0
        ↓
0x7fffffffdfe8

減少了:

8 Bytes

這就是因為 Stack 是往低位址成長。


0x03 push

x86 Assembly 裡有一個很常見的指令:

push

例如:

push rax

意思就是:

把 rax 的內容放進 Stack。

假設:

RSP = 0x1000
RAX = 0x12345678

執行:

push rax

大致上可以理解成:

sub rsp, 8
mov [rsp], rax

所以:

RSP

0x1000
  ↓
0x0ff8

然後:

0x0ff8 = 0x12345678

Stack:

High Address

0x1000
0x0ff8    0x12345678  ← RSP

Low Address

0x04 pop

既然有 push,當然也有相反的操作:

pop

例如:

pop rbx

意思就是:

把 Stack 最上面的資料取出來放進 rbx。

概念上可以理解成:

mov rbx, [rsp]
add rsp, 8

例如原本:

0x0ff8    0x12345678  ← RSP

執行:

pop rbx

之後:

RBX = 0x12345678
RSP = 0x1000

所以可以先把它記成:

push → Stack 增加資料 → RSP 下降

pop  → Stack 取出資料 → RSP 上升

0x05 回到昨天的 call

昨天有提到:

call foo

其實 call 做的事情可以簡化成:

1. 保存 Return Address
2. 跳到 foo

那 Return Address 存在哪裡?

就是:

Stack

假設程式:

0x401000: call foo
0x401005: mov eax, 1

執行:

call foo

之後,CPU 會把:

0x401005

保存到 Stack。

大概像:

High Address

┌──────────────────┐
│     0x401005     │ ← Return Address
└──────────────────┘
          ↑
         RSP

Low Address

接著 RIP 跳去 foo。


0x06 ret 又做了什麼?

當 foo 執行完:

ret

CPU 就會從 Stack 取出:

0x401005

然後放到:

RIP

所以:

RIP = 0x401005

程式就可以回到:

mov eax, 1

繼續執行。

因此昨天講的:

call
 ↓
function
 ↓
ret

現在可以畫得更完整:

main
 │
 │ call foo
 ▼
Push Return Address
 │
 ▼
foo()
 │
 │ ret
 ▼
Pop Return Address
 │
 ▼
main

這也是為什麼 Stack 對 Function Call 非常重要。


0x07 RBP

除了 rsp 之外,逆向時還會一直看到另一個 Register:

RBP

也就是:

Base Pointer

rsp 的位置會一直改變。

例如:

push rax
push rbx
push rcx

每執行一次:

RSP
↓
↓
↓

因此如果我們想要有一個比較固定的位置,用來存取 Function 裡面的資料,就可以使用:

RBP

0x08 Stack Frame

我們先看一個非常經典的 Function:

int add(int a, int b) {
    int result = a + b;
    return result;
}

使用:

gcc -O0 -fno-omit-frame-pointer test.c -o test

編譯。

在 Assembly 裡可能會看到類似:

add:
    push rbp
    mov rbp, rsp

    ...

    pop rbp
    ret

其中這兩行:

push rbp
mov rbp, rsp

就是非常經典的 Function Prologue。


push rbp

首先:

push rbp

把上一個 Function 的 rbp 保存起來。

Stack 可能變成:

High Address

┌──────────────────┐
│  Return Address  │
├──────────────────┤
│    Saved RBP     │ ← RSP
└──────────────────┘

Low Address

mov rbp, rsp

接著:

mov rbp, rsp

讓:

RBP = RSP

因此:

               RBP
                ↓
┌──────────────────┐
│    Saved RBP     │
└──────────────────┘
                ↑
               RSP

這個位置就可以當作這個 Function 的參考點。


0x09 Local Variable

接下來 Function 裡通常還會有:

int a;
int b;

這些 Local Variable 可能會被放在 Stack。

例如:

sub rsp, 0x10

這代表:

RSP = RSP - 0x10

也就是在 Stack 上保留:

16 Bytes

的空間。

Stack 可能變成:

High Address

┌────────────────────┐
│   Return Address   │
├────────────────────┤
│     Saved RBP      │ ← RBP
├────────────────────┤
│                    │
│   Local Variable   │
│                    │
├────────────────────┤
│                    │ ← RSP
└────────────────────┘

Low Address

所以我們可能會看到:

mov DWORD PTR [rbp-0x4], 5

意思就是:

rbp - 4

的位置放入:

5

換成 C 的概念可能就是:

int a = 5;

0x0A rbp - 0x4 是什麼?

這也是 Reverse 很常看到的東西。

例如:

mov DWORD PTR [rbp-0x4], 0x5
mov DWORD PTR [rbp-0x8], 0xa

可能對應:

int a = 5;
int b = 10;

Stack 看起來大概就是:

High Address

┌────────────────────┐
│   Return Address   │
├────────────────────┤
│     Saved RBP      │ ← RBP
├────────────────────┤
│      int a         │ ← rbp - 0x4
├────────────────────┤
│      int b         │ ← rbp - 0x8
└────────────────────┘

Low Address

這時候就可以開始理解:

mov eax, DWORD PTR [rbp-0x4]
add eax, DWORD PTR [rbp-0x8]

其實就是:

a + b

這也是為什麼理解 Stack 之後,Assembly 會突然比較容易閱讀。


0x0B 完整看一次 Function

來看一個簡單的例子:

#include <stdio.h>

int add(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int x = add(10, 20);
    printf("%d\n", x);
}

昨天介紹 System V AMD64 Calling Convention 時有提到:

前幾個參數會透過 Register 傳遞。

所以:

add(10, 20);

大致會是:

RDI = 10
RSI = 20

接著:

call add

進入 add。


Function 一開始可能看到:

push rbp
mov rbp, rsp

建立 Stack Frame。

接著 Compiler 可能把參數暫時存進 Stack:

mov DWORD PTR [rbp-0x14], edi
mov DWORD PTR [rbp-0x18], esi

然後:

mov edx, DWORD PTR [rbp-0x14]
mov eax, DWORD PTR [rbp-0x18]
add eax, edx
mov DWORD PTR [rbp-0x4], eax

如果轉回 C:

int result = a + b;

最後:

mov eax, DWORD PTR [rbp-0x4]
pop rbp
ret

因為昨天有提到:

RAX

通常用來保存 Return Value。

所以:

mov eax, [rbp-0x4]

就是把:

result

當作 Function Return Value。

最後:

ret

回到 main。


0x0C 一個 Function 的 Stack 長什麼樣?

把目前的東西全部放在一起:

High Address
                ↑

┌─────────────────────┐
│   Return Address    │
├─────────────────────┤
│      Saved RBP      │ ← RBP
├─────────────────────┤
│                     │
│   Local Variable    │
│                     │
├─────────────────────┤
│   Local Variable    │
├─────────────────────┤
│                     │ ← RSP
└─────────────────────┘

                ↓
Low Address

現在應該就比較能理解:

push rbp
mov rbp, rsp
sub rsp, 0x20

是在做什麼。

可以把它想成:

保存上一個 Stack Frame
        ↓
建立新的 Stack Fr

上一篇
Day06|Function Call
下一篇
Day08|Static Analysis
系列文
從零開始の Binary CTF:30 天系統化學習 Rev & Pwn 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言