
Я хочу посчитать, сколько раз определенная последовательность байтов встречается внутри файла, который у меня есть. Например, я хочу узнать, сколько раз число \0xdeadbeef
встречается внутри исполняемого файла. Прямо сейчас я делаю это с помощью grep:
#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file
(Байты записаны в обратном порядке, поскольку мой процессор имеет прямой порядок байтов)
Однако у меня есть две проблемы с моим подходом:
- Эти
\Xnn
последовательности действий работают только в рыбьей раковине. - grep на самом деле подсчитывает количество строк, содержащих мое магическое число. Если шаблон встречается дважды в одной строке, он будет засчитан только один раз.
Есть ли способ исправить эти проблемы? Как мне заставить этот однострочный код работать в оболочке 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
Ниже приведено подробное описание каждой части решения.
Значения байтов из шестнадцатеричных чисел:
Вашу первую проблему легко решить:
Эти управляющие последовательности \Xnn работают только в оболочке рыбы.
Измените верхнюю часть X
на нижнюю x
и используйте printf (для большинства оболочек):
$ printf -- '\xef\xbe\xad\xde'
Или используйте:
$ /usr/bin/printf -- '\xef\xbe\xad\xde'
Для тех оболочек, которые решили не реализовывать представление '\x'.
Конечно, преобразование шестнадцатеричного числа в восьмеричное будет работать на (почти) любой оболочке:
$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'
Где "$sh" - любая (разумная) оболочка. Но ее довольно сложно правильно закавычить.
Двоичные файлы.
Наиболее надежным решением является преобразование файла и последовательности байтов (обоих) в некоторую кодировку, которая не имеет проблем с нечетными значениями символов, такими как (новая строка) 0x0A
или (нулевой байт) 0x00
. Оба довольно сложно корректно обрабатывать с помощью инструментов, разработанных и адаптированных для обработки «текстовых файлов».
Преобразование типа base64 может показаться допустимым, но оно создает проблему, заключающуюся в том, что каждый входной байт может иметь до трех выходных представлений в зависимости от того, является ли он первым, вторым или третьим байтом позиции mod 24 (бит).
$ echo "abc" | base64
YWJjCg==
$ echo "-abc" | base64
LWFiYwo=
$ echo "--abc" | base64
LS1hYmMK
$ echo "---abc" | base64 # Note that YWJj repeats.
LS0tYWJjCg==
Шестнадцатеричное преобразование.
Вот почему наиболее надежное преобразование должно быть таким, которое начинается на каждой границе байта, как простое представление HEX.
Мы можем получить файл с шестнадцатеричным представлением файла с помощью любого из этих инструментов:
$ 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"
Но его также можно преобразовать. Ниже приведен пример кругового пути hex-bin-hex:
$ 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' ' ')
… 0а3131323231313232313132323131323231313232313132323131323231313232313132320а
Буферизация
Есть опасение, что 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
С флагом GNU grep
( -P
perl-regexp)
LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l
LC_ALL=C
заключается в том, чтобы избежать проблем в многобайтовых локалях, где grep
в противном случае последовательности байтов пытались бы интерпретировать как символы.
-a
обрабатывает двоичные файлы как эквивалентные текстовым файлам (вместо обычного поведения, когда grep
выводится только информация о том, есть ли хотя бы одно совпадение или нет)
решение3
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file
Который обрабатывает входные файлы как двоичные (без перевода строк или кодировок, см.perlrun) затем выполняет цикл по входным файлам, не печатая, увеличивая счетчик для всех совпадений заданного шестнадцатеричного числа (или любой другой формы, см.перлр).
решение4
С помощью GNU awk
вы можете сделать:
LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'
Если какие-либо из байтов являются операторами ERE, их придется экранировать (с помощью \\
). Например, 0x2e
which is .
нужно будет ввести как \\.
или \\\x2e
. В остальном он должен работать с произвольными значениями байтов, включая 0 и 0xa.
Обратите внимание, что все не так просто, NR-1
поскольку есть несколько особых случаев:
- когда вход пуст, NR равен 0, NR-1 даст -1.
- когда ввод заканчивается в разделителе записей, пустая запись после этого не создается. Мы проверяем это с помощью
RT==""
.
Также следует отметить, что в худшем случае (если файл не содержит искомого термина) файл будет загружен в память целиком.