
ファイル内で特定のバイト シーケンスが何回出現するかを数えたいと考えています。たとえば、\0xdeadbeef
実行可能ファイル内でその数字が何回出現するかを調べたいとします。現在、grep を使用してこれを行っています。
#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file
(私のCPUはリトルエンディアンなので、バイトは逆順に書き込まれます)
しかし、私のアプローチには 2 つの問題があります。
- これらの
\Xnn
エスケープ シーケンスは fish シェルでのみ機能します。 - grep は実際にはマジックナンバーを含む行の数をカウントします。パターンが同じ行に 2 回出現する場合は 1 回だけカウントされます。
これらの問題を解決する方法はありますか? このワンライナーを Bash シェルで実行し、ファイル内でパターンが出現する回数を正確にカウントするにはどうすればよいですか?
答え1
これは、要求されたワンライナーソリューションです(「プロセス置換」を備えた最近のシェルの場合):
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
以下にソリューションの各部分の詳細な説明を示します。
16 進数からのバイト値:
最初の問題は簡単に解決できます。
これらの \Xnn エスケープ シーケンスは fish シェルでのみ機能します。
上段をX
下段に変更しx
、printf を使用します (ほとんどのシェルの場合)。
$ printf -- '\xef\xbe\xad\xde'
または以下を使用します:
$ /usr/bin/printf -- '\xef\xbe\xad\xde'
'\x' 表現を実装しないことを選択したシェルの場合。
もちろん、16 進数を 8 進数に変換すると、(ほぼ) どのシェルでも機能します。
$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'
ここで、「$sh」は任意の(適切な)シェルです。ただし、これを正しく引用符で囲むのは非常に困難です。
バイナリ ファイル。
最も堅牢な解決策は、ファイルとバイト シーケンス (両方) を、 (改行)0x0A
や (null バイト)などの奇妙な文字値で問題が発生しないエンコードに変換することです0x00
。どちらも、「テキスト ファイル」を処理するように設計および調整されたツールでは正しく管理するのが非常に困難です。
base64 のような変換は有効なもののように思われるかもしれませんが、mod 24 (ビット) 位置の 1 番目、2 番目、または 3 番目のバイトであるかどうかに応じて、すべての入力バイトに最大 3 つの出力表現がある可能性があるという問題があります。
$ echo "abc" | base64
YWJjCg==
$ echo "-abc" | base64
LWFiYwo=
$ echo "--abc" | base64
LS1hYmMK
$ echo "---abc" | base64 # Note that YWJj repeats.
LS0tYWJjCg==
16進変換。
そのため、最も堅牢な変換は、単純な HEX 表現のように、各バイト境界から開始する変換である必要があります。
次のいずれかのツールを使用して、ファイルの 16 進表現を含むファイルを取得できます。
$ 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
この場合、検索するバイト シーケンスはすでに 16 進数になっています。
:
$ var="ef be ad de"
しかし、変換することもできます。16 進数 - バイナリ - 16 進数の往復の例を次に示します。
$ echo "ef be ad de" | xxd -p -r | od -vAn -tx1
ef be ad de
検索文字列はバイナリ表現から設定できます。上記の 3 つのオプション 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
一行ですか?
これらすべてを 1 行で実行できます。
$ grep -o "ef be ad de" <(xxd -c 1 -p infile.bin | tr '\n' ' ') | wc -l
たとえば、11221122
同じファイル内で検索するには、次の 2 つの手順が必要になります。
$ 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
)、一致する文字列ごとにストリームに 2 つの改行を挿入するためにのみ使用されます。2 番目は、sed
一致する (短い) 行のみを出力します。wc -l は一致する行をカウントします。
これにより、いくつかの短い行のみがバッファリングされます。2 番目の sed 内の一致する文字列。これにより、使用されるリソースはかなり少なくなるはずです。
または、理解するのは少し複雑ですが、同じアイデアを 1 つの 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
grep
GNUの-P
(perl-regexp)フラグ付き
LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l
LC_ALL=C
マルチバイト ロケールでgrep
バイトのシーケンスを文字として解釈しようとする場合に問題が発生するのを回避するためです。
-a
バイナリ ファイルをテキスト ファイルと同等に扱います (通常の動作では、grep
少なくとも 1 つの一致があるかどうかのみを出力します)
答え3
答え4
GNU を使用するとawk
、次のことが可能になります。
LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'
ただし、バイトのいずれかが ERE 演算子である場合は、エスケープする必要があります ( を使用\\
)。 たとえば、0x2e
はまたは.
として入力する必要があります。 それ以外は、0 および 0xa を含む任意のバイト値で動作するはずです。\\.
\\\x2e
NR-1
いくつかの特殊なケースがあるため、それほど単純ではないことに注意してください。
- 入力が空の場合、NR は 0 になり、NR-1 は -1 になります。
- 入力がレコード区切り文字で終わる場合、その後に空のレコードは作成されません。これを でテストします
RT==""
。
また、最悪の場合(ファイルに検索用語が含まれていない場合)、ファイル全体がメモリに読み込まれることになることに注意してください。