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 AddressCALL指令自动压入的下一条指令地址
序言/尾声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. 实战应用:实现一个简单的栈回溯函数,打印调用链