Comment puis-je compter le nombre de fois où une séquence d'octets se produit dans un fichier?

16

Je veux compter combien de fois une certaine séquence d'octets se produit dans un fichier que j'ai. Par exemple, je veux savoir combien de fois le nombre \0xdeadbeefse produit dans un fichier exécutable. En ce moment, je fais cela en utilisant grep:

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

(Les octets sont écrits dans l'ordre inverse car mon processeur est peu endian)

Cependant, j'ai deux problèmes avec mon approche:

  • Ces \Xnnséquences d'échappement ne fonctionnent que dans la coquille du poisson.
  • grep compte en fait le nombre de lignes qui contiennent mon nombre magique. Si le motif se produit deux fois sur la même ligne, il ne comptera qu'une seule fois.

Existe-t-il un moyen de résoudre ces problèmes? Comment puis-je faire fonctionner cette doublure dans le shell Bash et compter avec précision le nombre de fois que le motif se produit à l'intérieur du fichier?

hugomg
la source
un peu d'aide: unix.stackexchange.com/q/231213/117549 - en particulier,grep -o
Jeff Schaller
1
grep n'est pas le bon outil à utiliser. Considérez bgrep ou bgrep2.
fpmurphy
3
Si la séquence à rechercher est 11221122, que doit-on renvoyer sur une entrée comme 112211221122? 1 ou 2?
Stéphane Chazelas
Je serais d'accord pour rapporter 2 ou 3 correspondances dans ce cas. Quel que soit le plus simple à mettre en œuvre.
hugomg

Réponses:

15

Il s'agit de la solution monoplace demandée (pour les shells récents qui ont une "substitution de processus"):

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

Si aucune "substitution de processus" <(…)n'est disponible, utilisez simplement grep comme filtre:

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

Vous trouverez ci-dessous la description détaillée de chaque partie de la solution.

Valeurs d'octets à partir de nombres hexadécimaux:

Votre premier problème est facile à résoudre:

Ces séquences d'échappement \ Xnn ne fonctionnent que dans la coquille du poisson.

Changez le haut Xen bas xet utilisez printf (pour la plupart des shells):

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

Ou utiliser:

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

Pour les shells qui choisissent de ne pas implémenter la représentation '\ x'.

Bien sûr, traduire hex en octal fonctionnera sur (presque) n'importe quel shell:

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

Où "$ sh" est un shell (raisonnable). Mais il est assez difficile de le garder correctement cité.

Fichiers binaires.

La solution la plus robuste consiste à transformer le fichier et la séquence d'octets (les deux) en un encodage qui n'a aucun problème avec les valeurs de caractères impaires comme (nouvelle ligne) 0x0Aou (octet nul) 0x00. Les deux sont assez difficiles à gérer correctement avec des outils conçus et adaptés pour traiter des "fichiers texte".

Une transformation comme base64 peut sembler valide, mais elle présente le problème que chaque octet d'entrée peut avoir jusqu'à trois représentations de sortie selon qu'il s'agit du premier, du deuxième ou du troisième octet de la position du mod 24 (bits).

$ echo "abc" | base64
YWJjCg==

$ echo "-abc" | base64
LWFiYwo=

$ echo "--abc" | base64
LS1hYmMK

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

Transformation hexagonale.

C'est pourquoi la transformation la plus robuste devrait être celle qui commence sur chaque frontière d'octet, comme la simple représentation HEX.
Nous pouvons obtenir un fichier avec la représentation hexadécimale du fichier avec l'un ou l'autre de ces outils:

$ 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 séquence d'octets à rechercher est déjà en hexadécimal dans ce cas.
:

$ var="ef be ad de"

Mais il pourrait aussi être transformé. Voici un exemple d'hex-bin-hex aller-retour:

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

La chaîne de recherche peut être définie à partir de la représentation binaire. Chacune des trois options présentées ci-dessus od, hexdump ou xxd sont équivalentes. Assurez-vous simplement d'inclure les espaces pour vous assurer que la correspondance est sur les limites d'octets (aucun décalage de quartet autorisé):

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

Si le fichier binaire ressemble à ceci:

$ 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

Ensuite, une simple recherche grep donnera la liste des séquences correspondantes:

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

Une ligne?

Tout peut être exécuté sur une seule ligne:

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

Par exemple, la recherche 11221122dans le même fichier nécessitera ces deux étapes:

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

Pour "voir" les matchs:

$ 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


Mise en mémoire tampon

Il est à craindre que grep ne mette en mémoire tampon tout le fichier et, si le fichier est volumineux, crée une lourde charge pour l'ordinateur. Pour cela, nous pouvons utiliser une solution sed sans tampon:

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

Le premier sed est sans tampon ( -u) et n'est utilisé que pour injecter deux sauts de ligne sur le flux par chaîne correspondante. La seconde sedn'imprimera que les (courtes) lignes correspondantes. Le wc -l comptera les lignes correspondantes.

Cela ne mettra en mémoire tampon que quelques lignes courtes. La ou les cordes correspondantes dans le deuxième sed. Cela devrait être assez faible dans les ressources utilisées.

Ou, un peu plus complexe à comprendre, mais la même idée dans 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
la source
2
Notez que si vous mettez tout le texte sur une seule ligne, cela signifie grepqu'il finira par le charger en entier (ici deux fois la taille du fichier d'origine + 1 à cause de l'encodage hexadécimal), donc à la fin, cela finit par être plus les frais généraux que l' pythonapproche ou perlcelui avec -0777. Vous avez également besoin d'une grepimplémentation qui prend en charge les lignes de longueur arbitraire (celles qui le prennent -ogénéralement en charge ). Bonne réponse sinon.
Stéphane Chazelas
1
Vos versions hexadécimales correspondent aux valeurs décalées de quartet? E fb ea dd e? en plus des octets souhaités. od -An -tx1 | tr -d '\n'ou hexdump -v -e '/1 " %02x"'avec une chaîne de recherche contenant également des espaces, évitez cela, mais je ne vois pas de tel correctif pour xxd.
dave_thompson_085
@ dave_thompson_085 Réponse modifiée. Je crois que la réponse ne correspondra qu'aux limites d'octets maintenant, merci encore.
sorontar
@ StéphaneChazelas Pourriez-vous revoir l'option proposée d'utiliser un sed sans tampon. Merci.
sorontar
sed -u(le cas échéant) est pour la mémoire tampon. Cela signifie qu'il lira un octet à la fois en entrée et produira sa sortie immédiatement sans mise en mémoire tampon. Dans tous les cas, il faudra toujours charger toute la ligne dans l'espace de motif, donc cela ne sera pas utile ici.
Stéphane Chazelas
7

Avec GNU grepde -P(perl-regexp) drapeau

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

LC_ALL=Cest d'éviter les problèmes dans les paramètres régionaux multi-octets grepqui, sinon, essaieraient d'interpréter des séquences d'octets comme des caractères.

-atraite les fichiers binaires équivalents aux fichiers texte (au lieu du comportement normal, où grepne s'affiche que s'il y a au moins une correspondance ou non)

iruvar
la source
Cette solution me donne toujours 0 correspondances au lieu du nombre correct.
hugomg
@hugomg, se pourrait-il que vous ayez besoin d'inverser les octets passés pour grep le faire correspondre?
iruvar
Je ne pense pas que ce soit l'ordre. Les deux autres réponses à cette question fonctionnent correctement.
hugomg
2
@hugomg, c'est la locale. Voir modifier.
Stéphane Chazelas
2
Je proposerai d'inclure l' -aoption, sinon grep répondra avec Binary file file.bin matchespour tout fichier que grep détecte comme binaire.
sorontar
6
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file

Qui traite le (s) fichier (s) d'entrée comme binaire (pas de traduction pour les sauts de ligne ou les encodages, voir perlrun ) puis boucle sur le (s) fichier (s) d'entrée n'imprimant pas en incrémentant un compteur pour toutes les correspondances de l'hex donné (ou quelle que soit la forme, voir perlre ) .

branler
la source
2
Notez que vous ne pouvez pas l'utiliser si la séquence à rechercher contient l'octet 0xa. Dans ce cas, vous pouvez utiliser un séparateur d'enregistrement différent (avec -0ooo).
Stéphane Chazelas
1
@ StéphaneChazelas vous pouvez utiliser la séquence d'intérêt elle-même car $/, avec un compromis légèrement différent (utilisation de la mémoire proportionnelle à la distance maximale entre de telles séquences):perl -nE 'BEGIN { $/ = "\xef\xbe\xad\xde" } chomp; $c++ unless eof && length; END { say $c }'
hobbs
@ StéphaneChazelas Veuillez lire ma réponse pour une solution pour toutes les valeurs d'octets.
sorontar
1
@hobbs, dans tous les cas, même ici, l'utilisation de la mémoire sera proportionnelle à la distance maximale entre deux octets 0xa qui, pour les fichiers non textuels, pourrait être arbitrairement grande.
Stéphane Chazelas
5

Avec GNU awk, vous pouvez faire:

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

Si l'un des octets est un opérateur ERE, il doit être échappé (avec \\). Comme 0x2ece qui .devrait être entré comme \\.ou \\\x2e. En dehors de cela, cela devrait fonctionner avec des valeurs d'octets arbitraires, y compris 0 et 0xa.

Notez que ce n'est pas aussi simple que simplement NR-1parce qu'il existe quelques cas spéciaux:

  • lorsque l'entrée est vide, NR est 0, NR-1 donnerait -1.
  • lorsque l'entrée se termine dans le séparateur d'enregistrement, un enregistrement vide n'est pas créé par la suite. Nous testons cela avec RT=="".

Notez également que dans le pire des cas (si le fichier ne contient pas le terme de recherche), le fichier finira par être chargé entier en mémoire).

