分步入栈参数后调用可调用对象的x86-64汇编+C++实现问题
问题分析与解决方案
核心问题
你实现的参数压栈+调用机制在基础场景下可用,但当目标函数内部调用其他函数时崩溃,根源在于栈结构处理错误、调用约定不匹配以及参数管理逻辑缺陷。
原代码的关键错误
- 错误的栈指针保存:
FT_StartCall直接保存call指令执行后的rsp(已包含返回地址),导致后续恢复栈时丢失调用上下文。 - 参数压入逻辑违规:
FT_PushIntPointer手动弹出并重新压入返回地址,破坏正常栈帧结构,干扰中间函数调用的栈回溯。 - 硬编码参数数量:
FT_CallFunction固定处理4个参数,参数数量不符时会错误弹出栈中无关数据,直接破坏栈结构。 - 参数顺序颠倒:通过pop获取参数时,先得到最后压入的参数,导致传递给目标函数的参数顺序与预期相反。
- 未保证栈对齐:Windows x64调用约定要求调用函数前rsp必须16字节对齐,原代码未处理此要求,可能触发内存访问异常。
- 未保护非易失寄存器:目标函数可能修改非易失寄存器,原代码未保存/恢复这些寄存器,破坏调用者上下文。
修正后的完整实现
汇编代码(x64 Windows)
.data StackPointer QWORD 0 ; 保存调用FT_StartCall前的栈指针 ArgCount QWORD 0 ; 记录压入的参数数量 .code ALIGN 16 ; 初始化调用流程,保存初始栈指针并重置参数计数 FT_StartCall PROC lea rax, [rsp+8] ; 跳过call指令压入的返回地址,保存调用前的rsp mov [StackPointer], rax mov [ArgCount], 0 ret FT_StartCall ENDP ; 压入一个指针/整数类型的参数 FT_PushIntPointer PROC ; __fastcall约定:rcx为传入的参数 push rcx inc [ArgCount] ret FT_PushIntPointer ENDP ; 调用目标可调用对象,传递已压入的参数并返回结果 FT_CallFunction PROC ; __fastcall约定:rcx为目标函数指针 push rbx push rbp push rsi push rdi push r12 push r13 push r14 push r15 mov rbx, rcx ; 保存目标函数指针 mov rcx, [ArgCount] ; 获取参数总数 mov rdx, [StackPointer] ; 获取初始栈指针 sub rdx, 8 ; 指向第一个压入的参数(函数的第一个参数) ; 初始化寄存器参数 xor r8, r8 xor r9, r9 xor rax, rax ; 分配寄存器参数(前四个参数) cmp rcx, 0 jz NoArgs mov rax, [rdx] mov rcx, rax ; 第一个参数->rcx cmp rcx, 1 jz OnlyOneArg sub rdx, 8 mov rax, [rdx] mov rdx, rax ; 第二个参数->rdx cmp rcx, 2 jz OnlyTwoArgs sub rdx, 8 mov rax, [rdx] mov r8, rax ; 第三个参数->r8 cmp rcx, 3 jz OnlyThreeArgs sub rdx, 8 mov rax, [rdx] mov r9, rax ; 第四个参数->r9 OnlyThreeArgs: OnlyTwoArgs: OnlyOneArg: NoArgs: ; 处理栈上参数(超过四个的情况) mov rsi, [ArgCount] cmp rsi, 4 jbe NoStackArgs sub rsi, 4 ; 栈上参数数量 lea rdx, [StackPointer - 8*[ArgCount]] ; 指向最后一个参数(argN) StackArgLoop: mov rax, [rdx] push rax add rdx, 8 dec rsi jnz StackArgLoop NoStackArgs: ; 保证栈16字节对齐 mov rbp, rsp and rbp, 0xF jz StackAligned sub rsp, 8 StackAligned: ; 调用目标函数 call rbx ; 恢复初始栈指针,清理所有压入的参数 mov rsp, [StackPointer] ; 恢复非易失寄存器 pop r15 pop r14 pop r13 pop r12 pop rdi pop rsi pop rbp pop rbx ; 返回结果(已在rax中) ret FT_CallFunction ENDP END
C++测试代码
extern "C" void* __fastcall FT_CallFunction(void* Function); extern "C" void __fastcall FT_PushIntPointer(void* ArgOrPointer); extern "C" void __fastcall FT_StartCall(); // 测试函数:内部调用其他函数,验证栈稳定性 int Calculate(int a, int b, int c) { int temp = a + b; return temp * c; } int main() { FT_StartCall(); FT_PushIntPointer((void*)2); // 第一个参数a=2 FT_PushIntPointer((void*)3); // 第二个参数b=3 FT_PushIntPointer((void*)4); // 第三个参数c=4 int res = (int)FT_CallFunction(Calculate); // 预期结果:(2+3)*4=20 return res; }
关键修正说明
- 正确保存初始栈指针:
FT_StartCall通过lea rax, [rsp+8]跳过call指令压入的返回地址,保存调用者调用前的栈指针,确保后续参数压入位置正确。 - 简化参数压入逻辑:
FT_PushIntPointer直接push参数并递增计数,不再手动处理返回地址,保证栈帧结构正常。 - 动态处理参数数量:根据
ArgCount动态分配寄存器参数,超过四个的参数按x64调用约定从右到左压入栈。 - 栈对齐保证:调用目标函数前检查并调整rsp到16字节对齐,符合Windows x64调用要求。
- 非易失寄存器保护:保存并恢复所有非易失寄存器,避免目标函数修改寄存器破坏调用者上下文。
- 参数顺序修正:通过内存地址直接读取参数,保证第一个压入的参数对应函数的第一个参数,解决顺序颠倒问题。
内容的提问来源于stack exchange,提问作者Trevor
相关产品推荐
相关产品推荐

