¿Cómo puedo contar la cantidad de veces que ocurre una secuencia de bytes en un archivo?

16

Quiero contar cuántas veces ocurre una determinada secuencia de bytes dentro de un archivo que tengo. Por ejemplo, quiero averiguar cuántas veces \0xdeadbeefaparece el número dentro de un archivo ejecutable. En este momento estoy haciendo eso usando grep:

#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file

(Los bytes están escritos en orden inverso porque mi CPU es little-endian)

Sin embargo, tengo dos problemas con mi enfoque:

  • Esas \Xnnsecuencias de escape solo funcionan en el caparazón de los peces.
  • grep realmente está contando el número de líneas que contienen mi número mágico. Si el patrón ocurre dos veces en la misma línea, solo contará una vez.

¿Hay alguna forma de solucionar estos problemas? ¿Cómo puedo hacer que este liner se ejecute en Bash Shell y cuente con precisión el número de veces que el patrón ocurre dentro del archivo?

hugomg
fuente
ayuda: unix.stackexchange.com/q/231213/117549 - específicamente,grep -o
Jeff Schaller
1
grep es la herramienta incorrecta para usar. Considere bgrep o bgrep2.
fpmurphy
3
Si la secuencia para buscar es 11221122, ¿cómo debería devolverse en una entrada 112211221122? ¿1 o 2?
Stéphane Chazelas
Estaría bien con informar 2 o 3 coincidencias en ese caso. Lo que sea más sencillo de implementar.
hugomg

Respuestas:

15

Esta es la solución de línea única solicitada (para shells recientes que tienen "sustitución de proceso"):

grep -o "ef be ad de" <(hexdump -v -e '/1 "%02x "' infile.bin) | wc -l

Si no hay una "sustitución de proceso" <(…)disponible, solo use grep como filtro:

hexdump -v -e '/1 "%02x "' infile.bin  | grep -o "ef be ad de" | wc -l

A continuación se muestra la descripción detallada de cada parte de la solución.

Valores de bytes de números hexadecimales:

Su primer problema es fácil de resolver:

Esas secuencias de escape \ Xnn solo funcionan en la concha de pescado.

Cambie el superior Xa uno inferior xy use printf (para la mayoría de los shells):

$ printf -- '\xef\xbe\xad\xde'

O usar:

$ /usr/bin/printf -- '\xef\xbe\xad\xde'

Para aquellos shells que eligen no implementar la representación '\ x'.

Por supuesto, traducir hexadecimal a octal funcionará en (casi) cualquier shell:

$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'

Donde "$ sh" es cualquier shell (razonable). Pero es bastante difícil mantenerlo correctamente citado.

Archivos binarios.

La solución más sólida es transformar el archivo y la secuencia de bytes (ambos) en alguna codificación que no tenga problemas con valores de caracteres impares como (nueva línea) 0x0Ao (byte nulo) 0x00. Ambos son bastante difíciles de administrar correctamente con herramientas diseñadas y adaptadas para procesar "archivos de texto".

Una transformación como base64 puede parecer válida, pero presenta el problema de que cada byte de entrada puede tener hasta tres representaciones de salida dependiendo de si es el primer, segundo o tercer byte de la posición mod 24 (bits).

$ echo "abc" | base64
YWJjCg==

$ echo "-abc" | base64
LWFiYwo=

$ echo "--abc" | base64
LS1hYmMK

$ echo "---abc" | base64        # Note that YWJj repeats.
LS0tYWJjCg==

Transformación hexadecimal.

Es por eso que la transformación más robusta debería ser una que comience en cada límite de byte, como la simple representación HEX.
Podemos obtener un archivo con la representación hexadecimal del archivo con cualquiera de estas herramientas:

$ 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

La secuencia de bytes para buscar ya está en hexadecimal en este caso.
:

$ var="ef be ad de"

Pero también podría ser transformado. A continuación se muestra un ejemplo de ida y vuelta hex-bin-hex:

$ echo "ef be ad de" | xxd -p -r | od -vAn -tx1
ef be ad de

La cadena de búsqueda se puede establecer a partir de la representación binaria. Cualquiera de las tres opciones presentadas anteriormente od, hexdump o xxd son equivalentes. Solo asegúrese de incluir los espacios para asegurarse de que la coincidencia esté en los límites de bytes (no se permite el cambio de mordisco):

$ a="$(printf "\xef\xbe\xad\xde" | hexdump -v -e '/1 "%02x "')"
$ echo "$a"
ef be ad de