Stéphane Chazelas
la source
5

La traduction la plus simple que je vois est:

$ 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

Là où je l' ai utilisé $'\xef'comme bash ANSI-citant ( à l' origine une ksh93fonctionnalité, désormais pris en charge par zsh, bash, mksh, FreeBSD shversion) de ce poisson \Xef, et utilisé grep -o ... | wc -lpour compter les cas. grep -oaffiche chaque correspondance sur une ligne distincte. Le -adrapeau fait que grep se comporte sur les fichiers binaires de la même manière que sur les fichiers texte. -Fest pour les chaînes fixes, vous n'avez donc pas besoin d'échapper aux opérateurs regex.

Comme dans votre fishcas, vous ne pouvez pas utiliser cette approche si la séquence à rechercher inclut les octets 0 ou 0xa (nouvelle ligne en ASCII).

Jeff Schaller
la source
L'utilisation printf '%b' $(printf '\\%o ' $((0xef)) $((0xbe)) $((0xad)) $((0xde))) > hugohex'serait la méthode la plus portable de "pure shell". Bien sûr: cela printf "efbeadde" | xxd -p -r > hugohexsemble être la méthode la plus pratique.
sorontar
4

Vous pouvez utiliser la bytes.countméthode de Python pour obtenir le nombre total de sous-chaînes qui ne se chevauchent pas dans un bytestring.

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

