为了完整起见,我将首先简要回顾代码,即使 OP 清楚地了解发生了什么并且主要询问其背后的推理。
第一段代码可以很容易地用 C 编写,如下所示:
dword var_4 = &func1 - &func2 - 5;
这段代码本身就提出了一些问题,我们将在稍后回答,但首先让我们深入研究第二个程序集片段:
mov edx, [ebp+func2]
mov [edx], 0E9h ;E9 is opcode for jmp
的第一个字节func2
设置为0xE9
,这是“Jump near,relative,immediate”跳转的操作码。
mov eax, [ebp+func2]
mov ecx, [ebp+var_4]
mov [eax+1], ecx
然后,接下来的四个字节func
(1 到 5)被设置为先前在第一个片段中计算的偏移量。
现在,这可能会引发几个问题:
为什么偏移量然后减少了5
?
这样做是因为相对跳转是相对于下一条指令的,因此减去 5 会删除跳转指令本身的 5 个附加字节。一种更准确的查看方式是偏移量应从 计算&func2 + 5
。原方程 ( &func1 - &func2 - 5
) 显然与 相同&func1 - (&func2 + 5)
。
为什么我们一开始就如此关心指令长度?
所以,正如这里的一些人已经暗示的那样,勾手跳跃的长度很重要。这是非常正确的(尽管并没有说明相对跳跃偏好背后的全部原因)。钩子(或跳跃序列)的长度很重要,因为它会产生奇怪的边缘情况。正如人们可能假设的那样,这不仅仅是一些小的性能优化或保持简单。
一个重要的考虑因素是您需要替换您覆盖的任何指令。您用于跳转的那些字节是有意义的。它们必须保存在某个地方。覆盖更多字节意味着您必须在其他地方复制更多字节。例如,固定原始指令序列上的相关指令。你需要确保你没有留下一半的说明。
为什么使用相对跳转而不是绝对地址?
对不起,花了一段时间才到这里;D
我认为随着时间的推移,很多人会忽略或忘记这一点,但仔细查看跳转指令会发现,x86 跳转操作码缺乏接近、立即、绝对跳转。
我们在 x86 中有三种不同类型的跳转:
E9
对于近乎立即的偏移量(偏移量直接硬编码为指令本身内部的整数)。
FF /4
对于近乎绝对的跳跃。
- 我们已经得到
EA
了非常直接的绝对跳跃。
在远跳操作码(EA
)是缓慢的,主要用于改变段寄存器(其具有在保护模式完全不同的使用),则因此很少用作正常跳每本身,而是作为一个呼叫栅极,用于切换执行上下文等。
该绝对地址跳转的操作码(FF /4
)不接受立即值。它只能跳转到存储在寄存器中或存储在内存中的值。因此,使用它需要您:
- 将绝对偏移量存储在某个保留的内存空间中,由钩子例程专门为此目的为每个钩子函数分配,或
- 硬编码寄存器加载指令,该指令会将寄存器设置为绝对值。像
mov eax, <absolute value> / jump eax
或 之类的东西push <absolute value> / ret
。
理解这一点,很明显,使用近、直接、相对跳跃比这两种方法都容易得多。
因此,尽管准确地说使用绝对地址将需要更长的指令序列,但这并不能说明全部情况。
这又引出另一个问题:
那么,为什么在 x86 中没有近乎、直接、绝对的跳跃呢?
简单的答案是没有。人们可以推测指令集设计决策背后的推理,但添加指令既昂贵又复杂。我认为没有真正需要近乎绝对的立即跳转,因为这确实是一种罕见的情况,您需要跳转到提前已知的地址,而相对跳转是行不通的。