除非使用某些寄存器,否则函数挂钩会崩溃
Function hook crashes unless certain registers are used
所以我正在尝试为游戏挂钩一个函数,但有一个小问题。如果 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
}
}
我认为崩溃可能是由于我的汇编代码覆盖了寄存器 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
指令,并在 [=13= 相同的位置添加 pop ebx
指令] 操作说明。但是请注意,由于栈的工作方式,push和pop指令必须倒序,像这样:
挂钩开始:
push eax
push ebx
钩端:
pop ebx
pop eax
If registers such as eax, ebx, ecx, and edx are interchangeable, how come the first code sample below is crashing the game process but the second code does not crash and works as intended?
可能你的调用者在这个函数跳转到 g_dwBase_Addr + RETURN_UnfreezePlayer
之后使用 EBX 做一些重要的事情。
如果您正在挂钩现有的函数调用,那么 EAX、ECX 和 EDX 在标准调用约定中被调用破坏,而其他整数 regs 调用被保留。
当你销毁 EBP 时,你的调用者恰好不会中断,只有当你销毁 EBX 时,这似乎是合理的。
或者,如果您要将 jump/call 插入到此代码中根本不需要函数调用的地方,那么您应该 save/restore 您修改的每个寄存器,可能包括 EFLAGS。 (查看 "call site" 以查看它是否在您 "return" 之后销毁任何寄存器;例如 add
或 cmp
仅写入 EFLAGS,而不读取,因此如果您看到一条指令这样你就知道你不必 save/restore EFLAGS。同样,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
}
您正在使用寄存器保存跳跃目标。您也许能够分析 "caller" 并找到一个可以安全销毁的寄存器,这样您就可以使用那个 without/save 恢复。或者硬编码跳转目标地址,这样你就可以使用 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
指令在调用堆栈上的分支预测错误,因为我们得到 call/ret 预测器堆栈不匹配。这个函数中某处的虚拟 call
可以解决这个问题,只是 this ret
预测错误。 (但请注意,call next_instruction
不会工作;CPU 的特殊情况不会将其视为真正的调用。您必须实际跳过某些内容。http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0)
您可能会想 xchg [esp], eax
/ ret
,但那是 非常 慢:带有内存操作数的 xchg 意味着 lock
前缀(全内存屏障,微码原子交换)。
在最初推送时为 "return address" 保留一个插槽似乎最有效,否则你可能会推送一个 return 地址,mov
加载保存的 EAX 值,然后 pop [esp+4]
将 return 地址向上复制 4 个字节。但是在发现 ret
错误预测之前,额外的副本会增加延迟。
如果这不必是线程安全的,您可以在存储目标地址后使用jmp [target_address]
。或者,如果 g_dwBase_Addr + RETURN_UnfreezePlayer
是一个常量,只需将其保存在某处的静态变量中,这样您就可以 jmp dword ptr [target_address]
而不是每次都计算目标。
您可以使用低于 ESP 的 space,但这并不绝对安全。就像恢复寄存器后的 jmp [esp-4]
一样。 SEH 可以踩到它,调试器也可以。
您可以优化您的函数以使用更少的寄存器
具体修改一个即可,save/restore即可。或者 none 如果你选择一个,你可以安全地破坏 return 地址。
这两条指令之后,你再也不用EAX了。
mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]
所以你可以使用 EAX 而不是 EBX:
mov eax, g_dwBase_Addr
mov eax, [eax + LOCAL_PLAYER_INFO_OFFSET]
; then use EAX everywhere you were using EBX in later instructions
因此 save/restore 的注册数较少。另外,这毫无意义:
add ebx, 0x4 ; add eax, 4 // with changes from above
mov ecx, [ebx] ; mov ecx, [eax]
可以在寻址方式下做+4
:mov ecx, [eax + 4]
add/lea -> cmp
也可以优化。 ecx + 0x40 == esi + 0xc
与 ecx + 0x40 - 0xc == esi
相同。
// 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
}
所以我正在尝试为游戏挂钩一个函数,但有一个小问题。如果 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
}
}
我认为崩溃可能是由于我的汇编代码覆盖了寄存器 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
指令,并在 [=13= 相同的位置添加 pop ebx
指令] 操作说明。但是请注意,由于栈的工作方式,push和pop指令必须倒序,像这样:
挂钩开始:
push eax
push ebx
钩端:
pop ebx
pop eax
If registers such as eax, ebx, ecx, and edx are interchangeable, how come the first code sample below is crashing the game process but the second code does not crash and works as intended?
可能你的调用者在这个函数跳转到 g_dwBase_Addr + RETURN_UnfreezePlayer
之后使用 EBX 做一些重要的事情。
如果您正在挂钩现有的函数调用,那么 EAX、ECX 和 EDX 在标准调用约定中被调用破坏,而其他整数 regs 调用被保留。
当你销毁 EBP 时,你的调用者恰好不会中断,只有当你销毁 EBX 时,这似乎是合理的。
或者,如果您要将 jump/call 插入到此代码中根本不需要函数调用的地方,那么您应该 save/restore 您修改的每个寄存器,可能包括 EFLAGS。 (查看 "call site" 以查看它是否在您 "return" 之后销毁任何寄存器;例如 add
或 cmp
仅写入 EFLAGS,而不读取,因此如果您看到一条指令这样你就知道你不必 save/restore EFLAGS。同样,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
}
您正在使用寄存器保存跳跃目标。您也许能够分析 "caller" 并找到一个可以安全销毁的寄存器,这样您就可以使用那个 without/save 恢复。或者硬编码跳转目标地址,这样你就可以使用 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
指令在调用堆栈上的分支预测错误,因为我们得到 call/ret 预测器堆栈不匹配。这个函数中某处的虚拟 call
可以解决这个问题,只是 this ret
预测错误。 (但请注意,call next_instruction
不会工作;CPU 的特殊情况不会将其视为真正的调用。您必须实际跳过某些内容。http://blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0)
您可能会想 xchg [esp], eax
/ ret
,但那是 非常 慢:带有内存操作数的 xchg 意味着 lock
前缀(全内存屏障,微码原子交换)。
在最初推送时为 "return address" 保留一个插槽似乎最有效,否则你可能会推送一个 return 地址,mov
加载保存的 EAX 值,然后 pop [esp+4]
将 return 地址向上复制 4 个字节。但是在发现 ret
错误预测之前,额外的副本会增加延迟。
如果这不必是线程安全的,您可以在存储目标地址后使用jmp [target_address]
。或者,如果 g_dwBase_Addr + RETURN_UnfreezePlayer
是一个常量,只需将其保存在某处的静态变量中,这样您就可以 jmp dword ptr [target_address]
而不是每次都计算目标。
您可以使用低于 ESP 的 space,但这并不绝对安全。就像恢复寄存器后的 jmp [esp-4]
一样。 SEH 可以踩到它,调试器也可以。
您可以优化您的函数以使用更少的寄存器
具体修改一个即可,save/restore即可。或者 none 如果你选择一个,你可以安全地破坏 return 地址。
这两条指令之后,你再也不用EAX了。
mov eax, g_dwBase_Addr
mov ebx, [eax + LOCAL_PLAYER_INFO_OFFSET]
所以你可以使用 EAX 而不是 EBX:
mov eax, g_dwBase_Addr
mov eax, [eax + LOCAL_PLAYER_INFO_OFFSET]
; then use EAX everywhere you were using EBX in later instructions
因此 save/restore 的注册数较少。另外,这毫无意义:
add ebx, 0x4 ; add eax, 4 // with changes from above
mov ecx, [ebx] ; mov ecx, [eax]
可以在寻址方式下做+4
:mov ecx, [eax + 4]
add/lea -> cmp
也可以优化。 ecx + 0x40 == esi + 0xc
与 ecx + 0x40 - 0xc == esi
相同。
// 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
}