我目前正在编写一个内存扫描程序,用于扫描另一个进程中的AOB。 aob包含通配符,由一个看起来像39 35 ?? ?? ?? ?? 75 10 6A 01 E8
这是我到目前为止所拥有的:
currentAddress
变量,它存储了我正在查看的地址。 我的算法采取天真的方式;它暴力强迫问题并搜索所有可能的位置。代码如下:
char *haystack = .....
short *needle = .... //"39 35 ?? ?? ?? ?? 75 10 6A 01 E8"
outer:for(int i = 0; i < lengthOfHayStack - lengthOfNeedle; i ++)
{
for(int j = 0; j < lengthOfNeedle; j ++)
{
if(buffer[i+j] != needle[j] && needle[j] != WILDCARD)
continue outer;
}
//found one?
}
这是算法明智的。实施明智,我首先使用repne scasb
来查找大海捞针的第一个字节。此过程由内联汇编完成。找到索引后,我使用c代码来比较其余部分,因为我需要处理外卡。
我的Memory Scanner的性能还可以,但我仍然希望改进它。有哪些方法,无论是算法方式还是实现方式,都可以加速我的内存扫描器?
PS:AOB的模块未知。因此,我必须扫描整个内存区域。
答案 0 :(得分:3)
1)此处的其他答案建议建立DFA,即线性时间。 您可以构建Knuth-Morris-Pratt search,并在许多情况下实现次线性次。它跳过可以包含模式的内存块,基于它在跳过的块之前已经看到的位。如果你想要这么快,我想你会发现核心算法必须用汇编语言编码。
2)我不想从目标进程空间中读取块(需要通过内核进行复制),而是试图将虚拟页面从目标空间映射到搜索器的空间。你可以使这些页面相当大(16Mb?),这可以分摊映射成本;复制成本为零。
答案 1 :(得分:2)
将搜索模式视为正则表达式,并将其转换为Deterministic Finite Automaton或DFA
。除了这个维基百科条目,你应该找到大量的谷歌食物来调查。
基本上,搜索模式会转换为状态机。状态机的输入是来自您正在搜索的存储器的字节流,自动机的最终状态是遇到搜索模式后达到的状态。
在数学上不可能提出逻辑上更快的算法,因为状态机的输入只是对内存范围的线性扫描,而不是当前代码中的嵌套循环方法。搜索复杂度应为O(n),与搜索的内存大小成线性关系。在这里,不要认为它在理论上可以实现更好的复杂性。
正则表达式基本上是nondeterministic finite automaton或NFA
(如本文引用的维基百科条目所示),它使用最方便的算法转换为确定性有限自动机。然后,要扫描的内存范围成为DFA状态机的输入,一旦达到DFA的最终状态,就会找到该模式。
std::regex_search
使用一对双向迭代器来定义使用正则表达式搜索的序列。
定义并实现一个迭代器类,该类满足双向迭代器的要求,并迭代您要搜索的内存区域。将搜索模式转换为std::regex
,并使用std::regex_search
进行搜索。
通过正则表达式库的正式定义的简短扫描似乎并不表示std::regex_search
保证某种类型的最大复杂性(我可能在这里错了,我没有执行穷尽搜索整个图书馆规范);此外,它需要双向迭代器,而不是输入或转发迭代器,这表明实现可能不如沼泽标准DFA那么高效,但实际上,可能需要最少量的工作,以获得相当快的结果。
答案 2 :(得分:0)
repne scasb
isn't faster than a plain byte-at-a-time loop, unfortunately.
使用向量指令扫描起始字节会好得多:
使用pcmpeqb
一次检查整个向量以找到匹配的起始字节。使用匹配的位位置作为偏移量来加载完整匹配候选项。 (未对齐的加载远比尝试执行数据相关的移位或随机播放更容易,因为palignr
仅在立即计数时可用。索引pshufb
shuffle表掩码是可能的,但没有帮助,因为无论如何你需要加载更多。
# load your search pattern into xmm4
#broadcast the first byte to every byte of xmm5
# then
.loop:
...
vpcmpeqb xmm0, xmm5, [rsi]
vpmovmskb ecx, xmm0
test ecx,ecx
jnz .found_a_0x39_byte
.resume_search:
add rsi, 16
cmp rsi, rdi # end pointer
jb .loop
...
.found_a_0x39_byte
bsf edx, ecx
vpcmpeqb xmm0, xmm4, [rsi+rdx] ; check against the full pattern (unaligned load, use movdqu if implementing without avx)
vpmovmskb eax, xmm0
; eax has a one bit for every matching byte
; "39 35 ?? ?? ?? ?? 75 10 6A 01 E8"
;0b 1 1 0 0 0 0 1 1 1 1 1 reversed because little endian
not eax ; 0 bits are matching bytes
test eax, 0b11111000011 ; check that all bits we care about are zero
jnz .try_again_with_next_set_bit_in_ecx ; TODO implement this loop
# .found_match:
add rdx, rsi ; pointer to the start of the match
您需要在ecx中循环设置位位置,以检查所有候选起点。或者可以通过检查模式的第二个字节进行优化,将该位掩码左移一个,并将其与第一个位掩码进行AND运算。然后你只有一个掩码,其中只有0x39后跟一个0x35的位置。
循环设置位:BMI1&#39; s BLSR
将清除源中的最低设置位,如果结果为零,则设置ZF
。它可能会有所帮助。 (如果源头为零,它也会设置CF
,但这里没有用处)。如果您无法使用BMI1,there are other ways to clear the lowest bit。
注意bsf
如果输入为零,则设置ZF,即使在这种情况下输出寄存器未定义。 (在这种情况下,使用BMI1&#39; tzcnt
获得32
或64
的保证结果。来自C的更多功能(函数无法返回值)和布尔值),但并不总是asm的改进。)
你可能很容易对内存带宽产生瓶颈,所以可能会做类似
的事情vpcmpeqw xmm0, xmm5, [rsi]
vpcmpeqw xmm1, xmm5, [rsi+1]
当你找到候选的两个字节序列时,才会突破主搜索循环。但是,这将导致Sandybridge的L1中的缓存库冲突。它只能从128B块(2个缓存线)的相同1/8的每个时钟服务一个负载。英特尔Haswell以及后来没有缓存库冲突。理论上,SnB 可以通过仅使用对齐的加载来获胜,并使用palignr
来获得第二次检查的未对齐加载。这可能是问题。在SnB之前很好,只有一个加载端口,并且你也希望使用数据进行对齐检查。
为了利用库函数进行繁重的工作,GNU libc提供了memmem
。它与strstr
类似,但采用显式大小而不是对以null结尾的字符串进行操作。你是在Windows上,但也许有类似的功能,它具有矢量优化的实现。在75 10 6A 01 E8
序列上使用它来查找潜在的最终候选者。
在块之间的边界处,可能只是做一些手动一次一字节检查?或者使用palignr
以两种可能的方式将一个块的最后16B与下一个块的前16B组合在一起?
如果从块的末尾开始小于11B的0x39,可能只会执行palignr
吗?