Si el archivo binario se ve así:

$ 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

Luego, una simple búsqueda grep dará la lista de secuencias coincidentes:

$ grep -o "$a" infile.hex | wc -l
2

¿Una línea?

Todo se puede realizar en una línea:

$ grep -o "ef be ad de" <(xxd -c 1 -p infile.bin | tr '\n' ' ') | wc -l

Por ejemplo, buscar 11221122en el mismo archivo necesitará estos dos pasos:

$ a="$(printf '11221122' | hexdump -v -e '/1 "%02x "')"
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ') | wc -l
4

Para "ver" los partidos:

$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
3131323231313232
3131323231313232
3131323231313232
3131323231313232

$ grep "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')

… 0a 3131323231313232313132323131323231313232313132323131323231313232 313132320a


Tamponamiento

Existe la preocupación de que grep almacenará en el búfer todo el archivo y, si el archivo es grande, creará una gran carga para la computadora. Para eso, podemos usar una solución sed no tamponada:

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

El primer sed no tiene búfer ( -u) y se usa solo para inyectar dos nuevas líneas en la secuencia por cadena coincidente. El segundo sedsolo imprimirá las líneas coincidentes (cortas). El wc -l contará las líneas coincidentes.

Esto almacenará solo algunas líneas cortas. Las cadenas coincidentes en el segundo sed. Esto debería ser bastante bajo en recursos utilizados.

O, algo más complejo de entender, pero la misma idea en un sed:

a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin  |
    sed -u '/\n/P;//!s/'"$a"'/\n&\n/;D' |
        wc -l
sorontar
fuente
2
Tenga en cuenta que si coloca todo el texto en una línea, eso significa grepque terminará cargándolo completo en la memoria (aquí dos veces el tamaño del archivo original + 1 debido a la codificación hexadecimal), por lo que al final, termina siendo más sobrecarga que el pythonenfoque o el perlque tiene-0777 . También necesita una grepimplementación que admita líneas de longitud arbitraria (las que admiten -ogeneralmente lo hacen). Buena respuesta de lo contrario.
Stéphane Chazelas
1
¿Sus versiones hexadecimales coinciden con los valores de mordisco? E fb ea dd e? Además de los bytes deseados. od -An -tx1 | tr -d '\n'o hexdump -v -e '/1 " %02x"'con una cadena de búsqueda que también contenga espacios, evite esto, pero no veo esa solución xxd.
dave_thompson_085
@ dave_thompson_085 Respuesta editada. Creo que la respuesta solo coincidirá con los límites de bytes ahora, gracias de nuevo.
sorontar
@ StéphaneChazelas ¿Podría revisar la opción propuesta de usar un sed sin búfer? Gracias.
sorontar
sed -u(donde esté disponible) es para unbuffer. Eso significa que leerá un byte a la vez en la entrada y emitirá su salida de inmediato sin almacenar en búfer. En cualquier caso, aún tendrá que cargar toda la línea en el espacio del patrón, por lo que no ayudará aquí.
Stéphane Chazelas
7

Con GNU grep's -Pbandera (perl-regexp)

LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l

LC_ALL=Ces para evitar problemas en entornos locales de varios bytes donde, de greplo contrario, se trataría de interpretar secuencias de bytes como caracteres.

-atrata archivos binarios equivalentes a archivos de texto (en lugar del comportamiento normal, donde grepsolo imprime si hay al menos una coincidencia o no)

iruvar
fuente
Esta solución siempre me da 0 coincidencias en lugar del número correcto.
hugomg
@hugomg, ¿podría ser que necesites revertir los bytes pasados ​​para grep que coincida?
iruvar
No creo que sea el orden. Las otras dos respuestas a esta pregunta funcionan correctamente.
hugomg
2
@hugomg, es el local. Ver editar.
Stéphane Chazelas
2
Sugeriré incluir la -aopción, de lo contrario grep responderá con Binary file file.bin matchescualquier archivo que grep detecte como binario.
sorontar
6
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file

Que trata los archivos de entrada como binarios (no hay traducción para saltos de línea o codificaciones, consulte perlrun ) y luego recorre los archivos de entrada sin imprimir incrementando un contador para todas las coincidencias del hexadecimal dado (o cualquier forma, vea perlre ) .

