我想计算某个特定字节序列在我拥有的文件中发生的次数。例如,我想找出该数字\0xdeadbeef
在可执行文件中出现了多少次。现在我正在使用 grep 执行此操作:
#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file
(字节以相反的顺序写入,因为我的CPU是little-endian)
但是,我的方法有两个问题:
- 这些
\Xnn
转义序列仅在鱼壳中起作用。 - grep 实际上是在计算包含我的幻数的行数。如果该模式在同一行中出现两次,则仅计算一次。
有办法解决这些问题吗?如何使这一行在 Bash shell 中运行并准确计算该模式在文件内出现的次数?
答案1
这是所请求的单行解决方案(对于最近具有“进程替换”的 shell):
grep -o "ef be ad de" <(hexdump -v -e '/1 "%02x "' infile.bin) | wc -l
如果没有可用的“进程替换” <(…)
,只需使用 grep 作为过滤器:
hexdump -v -e '/1 "%02x "' infile.bin | grep -o "ef be ad de" | wc -l
下面是该解决方案各部分的详细说明。
来自十六进制数字的字节值:
你的第一个问题很容易解决:
这些 \Xnn 转义序列仅在 Fish shell 中有效。
将上部更改X
为下部x
并使用 printf (对于大多数 shell):
$ printf -- '\xef\xbe\xad\xde'
或者使用:
$ /usr/bin/printf -- '\xef\xbe\xad\xde'
对于那些选择不实现 '\x' 表示的 shell。
当然,将十六进制转换为八进制将适用于(几乎)任何 shell:
$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'
其中“$sh”是任何(合理的)shell。但要正确引用它是相当困难的。
二进制文件。
0x0A
最可靠的解决方案是将文件和字节序列(两者)转换为某种编码,这种编码对于奇数字符值(如 (new line)或 (null byte) )没有问题0x00
。使用设计和适应处理“文本文件”的工具来正确管理两者都相当困难。
像 base64 这样的转换可能看起来是有效的,但它提出了一个问题,即每个输入字节可能有最多三个输出表示,具体取决于它是 mod 24(位)位置的第一个、第二个还是第三个字节。
$ echo "abc" | base64
YWJjCg==
$ echo "-abc" | base64
LWFiYwo=
$ echo "--abc" | base64
LS1hYmMK
$ echo "---abc" | base64 # Note that YWJj repeats.
LS0tYWJjCg==
十六进制变换。
这就是为什么最强大的转换应该是从每个字节边界开始的转换,就像简单的十六进制表示一样。
我们可以使用以下任何工具获取具有文件十六进制表示形式的文件:
$ od -vAn -tx1 infile.bin | tr -d '\n' > infile.hex
$ hexdump -v -e '/1 "%02x "' infile.bin > infile.hex
$ xxd -c1 -p infile.bin | tr '\n' ' ' > infile.hex
在这种情况下,要搜索的字节序列已经是十六进制。
:
$ var="ef be ad de"
但它也可以转变。往返十六进制-二进制-十六进制的示例如下:
$ echo "ef be ad de" | xxd -p -r | od -vAn -tx1
ef be ad de
搜索字符串可以根据二进制表示来设置。上面提供的三个选项中的任何一个 od、hexdump 或 xxd 都是等效的。只需确保包含空格以确保匹配位于字节边界上(不允许半字节移位):
$ a="$(printf "\xef\xbe\xad\xde" | hexdump -v -e '/1 "%02x "')"
$ echo "$a"
ef be ad de
如果二进制文件如下所示:
$ cat infile.bin | xxd
00000000: 5468 6973 2069 7320 efbe adde 2061 2074 This is .... a t
00000010: 6573 7420 0aef bead de0a 6f66 2069 6e70 est ......of inp
00000020: 7574 200a dead beef 0a66 726f 6d20 6120 ut ......from a
00000030: 6269 0a6e 6172 7920 6669 6c65 2e0a 3131 bi.nary file..11
00000040: 3232 3131 3232 3131 3232 3131 3232 3131 2211221122112211
00000050: 3232 3131 3232 3131 3232 3131 3232 3131 2211221122112211
00000060: 3232 0a
然后,简单的 grep 搜索将给出匹配序列的列表:
$ grep -o "$a" infile.hex | wc -l
2
一条线?
这一切都可以在一行中执行:
$ grep -o "ef be ad de" <(xxd -c 1 -p infile.bin | tr '\n' ' ') | wc -l
例如,11221122
在同一个文件中搜索将需要以下两个步骤:
$ a="$(printf '11221122' | hexdump -v -e '/1 "%02x "')"
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ') | wc -l
4
要“查看”比赛:
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
3131323231313232
3131323231313232
3131323231313232
3131323231313232
$ grep "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
… 0a3131323231313232313132323131323231313232313132323131323231313232313132320a
缓冲
人们担心 grep 会缓冲整个文件,如果文件很大,会给计算机带来沉重的负载。为此,我们可以使用无缓冲的 sed 解决方案:
a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin |
sed -ue 's/\('"$a"'\)/\n\1\n/g' |
sed -n '/^'"$a"'$/p' |
wc -l
第一个 sed 是无缓冲的 ( -u
),仅用于在每个匹配字符串的流上注入两个换行符。第二个sed
只会打印(短)匹配行。 wc -l 将计算匹配的行数。
这只会缓冲一些短行。第二个 sed 中的匹配字符串。这所使用的资源应该相当低。
或者,理解起来有些复杂,但在一个 sed 中具有相同的想法:
a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin |
sed -u '/\n/P;//!s/'"$a"'/\n&\n/;D' |
wc -l
答案2
使用 GNUgrep
的-P
(perl-regexp) 标志
LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l
LC_ALL=C
是为了避免多字节语言环境中出现问题,grep
否则会尝试将字节序列解释为字符。
-a
将二进制文件视为等同于文本文件(而不是正常行为,其中grep
仅打印出是否至少有一个匹配项)
答案3
答案4
使用 GNU awk
,您可以执行以下操作:
LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'
如果任何字节是 ERE 运算符,则必须对它们进行转义(使用\\
)。就像0x2e
which is.
必须输入为\\.
or \\\x2e
。除此之外,它应该适用于任意字节值,包括 0 和 0xa。
请注意,这并不只是NR-1
因为有一些特殊情况那么简单:
- 当输入为空时,NR为0,NR-1将给出-1。
- 当输入以记录分隔符结束时,此后不会创建空记录。我们用 来测试这一点
RT==""
。
另请注意,在最坏的情况下(如果文件不包含搜索词),文件最终将被整个加载到内存中)。