Hiểu Stack Frame Trong Assembly: Từ Code C++ Đến Memory Address
Khi học Assembly, bạn thường gặp khái niệm stack frame nhưng khó hình dung cách nó hoạt động trong thực tế. Tại sao tham số lại ở [ebp+8]? Tại sao local variables ở [ebp-4]? Địa chỉ bộ nhớ thực sự trông như thế nào?
Bài viết này sẽ đưa bạn từ code C++ đơn giản, qua Assembly x86, đến phân tích stack frame với memory address cụ thể (như 0x0012FF78). Bạn sẽ thấy rõ cách compiler xây dựng stack frame, EBP/ESP thay đổi thế nào, và hiểu calling convention từ góc nhìn thực tế.
#1. Memory Layout - Stack Và Heap Nằm Ở Đâu?
Trước khi đi sâu vào stack frame, bạn cần hiểu bộ nhớ của process được tổ chức thế nào. Hiểu rõ memory layout giúp bạn:
- Phân biệt stack và heap - Biết khi nào dùng từng loại, tránh stack overflow
- Debug memory corruption - Xác định lỗi truy cập sai vùng nhớ (segmentation fault)
- Reverse malware hiệu quả - Nhận biết shellcode, buffer overflow, ROP chains
- Tối ưu hiệu năng - Hiểu chi phí của dynamic allocation vs stack allocation
Mỗi process có vùng nhớ riêng, chia thành nhiều segments khác nhau.
#1.1. Memory Layout Của Process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Địa chỉ cao (0xFFFFFFFF) ← HIGH ADDRESS
+---------------------------+
| Kernel Space | ← OS kernel (không truy cập được)
+---------------------------+
| Stack | ← Mở rộng XUỐNG ↓ (giảm địa chỉ)
| ↓ | ESP/RSP trỏ đây
+---------------------------+
| Memory Mapped Files | ← Shared libraries, DLLs
+---------------------------+
| ↑ |
| Heap | ← Mở rộng LÊN ↑ (tăng địa chỉ)
+---------------------------+ new/malloc cấp phát ở đây
| .bss (uninitialized) | ← Biến global chưa khởi tạo
+---------------------------+ int globalVar;
| .data (initialized) | ← Biến global đã khởi tạo
+---------------------------+ int globalVar = 10;
| .text (code) | ← Code thực thi (instructions)
+---------------------------+ 0x00400000 (Windows) / 0x08048000 (Linux)
Địa chỉ thấp (0x00000000) ← LOW ADDRESS#1.2. Tại Sao Stack Mở Rộng Xuống?
Lý do lịch sử:
- CPU architecture ban đầu thiết kế stack “grows down” (từ địa chỉ cao xuống thấp)
- Heap “grows up” (từ địa chỉ thấp lên cao)
- Tạo khoảng trống giữa stack và heap → tránh va chạm
Ví dụ cụ thể:
1
2
3
4
5
6
7
8
9
10
11
Ban đầu (process vừa start):
0xFFFFFFFF
|
0xBFFFFFFF ← Stack bắt đầu (ESP)
|
| (khoảng trống ~3GB)
|
0x08050000 ← Heap bắt đầu
|
0x08048000 ← .text (code)
0x00000000Sau khi gọi nhiều hàm và malloc:
1
2
3
4
5
6
7
8
9
10
11
12
0xFFFFFFFF
|
0xBFFFE000 ← Stack đã mở rộng XUỐNG (ESP giảm)
| Push nhiều frame
| (khoảng trống còn ~2.9GB)
|
0x08070000 ← Heap đã mở rộng LÊN (malloc tăng)
|
0x08050000
|
0x08048000
0x00000000#1.3. So Sánh Stack Và Heap
| Đặc điểm | Stack | Heap |
|---|---|---|
| Vị trí | Địa chỉ cao (gần 0xFFFFFFFF) | Địa chỉ thấp (sau .data segment) |
| Hướng mở rộng | Xuống ↓ (giảm địa chỉ) | Lên ↑ (tăng địa chỉ) |
| Kích thước | Nhỏ (1-8MB) | Lớn (chỉ giới hạn bởi RAM) |
| Tốc độ | Rất nhanh (ESP +/-) | Chậm hơn (malloc phức tạp) |
| Quản lý | Tự động (hàm return = giải phóng) | Thủ công (phải free/delete) |
| Thread-safe | Mỗi thread có stack riêng | Shared giữa threads |
| Fragmentation | Không có | Có thể bị phân mảnh |
#1.4. Khi Nào Dùng Stack Hay Heap?
Dùng Stack khi:
1
2
3
4
5
void process() {
int count = 0; // Stack - OK (vòng đời ngắn)
char name[32]; // Stack - OK (dữ liệu nhỏ)
int small[100]; // Stack - OK (400 byte)
}Dùng Heap khi:
1
2
3
4
5
6
7
8
9
10
11
void loadImage() {
// Heap - PHẢI dùng cho dữ liệu lớn
int* bigArray = new int[1000000]; // 4MB
delete[] bigArray;
}
// Modern C++ - Smart pointer
void loadImageModern() {
auto bigArray = std::make_unique<int[]>(1000000);
// Tự động delete khi ra khỏi scope
}Quyết định dùng Stack hay Heap:
View Mermaid diagram code
flowchart TD
Start([Cần cấp phát bộ nhớ]) --> Size{Kích thước<br/>dữ liệu?}
Size -->|"< 1KB"| Lifetime{Vòng đời<br/>dữ liệu?}
Size -->|"> 1KB"| UseHeap([Dùng Heap])
Lifetime -->|Chỉ trong hàm| Scope{Cần trả về<br/>pointer?}
Lifetime -->|Qua nhiều hàm| UseHeap
Scope -->|Không| UseStack([Dùng Stack<br/>Biến local tự động])
Scope -->|Có| UseHeap
UseHeap --> Modern{C++11+?}
Modern -->|Có| SmartPtr["std::unique_ptr<br/>std::shared_ptr"]
Modern -->|Không| NewDelete[new/delete<br/>Nhớ phải delete!]Bây giờ bạn đã hiểu stack nằm ở đâu trong memory và khi nào nên dùng, hãy cùng xem cách compiler sử dụng stack để gọi hàm và xây dựng stack frame.
#2. Gọi Hàm Trong Assembly - Từ C++ Đến Stack Frame (x86)
Mỗi hàm C++ được compile thành Assembly sẽ có function prologue (khởi tạo) và epilogue (dọn dẹp) do compiler tự động tạo ra để thiết lập và thu hồi stack frame. Hiểu rõ cách này hoạt động giúp bạn:
- Debug crash chính xác - Biết chính xác hàm nào gây lỗi và tại sao
- Phân tích malware - Reverse engineer để hiểu logic của mã độc
- Tối ưu performance - Nhận biết overhead của function calls
- Tránh lỗ hổng bảo mật - Phát hiện buffer overflow và stack corruption
Phần này sẽ phân tích từng bước từ code C++ → Assembly x86 → Stack frame với ví dụ thực tế.
#Lưu ý
Ví dụ này sử dụng x86 (32-bit) với cdecl calling convention - tham số truyền qua stack. Nếu bạn làm việc với x64 (64-bit), 4 tham số đầu được truyền qua thanh ghi (RCX, RDX, R8, R9) thay vì stack. Xem chi tiết tại Calling Conventions trong Windows x86 và x64.
#2.1. Các Thành Phần Chính Của Stack Frame
Trước khi đi vào code, bạn cần hiểu một stack frame bao gồm những gì. Mỗi khi hàm được gọi, một stack frame mới được tạo ra chứa:
| Thành phần | Vị trí | Mô tả |
|---|---|---|
| Local Variables | [ebp-4], [ebp-8], … | Biến cục bộ của hàm (int temp, char buffer[], …) |
| Saved EBP | [ebp] | EBP của hàm gọi (caller’s EBP) - dùng để khôi phục sau |
| Return Address | [ebp+4] | Địa chỉ để quay về caller sau khi hàm kết thúc |
| Arguments | [ebp+8], [ebp+12], … | Tham số truyền vào hàm (a, b, c, …) |
Sơ đồ stack frame điển hình:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Địa chỉ cao (High Address)
+-------------------------------+
0x0012FF7C: | Argument 2 (b) | [ebp+12] ← Tham số 2 (0x0012FF70 + 12 = 0x0012FF7C)
+-------------------------------+
0x0012FF78: | Argument 1 (a) | [ebp+8] ← Tham số 1 (0x0012FF70 + 8 = 0x0012FF78)
+-------------------------------+
0x0012FF74: | Return Address | [ebp+4] ← Địa chỉ quay về caller (0x0012FF70 + 4 = 0x0012FF74)
+-------------------------------+
0x0012FF70: | Saved EBP | [ebp] ← EBP của caller, EBP = 0x0012FF70
+-------------------------------+
0x0012FF6C: | Local Variable 1 | [ebp-4] ← Biến local 1 (0x0012FF70 - 4 = 0x0012FF6C)
+-------------------------------+
0x0012FF68: | Local Variable 2 | [ebp-8] ← Biến local 2 (0x0012FF70 - 8 = 0x0012FF68)
+-------------------------------+ ← ESP = 0x0012FF68 (đỉnh stack)
Địa chỉ thấp (Low Address)Tại sao cần EBP làm “anchor”?
ESP liên tục thay đổi khi push/pop, khó truy cập arguments và locals. EBP cố định suốt hàm như một “mốc tọa độ”:
- Arguments ở địa chỉ cao hơn EBP:
[ebp+8],[ebp+12], … (địa chỉ tăng lên) - Locals ở địa chỉ thấp hơn EBP:
[ebp-4],[ebp-8], … (địa chỉ giảm xuống) - EBP không đổi suốt hàm, nên offset luôn chính xác
- Saved EBP tạo “chain”: Mỗi hàm lưu EBP của caller → debugger theo chuỗi này để hiển thị call stack
#2.2. Liên Kết C++ + Assembly + Stack Frame
Bắt đầu với hàm tính lương sau thuế. Qua ví dụ này bạn sẽ thấy cách compiler chuyển C++ sang Assembly và xây dựng stack frame với local variables.
#2.2.1. Code C++
1
2
3
4
5
6
7
8
9
10
int calculateNetSalary(int baseSalary, int bonus) {
int grossPay = baseSalary + bonus; // Tổng lương trước thuế
int tax = grossPay / 10; // Thuế 10%
return grossPay - tax; // Lương sau thuế
}
int main() {
int salary = calculateNetSalary(1000, 200); // salary = 1080
return 0;
}#2.2.2. Assembly Code
Compiler sẽ chuyển đổi sang Assembly như sau. Chú ý các bước prologue (thiết lập frame) và epilogue (dọn dẹp frame):
Hàm calculateNetSalary (callee) - Hàm được gọi:
Phần 1: Prologue - Thiết lập stack frame
1
2
3
4
calculateNetSalary:
push ebp ; [1] Lưu EBP của main lên stack
mov ebp, esp ; [2] EBP = ESP (tạo mốc cố định)
sub esp, 8 ; [3] Cấp phát 8 byte cho 2 biến localChi tiết:
push ebp- Lưu EBP của caller (main) để khôi phục saumov ebp, esp- Tạo “anchor point” cố định (EBP = ESP)sub esp, 8- Cấp phát 8 byte trên stack (2 biến × 4 byte = grossPay và tax)
Phần 2: Function Body - Tính toán grossPay
1
2
3
4
; int grossPay = baseSalary + bonus
mov eax, [ebp+8] ; eax = baseSalary (tham số 1)
add eax, [ebp+12] ; eax = baseSalary + bonus (tham số 2)
mov [ebp-4], eax ; Lưu kết quả vào grossPayChi tiết:
[ebp+8]= baseSalary - Tham số đầu tiên ở[ebp+8](vì[ebp]= saved EBP,[ebp+4]= return address)[ebp+12]= bonus - Tham số thứ 2 ở vị trí kế tiếp (+4 byte)[ebp-4]= grossPay - Biến local đầu tiên ở phía dưới EBP
Phần 3: Function Body - Tính toán tax
1
2
3
4
5
6
; int tax = grossPay / 10
mov eax, [ebp-4] ; eax = grossPay
cdq ; Mở rộng EAX thành EDX:EAX cho phép chia
mov ecx, 10 ; ecx = 10
idiv ecx ; eax = grossPay / 10
mov [ebp-8], eax ; Lưu kết quả vào taxChi tiết:
cdq- Sign-extend EAX vào EDX:EAX (cần choidivvới số có dấu)[ebp-8]= tax - Biến local thứ 2 ở vị trí -8 byte từ EBP
Phần 4: Return Value - Tính kết quả cuối cùng
1
2
3
4
; return grossPay - tax
mov eax, [ebp-4] ; eax = grossPay
sub eax, [ebp-8] ; eax = grossPay - tax
; Return value luôn đặt trong EAXChi tiết:
eax= return value - Convention chuẩn: hàm trả về integer qua thanh ghi EAX
Phần 5: Epilogue - Dọn dẹp và quay về
1
2
3
mov esp, ebp ; [4] Giải phóng local variables (ESP trở về EBP)
pop ebp ; [5] Khôi phục EBP của main từ stack
ret ; [6] Pop return address và nhảy về mainChi tiết:
mov esp, ebp- Giải phóng toàn bộ local variables (ESP tăng từ 0x0012FF70 → 0x0012FF78)pop ebp- Khôi phục EBP của callerret- Pop return address và nhảy về caller
Hàm main (caller) - Hàm gọi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
main:
; === Prologue của main ===
push ebp
mov ebp, esp
sub esp, 4 ; Cấp phát 4 byte cho biến local 'salary'
; === Chuẩn bị gọi calculateNetSalary(1000, 200) ===
push 200 ; Đẩy tham số 2 (bonus) - push từ PHẢI sang TRÁI
push 1000 ; Đẩy tham số 1 (baseSalary)
call calculateNetSalary ; Gọi hàm (CPU tự động push return address)
add esp, 8 ; Dọn dẹp 2 tham số (8 byte = 2 * 4 byte)
; cdecl: CALLER dọn dẹp stack
; === Sử dụng return value ===
mov [ebp-4], eax ; salary = eax (1080)
; === Epilogue của main ===
mov esp, ebp
pop ebp
retTại sao push từ phải sang trái?
C/C++ convention: tham số push theo thứ tự ngược (right-to-left) để đảm bảo tham số đầu tiên luôn ở vị trí cố định [ebp+8]. Điều này giúp compiler dễ dàng sinh code và hỗ trợ variadic functions (hàm với số tham số thay đổi) - tham số đầu tiên luôn biết trước vị trí dù có bao nhiêu tham số sau đó.
#2.2.3. Stack Frame
Hãy theo dõi stack thay đổi thế nào khi main() gọi calculateNetSalary(1000, 200). Điều này rất quan trọng khi debug crash hoặc phân tích malware.
Bước 1: main() đẩy tham số lên stack
1
2
3
4
5
6
7
8
9
10
11
; Trong main: push 200
; push 1000
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF84: | 200 (bonus) | [esp+4] ← Tham số 2
+------------------------------+
0x0012FF80: | 1000 (baseSal.) | [esp] ← Tham số 1, ESP = 0x0012FF80
+------------------------------+
... (main's stack frame)
Địa chỉ thấp (Low Address)Bước 2: call calculateNetSalary - CPU tự động push return address
1
2
3
4
5
6
7
8
9
10
11
12
13
; CPU thực thi: push EIP (return address)
; jmp calculateNetSalary
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF84: | 200 (bonus) | [esp+8]
+------------------------------+
0x0012FF80: | 1000 (baseSal.) | [esp+4]
+------------------------------+
0x0012FF7C: | Return address | [esp] ← ESP = 0x0012FF7C (địa chỉ trong main)
+------------------------------+ ← Đầu hàm calculateNetSalary
... (main's stack frame)
Địa chỉ thấp (Low Address)Lúc này EIP (Instruction Pointer) nhảy đến hàm calculateNetSalary, CPU bắt đầu thực thi prologue. Lưu ý ESP giảm từ 0x0012FF80 → 0x0012FF7C (giảm 4 byte) sau khi call push return address.
Bước 3: Prologue của calculateNetSalary() - push ebp; mov ebp, esp; sub esp, 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; calculateNetSalary: push ebp
; mov ebp, esp
; sub esp, 8
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF84: | 200 (bonus) | [ebp+12] ← Tham số 2 (0x0012FF78 + 12 = 0x0012FF84)
+------------------------------+
0x0012FF80: | 1000 (baseSal.) | [ebp+8] ← Tham số 1 (0x0012FF78 + 8 = 0x0012FF80)
+------------------------------+
0x0012FF7C: | Return address | [ebp+4] ← Địa chỉ quay về main (0x0012FF78 + 4)
+------------------------------+
0x0012FF78: | Saved EBP | [ebp] ← EBP của main, EBP = 0x0012FF78
+------------------------------+
0x0012FF74: | grossPay (?) | [ebp-4] ← Local var 1 (0x0012FF78 - 4 = 0x0012FF74)
+------------------------------+
0x0012FF70: | tax (?) | [ebp-8] ← Local var 2 (0x0012FF78 - 8 = 0x0012FF70)
+------------------------------+ ← ESP = 0x0012FF70
0x0012FF6C: | main's frame |
+------------------------------+
Địa chỉ thấp (Low Address)Bây giờ bạn có thể hiểu:
- Stack grows down: ESP giảm từ
0x0012FF7C→0x0012FF78(push ebp) →0x0012FF70(sub esp, 8) - Tại sao
[ebp+8]? Vì EBP =0x0012FF78, nên[ebp+8]=0x0012FF80(baseSalary) - Tại sao
[ebp-4]? Vì0x0012FF78 - 4=0x0012FF74(grossPay) - Arguments ở địa chỉ cao hơn EBP (hướng lên +4, +8, +12…)
- Local variables ở địa chỉ thấp hơn EBP (hướng xuống -4, -8, -12…)
Bước 4: Epilogue của calculateNetSalary() - mov esp, ebp; pop ebp; ret
1
2
3
4
5
6
7
8
9
10
11
12
13
; calculateNetSalary: mov esp, ebp (giải phóng local variables)
; pop ebp (khôi phục EBP của main)
; ret (pop return address và nhảy về)
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF84: | 200 (bonus) | [esp+4] ← Rác, sẽ bị ghi đè sau
+------------------------------+
0x0012FF80: | 1000 (baseSal.) | [esp] ← ESP = 0x0012FF80
+------------------------------+ ← EIP quay về main, EAX = 1080
0x0012FF7C: | main's frame |
+------------------------------+
Địa chỉ thấp (Low Address)Sau ret, ESP trở về vị trí trước khi call (trỏ vào arguments). Lưu ý:
mov esp, ebp: ESP tăng từ0x0012FF70→0x0012FF78(giải phóng local vars)pop ebp: ESP tăng từ0x0012FF78→0x0012FF7C(khôi phục EBP)ret: ESP tăng từ0x0012FF7C→0x0012FF80(pop return address)- Return value (1080) nằm trong thanh ghi EAX
- main phải dọn dẹp arguments bằng
add esp, 8
Bước 5: main() dọn dẹp stack - add esp, 8
1
2
3
4
5
6
7
; main: add esp, 8 (bỏ qua 2 tham số)
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF88: | main's frame | ← ESP = 0x0012FF88 (trở về vị trí ban đầu)
+------------------------------+
Địa chỉ thấp (Low Address)Stack trở về trạng thái như trước khi gọi calculateNetSalary(). Lưu ý:
add esp, 8: ESP tăng từ0x0012FF80→0x0012FF88(bỏ qua 2 tham số)- Return value (1080) nằm trong EAX và được lưu vào biến
salarytại[ebp-4] - Stack đã được dọn dẹp hoàn toàn, sẵn sàng cho lần gọi hàm tiếp theo
#2.3. Function Prologue & Epilogue
Bây giờ bạn đã thấy toàn bộ quá trình, hãy tập trung vào prologue và epilogue - hai đoạn code quan trọng nhất trong mỗi hàm.
#Function Prologue - Thiết Lập Frame
Mục đích: Tạo một stack frame mới để hàm có thể hoạt động độc lập với caller.
1
2
3
4
myFunction:
push ebp ; [1] Lưu EBP của caller
mov ebp, esp ; [2] Tạo mốc cố định (EBP = ESP)
sub esp, 16 ; [3] Cấp phát 16 byte cho local variablesTừng bước:
push ebp- Lưu EBP của caller để khôi phục sau. Tạo “chain” giúp debugger unwind call stack và hiển thị toàn bộ chuỗi hàm gọimov ebp, esp- Đặt EBP = ESP. Từ giờ EBP là “anchor point” cố địnhsub esp, N- Cấp phát N byte cho biến cục bộ. Ví dụ: 2 biến int → cần 8 byte
Stack sau prologue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF84: | Argument 2 | [ebp+12]
+------------------------------+
0x0012FF80: | Argument 1 | [ebp+8]
+------------------------------+
0x0012FF7C: | Return address | [ebp+4]
+------------------------------+
0x0012FF78: | Saved EBP | [ebp] ← EBP = 0x0012FF78
+------------------------------+
0x0012FF74: | Local var 1 | [ebp-4]
+------------------------------+
0x0012FF70: | Local var 2 | [ebp-8]
+------------------------------+ ← ESP = 0x0012FF70
0x0012FF6C: | Caller's frame |
+------------------------------+
Địa chỉ thấp (Low Address)#Function Epilogue - Dọn Dẹp Frame
Mục đích: “Phá hủy” stack frame và quay về caller, giữ return value trong EAX.
Cách 1: Thủ công
1
2
3
4
mov eax, [ebp-4] ; Đặt return value vào EAX (nếu có)
mov esp, ebp ; [1] Giải phóng local variables
pop ebp ; [2] Khôi phục caller's EBP
ret ; [3] Nhảy về callerCách 2: Dùng lệnh leave (recommended)
1
2
3
mov eax, [ebp-4] ; Đặt return value vào EAX
leave ; Tương đương mov esp,ebp; pop ebp
ret ; Nhảy về callerTại sao dùng leave?
- Ngắn gọn hơn (1 lệnh thay vì 2)
- Ít lỗi hơn - không thể quên
mov esp, ebp - Compiler hiện đại thường dùng
leave
#3. Nguyên Nhân Hàm Crash Và Cách Phân Tích
Khi làm việc với Assembly hoặc reverse malware, bạn sẽ thường gặp các lỗi crash liên quan đến stack. Hiểu rõ nguyên nhân giúp bạn:
- Debug nhanh hơn - Nhận diện lỗi stack corruption ngay lập tức
- Phân tích crash dumps - Đọc call stack để tìm hàm gây lỗi
- Tránh lỗi khi viết Assembly - Đảm bảo prologue/epilogue đúng
- Phát hiện buffer overflow - Nhận biết dấu hiệu bị exploit
Phần này phân tích hai lỗi phổ biến nhất: stack pointer corruption và stack overflow.
#3.1. Stack Pointer Corruption - ESP/EBP Không Đồng Bộ
Lỗi phổ biến:
1
2
3
4
5
6
7
8
9
10
myFunction:
push ebp
mov ebp, esp
sub esp, 16 ; Cấp phát 16 byte
; ... code ...
; THIẾU: mov esp, ebp (quên khôi phục ESP!)
pop ebp
ret ; ESP sai → crashPhân tích:
Nếu thiếu mov esp, ebp, ESP vẫn trỏ vào vùng local variables (0x0012FF70). Khi pop ebp, sẽ lấy sai giá trị từ local variable thay vì saved EBP → EBP bị hỏng → các hàm tiếp theo crash.
Stack frame khi bị lỗi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF7C: | Return address | [ebp+4]
+------------------------------+
0x0012FF78: | Saved EBP | [ebp] ← EBP = 0x0012FF78 (✅ ĐÚNG)
+------------------------------+
0x0012FF74: | Local var 1 | [ebp-4]
+------------------------------+
0x0012FF70: | Local var 2 | [ebp-8] ← ESP = 0x0012FF70 (🚨 SAI!)
+------------------------------+ ⚠️ Thiếu mov esp, ebp
Địa chỉ thấp (Low Address)
⚠️ Khi thực thi pop ebp:
- pop ebp sẽ lấy giá trị tại [ESP] = 0x0012FF70
- Giá trị này là local variable (rác), KHÔNG PHẢI saved EBP!
- EBP bị ghi đè bằng giá trị rác → hàm tiếp theo crash
- ESP tăng lên 0x0012FF74 (vẫn sai)
- ret sẽ lấy return address từ 0x0012FF74 (local var) thay vì 0x0012FF7C → crash!Stack frame ĐÚNG (có mov esp, ebp):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Địa chỉ cao (High Address)
+------------------------------+
0x0012FF7C: | Return address | [ebp+4]
+------------------------------+
0x0012FF78: | Saved EBP | [ebp] ← EBP = 0x0012FF78
+------------------------------+ ← ESP = 0x0012FF78 (✅ sau mov esp, ebp)
0x0012FF74: | Local var 1 | [ebp-4] (đã giải phóng)
+------------------------------+
0x0012FF70: | Local var 2 | [ebp-8] (đã giải phóng)
+------------------------------+
Địa chỉ thấp (Low Address)
✅ Khi thực thi pop ebp:
- ESP đã được khôi phục về 0x0012FF78 (bằng EBP)
- pop ebp lấy giá trị ĐÚNG từ [ESP] = saved EBP
- ESP tăng lên 0x0012FF7C (trỏ vào return address)
- ret lấy return address từ 0x0012FF7C → quay về caller thành công!Trong thực tế:
Compiler hiện đại thường dùng lệnh leave (tương đương mov esp, ebp; pop ebp) để tránh lỗi này. Tuy nhiên khi viết Assembly thủ công, bạn phải cẩn thận!
#3.2. Stack Overflow - Đệ Quy Quá Sâu
Code gây overflow:
1
2
3
4
void infinite() {
int arr[1000]; // 4000 byte mỗi lần gọi
infinite(); // Đệ quy vô hạn
}Phân tích:
Mỗi lần gọi infinite() tốn ~4KB stack. Nếu gọi 2000 lần → tốn 8MB → vượt quá stack limit (thường 1-8MB) → Stack Overflow
Cách fix:
- Chuyển sang iterative (vòng lặp)
- Dùng đệ quy đuôi (tail recursion)
- Chuyển mảng lớn sang heap (malloc/new)
Vậy là bạn đã hiểu cách phân tích stack frame với địa chỉ cụ thể, tìm nguyên nhân crash, và phân biệt stack vs heap. Kiến thức này cực kỳ quan trọng khi debug Assembly, reverse malware, hoặc phân tích crash dumps.
Đặc biệt khi làm việc với legacy code hoặc viết shellcode, việc hiểu rõ stack layout giúp bạn tránh được 90% lỗi memory corruption. Hãy thử mở debugger và phân tích stack của chương trình bạn đang làm ngay!