C++ 除非使用某些寄存器,否则函数挂钩将崩溃
所以我试图为一个游戏钩住一个函数,但有一个小问题。如果eax、ebx、ecx和edx等寄存器是可互换的,那么为什么下面的第一个代码示例会使游戏进程崩溃,而第二个代码不会崩溃并按预期工作C++ 除非使用某些寄存器,否则函数挂钩将崩溃,c++,visual-c++,x86,inline-assembly,calling-convention,C++,Visual C++,X86,Inline Assembly,Calling Convention,所以我试图为一个游戏钩住一个函数,但有一个小问题。如果eax、ebx、ecx和edx等寄存器是可互换的,那么为什么下面的第一个代码示例会使游戏进程崩溃,而第二个代码不会崩溃并按预期工作 // Crashes game process void __declspec(naked) HOOK_UnfreezePlayer() { __asm push eax if ( !state->player.frozen || !state->ready ) __
// Crashes game process
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]
add ebx, 0x4
mov ecx, [ebx]
add ecx, 0x40
lea edx, [esi + 0x0C]
cmp edx, ecx
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}
// Works
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov ecx, g_dwBase_Addr
mov edx, [ecx + LOCAL_PLAYER_INFO_OFFSET]
add edx, 0x4
mov ebp, [edx]
add ebp, 0x40
lea ecx, [esi + 0x0C]
cmp ecx, ebp
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}
我认为崩溃可能是因为我的汇编代码覆盖了寄存器eax、ebx、ecx等中的重要数据。。例如,如果游戏正在eax中存储一个重要的值,然后由于我的if语句正在将结构指针移动到eax中而导致数据丢失,该怎么办?有没有办法保存这些寄存器的内容,并在返回之前将其恢复到原始值?当挂接一个已编译的程序时,寄存器肯定是不可交换的,因为各个寄存器的含义是由挂接程序的代码和挂接程序在该代码中的位置定义的。因此,必须检查钩子代码和钩子的位置,以确定钩子代码是否依赖于保留的某些寄存器的内容 使用开头的
push eax
指令和结尾的pop eax
指令,您已经在保存eax寄存器的内容,并在之后恢复它。您可以对EBX和EDX寄存器执行相同的操作,也可以简单地使用PUSHAD/POPAD指令保存所有通用寄存器。根据钩子在游戏中的位置,您可能还必须保留EFLAGS寄存器,这需要PUSHFD/POPFD指令
保存和恢复ECX寄存器并不容易,因为钩子正在使用该寄存器计算完成后要跳转到的地址
但是,由于您说第二个代码示例工作,而第一个代码示例导致挂接的程序崩溃,因此问题可能只是在修改EBX寄存器时出现的。这是因为第一个代码示例修改EBX寄存器,而第二个代码示例不修改
因此,您的问题的可能解决方案是以与保留EAX寄存器相同的方式保留EBX寄存器。为此,只需在push eax
指令的同一位置添加push ebx
指令,并在pop eax
指令的同一位置添加pop ebx
指令。但是,请注意,由于堆栈的工作方式,push和pop指令的顺序必须相反,如下所示:
挂钩启动:
push eax
push ebx
钩端:
pop ebx
pop eax
如果eax、ebx、ecx和edx等寄存器是可互换的,那么为什么下面的第一个代码示例会使游戏进程崩溃,而第二个代码不会崩溃并按预期工作
// Crashes game process
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]
add ebx, 0x4
mov ecx, [ebx]
add ecx, 0x40
lea edx, [esi + 0x0C]
cmp edx, ecx
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}
// Works
void __declspec(naked) HOOK_UnfreezePlayer()
{
__asm push eax
if ( !state->player.frozen || !state->ready )
__asm jmp hk_Disabled
__asm
{
mov ecx, g_dwBase_Addr
mov edx, [ecx + LOCAL_PLAYER_INFO_OFFSET]
add edx, 0x4
mov ebp, [edx]
add ebp, 0x40
lea ecx, [esi + 0x0C]
cmp ecx, ebp
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0
hk_Return:
pop eax
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}
}
在该函数跳转到g\u dwBase\u Addr+RETURN\u UnfreezePlayer
之后,可能您的调用者正在使用EBX执行一些重要的操作
如果要挂接现有的函数调用,那么EAX、ECX和EDX将在标准调用约定中被取消调用,而另一个整数regs调用将被保留
当您销毁EBP时,调用方恰好没有中断,只有当您销毁EBX时,这是有道理的
或者,如果要将此代码的跳转/调用插入到根本不需要函数调用的地方,则应该保存/恢复所修改的每个寄存器,可能包括EFLAG。(查看“调用站点”,查看它在您“返回”后是否会销毁任何寄存器;例如,add
或cmp
只写入EFLAG,而不是读取,因此如果您看到这样的指令,您就知道不必保存/恢复EFLAG。类似地,mov
的目标是只写。)
具体来说,在执行任何其他操作之前,在函数的顶部:
_asm {
push eax
push ecx
push edx
// and whatever other register you need
}
在底部,在跳跃前按匹配的顺序将其弹出
_asm {
// and whatever other register you need
pop edx
pop ecx
pop eax
jmp target
}
您正在使用寄存器来保存跳转目标。您可以分析“调用者”并找到一个可以安全销毁的寄存器,这样您就可以在不使用/save/restore的情况下使用该寄存器。或者硬编码跳转目标地址,以便使用jmp rel32
而不是间接jmp reg
或者(以显著的性能成本)您可以将jmp
替换为push
/ret
_asm {
push eax // extra dummy slot we can replace with a return address
push eax
push ecx
push edx
...
pop edx
pop ecx
//pop eax
mov eax, g_dwBase_Addr
add eax, RETURN_UnfreezePlayer
mov [esp+4], eax // store into the dummy slot
pop eax
ret // branch mispredict guaranteed
}
使用与push/ret等效的方法可以保证此ret的分支预测失误,对于未来的ret
指令,由于调用/ret预测器堆栈不匹配,因此会导致调用堆栈上的分支预测失误。此函数中的某个虚拟调用
可以修复此问题,使此ret
预测失误。(但请注意,callnext\u指令
将不起作用;CPU是一种特殊情况,不能将其视为真正的调用。您必须实际跳过某些内容。)
您可能会尝试使用xchg[esp],eax
/ret
,但这非常慢:带有内存操作数的xchg意味着一个锁
前缀(完整内存屏障,微码原子交换)
在最初推送时为“返回地址”保留一个插槽似乎是最有效的,否则您可能会推送一个返回地址,mov
加载保存的EAX值,然后pop[esp+4]
将该返回地址向上复制4个字节。但是,在发现ret
预测失误之前,额外的拷贝会增加延迟
如果这不一定是线程安全的,您可以在存储目标地址后使用jmp[target_address]
。或者,如果g_dwBase\u Addr+RETURN\u UnfreezePlayer
是一个常量,只需将其保存在某个静态变量中,就可以jmp dword ptr[target_address]
而不是每次都计算目标
您可以使用ESP下方的空间,但这并不是绝对安全的。恢复寄存器后,与jmp[esp-4]类似。SEH可以使用它,调试器也可以使用它
您可以优化您的函数以
mov eax, g_dwBase_Addr
mov eax, [eax + LOCAL_PLAYER_INFO_OFFSET]
; then use EAX everywhere you were using EBX in later instructions
add ebx, 0x4 ; add eax, 4 // with changes from above
mov ecx, [ebx] ; mov ecx, [eax]
// no push or pop needed, destroying only ECX
_asm {
mov ecx, g_dwBase_Addr
mov ecx, [ecx + LOCAL_PLAYER_INFO_OFFSET]
mov ecx, [ecx+4]
add ecx, 0x40 - 0x0C
cmp ecx, esi // ecx+0x40 == esi+0x0C
je hk_Return
hk_Disabled:
movss [esi + 0x0C], xmm0 // regs from the caller
hk_Return:
// Assuming we can destroy caller's ECX.
mov ecx, g_dwBase_Addr
add ecx, RETURN_UnfreezePlayer
jmp ecx
}