Ce one-liner chargera le fichier entier en mémoire, donc pas le plus efficace, mais fonctionne et est plus lisible que Perl; D

Nick T
la source
'plus lisible que Perl' n'est qu'à un pas de TECO - ce que IINM est: 239I$ 190I$ 173I$ 222I$ HXA ERfile$Y 0UC <:S^EQA$; %C$> QC=(gd & r)
dave_thompson_085
Vous pouvez mmap()un fichier en Python ; cela réduirait la validation de la mémoire.
Toby Speight
1
tr "$(printf \\0xef)\n" \\n\\0 < infile |
grep -c "^$(printf "\0xbe\0xad\0xde")"
mikeserv
la source
1

Je pense que vous pouvez utiliser Perl, essayez-le:

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

Remplacer la commande sdonne le nombre de remplacements effectués, -0777 signifie ne pas traiter la nouvelle ligne comme un caractère spécial, e- exécuter la commande, saypour imprimer ce qui va ensuite puis imprimer le nouveau caractère de ligne, nje n'avais pas complètement saisi, mais ne fonctionne pas sans - à partir de documents:

fait en sorte que Perl assume la boucle suivante autour de votre programme, ce qui le fait parcourir les arguments de nom de fichier un peu comme sed -n ou awk: LINE: while (<>) {... # votre programme va ici}

Alexei Martianov
la source