x86 64架构汇编语言

8、函数与堆栈

1、课程目标

  1. 掌握函数调用的汇编实现机制
  2. 理解堆栈帧的结构和管理
  3. 学会参数传递和返回值处理
  4. 掌握调用约定(Calling Convention)
  5. 理解局部变量的栈分配

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、函数调用流程

  1. 调用者压入参数(cdecl从右到左)
  2. CALL指令压入返回地址
  3. 被调用者建立栈帧(序言)
  4. 执行函数体
  5. 清理栈帧(尾声)
  6. 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、课后作业

  1. 基础练习:用汇编实现一个计算阶乘的函数,使用递归方式
  2. 栈分析:在调试器中观察函数调用时栈的变化过程
  3. 调用约定对比:编写三种调用约定的函数,观察生成的汇编代码差异
  4. 实战应用:实现一个简单的栈回溯函数,打印调用链