x86 64架构汇编语言
8、函数与堆栈
1、课程目标
- 掌握函数调用的汇编实现机制
- 理解堆栈帧的结构和管理
- 学会参数传递和返回值处理
- 掌握调用约定(Calling Convention)
- 理解局部变量的栈分配
2、名词解释
| 术语 | 英文 | 说明 |
|---|---|---|
| 堆栈帧 | Stack Frame | 函数调用时在栈上分配的独立内存区域 |
| 调用约定 | Calling Convention | 函数调用时参数传递和清理的规则 |
| 栈指针 | Stack Pointer (ESP/RSP) | 指向栈顶的寄存器 |
| 基址指针 | Base Pointer (EBP/RBP) | 指向当前栈帧底部的寄存器 |
| 返回地址 | Return Address | CALL指令自动压入的下一条指令地址 |
| 序言/尾声 | Prologue/Epilogue | 函数开头和结尾的标准代码模式 |
3、使用工具
| 工具 | 用途 |
|---|---|
| Visual Studio Debugger | 观察栈帧变化和调用过程 |
| WinDbg | 高级栈回溯分析 |
| x64dbg | 动态调试观察堆栈 |
4、技术原理
4.1、堆栈基础
栈是向低地址增长的内存区域:
- PUSH操作:ESP/RSP减少,数据写入
- POP操作:数据读出,ESP/RSP增加
4.2、函数调用流程
- 调用者压入参数(cdecl从右到左)
- CALL指令压入返回地址
- 被调用者建立栈帧(序言)
- 执行函数体
- 清理栈帧(尾声)
- RET返回,调用者清理参数
5、代码实现
5.1、示例1:基本函数调用(MASM)
.386
.model flat, stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
result DWORD 0
.code
; 加法函数
Add PROC
; 序言 - 建立栈帧
push ebp ; 保存旧的基址指针
mov ebp, esp ; 建立新的栈帧
; 访问参数: [ebp+8]=第一个参数, [ebp+12]=第二个参数
mov eax, [ebp+8] ; 第一个参数
add eax, [ebp+12] ; 加上第二个参数
; 尾声 - 清理栈帧
mov esp, ebp ; 恢复栈指针
pop ebp ; 恢复旧的基址指针
ret ; 返回,eax包含返回值
Add ENDP
main PROC
; 调用 Add(10, 20)
push 20 ; 第二个参数(从右到左)
push 10 ; 第一个参数
call Add
add esp, 8 ; 清理参数(cdecl调用者清理)
mov result, eax ; 保存结果
push 0
call ExitProcess
main ENDP
END main
5.2、示例2:栈帧结构详解
; 栈帧布局(从高地址到低地址):
; [ebp+16] 第三个参数
; [ebp+12] 第二个参数
; [ebp+8] 第一个参数
; [ebp+4] 返回地址(CALL自动压入)
; [ebp+0] 保存的旧EBP
; [ebp-4] 第一个局部变量
; [ebp-8] 第二个局部变量
CalcFunc PROC
push ebp
mov ebp, esp
sub esp, 16 ; 为4个局部变量分配空间
; 保存需要保留的寄存器
push ebx
push esi
push edi
; 使用局部变量
mov DWORD PTR [ebp-4], 100 ; 局部变量1
mov DWORD PTR [ebp-8], 200 ; 局部变量2
; 访问参数并计算
mov eax, [ebp+8]
add eax, [ebp-4]
add eax, [ebp-8]
; 恢复保存的寄存器
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret
CalcFunc ENDP
5.3、示例3:不同调用约定
#include <stdio.h>
// cdecl - C语言默认,调用者清栈
int __cdecl CdeclAdd(int a, int b) {
return a + b;
}
// stdcall - Win32 API默认,被调用者清栈
int __stdcall StdcallAdd(int a, int b) {
return a + b;
}
// fastcall - 前两个参数用寄存器
int __fastcall FastcallAdd(int a, int b) {
return a + b;
}
int main() {
int r1, r2, r3;
// cdecl调用
__asm {
push 20
push 10
call CdeclAdd
add esp, 8 ; 调用者清理
mov r1, eax
}
// stdcall调用
__asm {
push 20
push 10
call StdcallAdd ; 被调用者清理(ret 8)
mov r2, eax
}
// fastcall调用
__asm {
mov edx, 20 ; 第二个参数
mov ecx, 10 ; 第一个参数
call FastcallAdd
mov r3, eax
}
printf("cdecl: %d, stdcall: %d, fastcall: %d\n", r1, r2, r3);
return 0;
}
5.4、示例4:栈回溯实现
#include <windows.h>
#include <stdio.h>
void PrintStackTrace() {
DWORD* ebp;
DWORD* retAddr;
// 获取当前EBP
__asm {
mov ebp, ebp
}
printf("Stack Trace:\n");
// 遍历栈帧链
for (int i = 0; i < 10 && ebp != NULL; i++) {
retAddr = (DWORD*)(*(ebp + 1)); // 返回地址在[ebp+4]
printf(" Frame %d: EBP=0x%08X, RetAddr=0x%08X\n",
i, (DWORD)ebp, (DWORD)retAddr);
ebp = (DWORD*)(*ebp); // 上一个栈帧的EBP
}
}
void FuncC() {
printf("In FuncC\n");
PrintStackTrace();
}
void FuncB() {
printf("In FuncB\n");
FuncC();
}
void FuncA() {
printf("In FuncA\n");
FuncB();
}
int main() {
FuncA();
return 0;
}
6、课后作业
- 基础练习:用汇编实现一个计算阶乘的函数,使用递归方式
- 栈分析:在调试器中观察函数调用时栈的变化过程
- 调用约定对比:编写三种调用约定的函数,观察生成的汇编代码差异
- 实战应用:实现一个简单的栈回溯函数,打印调用链