thrig
fuente
2
Tenga en cuenta que no puede usar eso si la secuencia para buscar contiene el byte 0xa. En ese caso, puede usar un separador de registro diferente (con -0ooo).
Stéphane Chazelas
1
@ StéphaneChazelas puede usar la secuencia de interés en sí misma $/, con una compensación ligeramente diferente (uso de memoria proporcional a la distancia máxima entre tales secuencias):perl -nE 'BEGIN { $/ = "\xef\xbe\xad\xde" } chomp; $c++ unless eof && length; END { say $c }'
hobbs
@ StéphaneChazelas Lea mi respuesta para encontrar una solución para cualquier valor de byte.
sorontar
1
@hobbs, en cualquier caso, incluso aquí, el uso de memoria será proporcional a la distancia máxima entre dos bytes de 0xa que para archivos que no son de texto podría ser arbitrariamente grande.
Stéphane Chazelas
5

Con GNU awk, puedes hacer:

LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'

Si alguno de los bytes son operadores ERE, tendrían que escaparse (con \\). Como 0x2ecuál .debería ser ingresado como \\.o \\\x2e. Aparte de eso, debería funcionar con valores de bytes arbitrarios, incluidos 0 y 0xa.

Tenga en cuenta que no es tan simple como solo NR-1porque hay un par de casos especiales:

  • cuando la entrada está vacía, NR es 0, NR-1 daría -1.
  • cuando la entrada termina en el separador de registros, no se crea un registro vacío después de eso. Probamos para eso con RT=="".

También tenga en cuenta que en el peor de los casos (si el archivo no contiene el término de búsqueda), el archivo terminará cargándose completo en la memoria).

Stéphane Chazelas
fuente
5

La traducción más directa que veo es:

$ echo $'\xef\xbe\xad\xde' > hugohex
$ echo $'\xef\xbe\xad\xde\xef\xbe\xad\xde' >> hugohex
$ grep -F -a -o -e $'\xef\xbe\xad\xde' hugohex|wc -l
3

Cuando he utilizado $'\xef'como la fiesta de ANSI-citando (originalmente una ksh93función, ahora con el apoyo de zsh, bash, mksh, FreeBSD sh) versión de peces de \Xef, y se utiliza grep -o ... | wc -lpara contar los casos. grep -osaca cada partido en una línea separada. El -aindicador hace que grep se comporte en archivos binarios de la misma manera que en archivos de texto.-Fes para cadenas fijas, por lo que no necesita escapar de los operadores de expresiones regulares.

Como en su fishcaso, no puede usar ese enfoque si la secuencia a buscar incluye los bytes 0 o 0xa (nueva línea en ASCII).

Jeff Schaller
fuente
Usar printf '%b' $(printf '\\%o ' $((0xef)) $((0xbe)) $((0xad)) $((0xde))) > hugohex'sería el método más portátil de "shell puro". Por supuesto: printf "efbeadde" | xxd -p -r > hugohexparece el método más práctico.
sorontar
4

Puede usar el bytes.countmétodo de Python para obtener el número total de subcadenas no superpuestas en una cadena de bytes.

python -c "print(open('./myexecutable', 'rb').read().count(b'\xef\xbe\xad\xde'))"

Este one-liner cargará todo el archivo en la memoria, por lo que no es el más eficiente, pero funciona y es más legible que Perl; D

Nick T
fuente
'más legible que Perl' está a solo un paso de TECO, que es IINM: 239I$ 190I$ 173I$ 222I$ HXA ERfile$Y 0UC <:S^EQA$; %C$> QC=(gd & r)
dave_thompson_085
Puedes hacer mmap()un archivo en Python ; eso reduciría el compromiso de memoria.
Toby Speight
1
tr "$(printf \\0xef)\n" \\n\\0 < infile |
grep -c "^$(printf "\0xbe\0xad\0xde")"
mikeserv
fuente
1

Creo que puedes usar Perl, pruébalo:

perl -0777ne 'CORE::say STDOUT s/\xef\xbe\xad\xde//g' file_name  

El comando Reemplazar sda el número de reemplazos realizados, -0777 significa no tratar la nueva línea como un carácter especial, e- ejecutar el comando, saypara imprimir lo que sigue y luego imprimir el nuevo carácter de línea, nno había captado completamente, pero no funciona sin / desde docs:

hace que Perl asuma el siguiente ciclo alrededor de su programa, lo que lo hace iterar sobre argumentos de nombre de archivo algo así como sed -n o awk: LINE: while (<>) {... # su programa va aquí}

Alexei Martianov
fuente