Wie kann ich zählen, wie oft eine Bytesequenz in einer Datei vorkommt?

16

Ich möchte zählen, wie oft eine bestimmte Folge von Bytes in einer Datei vorkommt, die ich habe. Zum Beispiel möchte ich herausfinden, wie oft die Nummer \0xdeadbeefin einer ausführbaren Datei vorkommt. Im Moment mache ich das mit grep:

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

(Die Bytes werden in umgekehrter Reihenfolge geschrieben, da meine CPU Little-Endian ist.)

Ich habe jedoch zwei Probleme mit meinem Ansatz:

  • Diese \XnnFluchtsequenzen funktionieren nur in der Fischhülle.
  • grep zählt tatsächlich die Anzahl der Zeilen, die meine magische Zahl enthalten. Wenn das Muster zweimal in derselben Zeile vorkommt, zählt es nur einmal.

Gibt es eine Möglichkeit, diese Probleme zu beheben? Wie kann ich diesen einen Liner in der Bash-Shell laufen lassen und genau zählen, wie oft das Muster in der Datei vorkommt?

hugomg
quelle
Hilfe: unix.stackexchange.com/q/231213/117549 - speziellgrep -o
Jeff Schaller
1
grep ist das falsche Werkzeug. Betrachten Sie bgrep oder bgrep2.
Fpmurphy
3
Wenn die zu suchende Sequenz lautet 11221122, wie sollte sie bei einer Eingabe zurückgegeben werden 112211221122? 1 oder 2?
Stéphane Chazelas
In diesem Fall würde ich 2 oder 3 Treffer melden können. Was auch immer einfacher zu implementieren wäre.
Hugomg

Antworten:

15

Dies ist die angeforderte Ein-Liner-Lösung (für aktuelle Shells mit "Prozesssubstitution"):

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

Wenn keine "Prozessersetzung" <(…)verfügbar ist, verwenden Sie einfach grep als Filter:

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

Nachstehend finden Sie eine detaillierte Beschreibung der einzelnen Teile der Lösung.

Byte-Werte aus Hex-Zahlen:

Ihr erstes Problem ist leicht zu lösen:

Diese \ Xnn-Escape-Sequenzen funktionieren nur in der Fischhülle.

Ändern Sie die obere Xin eine untere xund verwenden Sie printf (für die meisten Shells):

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

Oder benutze:

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

Für diejenigen Shells, die die Darstellung '\ x' nicht implementieren möchten.

Natürlich funktioniert die Übersetzung von Hex nach Oktal auf (fast) jeder Shell:

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

Wobei "$ sh" eine beliebige (vernünftige) Shell ist. Aber es ist ziemlich schwierig, es richtig zu zitieren.

Binärdateien.

Die robusteste Lösung besteht darin, die Datei und die Bytefolge (beide) in eine Codierung umzuwandeln, die keine Probleme mit ungeraden Zeichenwerten wie (neue Zeile) 0x0Aoder (Null-Byte) aufweist 0x00. Beide sind mit Tools, die für die Verarbeitung von "Textdateien" entwickelt und angepasst wurden, nur schwer korrekt zu verwalten.

Eine Transformation wie base64 mag als gültig erscheinen, birgt jedoch das Problem, dass jedes Eingabebyte bis zu drei Ausgabedarstellungen haben kann, je nachdem, ob es sich um das erste, zweite oder dritte Byte der Mod-24-Position (Bits) handelt.

$ echo "abc" | base64
YWJjCg==

$ echo "-abc" | base64
LWFiYwo=

$ echo "--abc" | base64
LS1hYmMK

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

Verhexung transformieren.

Deshalb sollte die robusteste Transformation eine sein, die an jeder Byte-Grenze beginnt, wie die einfache HEX-Darstellung.
Wir können eine Datei mit der hexadezimalen Darstellung der Datei mit jedem dieser Tools erhalten:

$ 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

Die zu durchsuchende Bytefolge ist in diesem Fall bereits hexadezimal.
:

$ var="ef be ad de"

Es könnte aber auch transformiert werden. Ein Beispiel für eine Rundreise hex-bin-hex folgt:

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

Der Suchstring kann aus der Binärdarstellung gesetzt werden. Alle drei oben aufgeführten Optionen od, hexdump oder xxd sind gleichwertig. Stellen Sie einfach sicher, dass die Leerzeichen enthalten sind, um sicherzustellen, dass die Übereinstimmung an den Byte-Grenzen liegt (keine Nibble-Verschiebung zulässig):

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

Wenn die Binärdatei so aussieht:

$ 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

Dann gibt eine einfache Suche nach grep die Liste der übereinstimmenden Sequenzen aus:

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

Eine Linie?

Es kann alles in einer Zeile ausgeführt werden:

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

Für die Suche 11221122in derselben Datei sind beispielsweise die folgenden zwei Schritte erforderlich:

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

So "sehen" Sie die Übereinstimmungen:

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

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

… 0a 31313232313132323131323231313232313132323131323131323231313232 313132320a


Pufferung

Es besteht die Sorge, dass grep die gesamte Datei puffert und, wenn die Datei groß ist, eine hohe Last für den Computer erstellt. Dafür können wir eine ungepufferte sed-Lösung verwenden:

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

Die erste sed ist ungepuffert ( -u) und wird nur zum Einfügen von zwei Zeilenumbrüchen in den Stream pro übereinstimmender Zeichenfolge verwendet. Die Sekunde sedwird nur die (kurzen) übereinstimmenden Zeilen drucken. Das wc -l zählt die übereinstimmenden Zeilen.

Dies puffert nur einige kurze Zeilen. Die passende (n) Saite (n) im zweiten Satz. Dies sollte relativ ressourcenschonend sein.

Oder etwas komplexer zu verstehen, aber die gleiche Idee in einem Satz:

a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin  |
    sed -u '/\n/P;//!s/'"$a"'/\n&\n/;D' |
        wc -l
Sorontar
quelle
2
Beachten Sie, dass, wenn Sie den gesamten Text in eine Zeile schreiben, dies bedeutet, dass der gesamte Text grepin den Speicher geladen wird (hier doppelt so groß wie die ursprüngliche Datei + 1 aufgrund der Hexadezimalcodierung) Overhead als der pythonAnsatz oder der perlmit -0777. Sie benötigen auch eine grepImplementierung, die Zeilen beliebiger Länge unterstützt (die, die im -oAllgemeinen unterstützen). Gute Antwort ansonsten.
Stéphane Chazelas
1
Ihre Hex-Versionen stimmen mit nibble-shifted Werten überein? E fb ea dd e? zusätzlich zu den gewünschten Bytes. od -An -tx1 | tr -d '\n'oder hexdump -v -e '/1 " %02x"'mit einer Suchzeichenfolge, die auch Leerzeichen enthält, vermeiden Sie dies, aber ich sehe keine solche Lösung für xxd.
Dave_thompson_085
@ dave_thompson_085 Antwort bearbeitet. Ich glaube, dass die Antwort jetzt nur noch Bytegrenzen enthält. Nochmals vielen Dank.
Sorontar
@ StéphaneChazelas Könnten Sie die vorgeschlagene Option zur Verwendung eines ungepufferten Sed prüfen? Vielen Dank.
Sorontar
sed -u(wo verfügbar) dient zum Entpuffern. Das bedeutet, dass es bei der Eingabe jeweils ein Byte liest und seine Ausgabe sofort ohne Pufferung ausgibt. In jedem Fall muss immer noch die gesamte Zeile in den Musterbereich geladen werden, was hier nicht weiterhilft.
Stéphane Chazelas
7

Mit GNU grep‚s -P(perl-regexp) flag

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

LC_ALL=CDies dient zur Vermeidung von Problemen in Mehrbyte-Sprachumgebungen, in denen grepsonst versucht wird, Bytefolgen als Zeichen zu interpretieren.

-aBehandelt Binärdateien, die Textdateien entsprechen (anstelle des normalen Verhaltens, bei dem grepnur gedruckt wird, ob mindestens eine Übereinstimmung vorliegt oder nicht).

iruvar
quelle
Diese Lösung gibt mir immer 0 Übereinstimmungen anstelle der richtigen Nummer.
Hugomg
@hugomg, könnte es sein, dass Sie die übergebenen Bytes umkehren müssen grep , damit sie übereinstimmen?
Iruvar
Ich glaube nicht, dass es die Reihenfolge ist. Die beiden anderen Antworten auf diese Frage funktionieren korrekt.
Hugomg
2
@hugomg, es ist das Gebietsschema. Siehe Bearbeiten.
Stéphane Chazelas
2
Ich schlage vor, die -aOption einzuschließen, andernfalls antwortet grep mit Binary file file.bin matchesfür jede Datei, die grep als binär erkennt.
Sorontar
6
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file

Was die Eingabedatei (en) als binär behandelt (keine Übersetzung für Zeilenvorschübe oder Codierungen, siehe Perlrun ) , durchläuft dann die Eingabedatei (en) und druckt keinen Zähler für alle Übereinstimmungen des angegebenen Hex (oder in welcher Form auch immer, siehe Perlre ). .

Thrig
quelle
2
Beachten Sie, dass Sie dies nicht verwenden können, wenn die zu suchende Sequenz das Byte 0xa enthält. In diesem Fall können Sie ein anderes Datensatztrennzeichen (mit -0ooo) verwenden.
Stéphane Chazelas
1
@ StéphaneChazelas Sie können die interessierende Sequenz selbst wie $/perl -nE 'BEGIN { $/ = "\xef\xbe\xad\xde" } chomp; $c++ unless eof && length; END { say $c }'
folgt verwenden
@ StéphaneChazelas Bitte lies meine Antwort für eine Lösung für beliebige Bytewerte.
Sorontar
1
@hobbs, in jedem Fall ist die Speichernutzung auch hier proportional zum maximalen Abstand zwischen zwei 0xa-Bytes, die für Nicht-Textdateien beliebig groß sein können.
Stéphane Chazelas
5

Mit GNU awkkönnen Sie:

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

Wenn es sich bei einem der Bytes um ERE-Operatoren handelt, müssen sie jedoch (mit \\) maskiert werden . Wie 0x2edie .wäre als eingegeben werden \\.oder \\\x2e. Ansonsten sollte es mit beliebigen Bytewerten einschließlich 0 und 0xa funktionieren.

Beachten Sie, dass dies nicht so einfach ist, NR-1da es einige Sonderfälle gibt:

  • Wenn die Eingabe leer ist, ist NR 0, NR-1 würde -1 ergeben.
  • Wenn die Eingabe im Datensatztrennzeichen endet, wird danach kein leerer Datensatz erstellt. Wir testen das mit RT=="".

Beachten Sie auch, dass im schlimmsten Fall (wenn die Datei keinen Suchbegriff enthält) die Datei vollständig in den Speicher geladen wird.

Stéphane Chazelas
quelle
5

Die einfachste Übersetzung, die ich sehe, ist:

$ 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

Wo ich verwendet habe , $'\xef'als die bash-ANSI zitiert (ursprünglich eine ksh93Funktion, unterstützt jetzt durch zsh, bash, mksh, FreeBSD sh) Version von Fisch \Xefund verwendet grep -o ... | wc -ldie Instanzen zu zählen. grep -ogibt jede Übereinstimmung in einer separaten Zeile aus. Das -aFlag bewirkt, dass sich grep bei Binärdateien genauso verhält wie bei Textdateien. -Fist für feste Zeichenfolgen vorgesehen, damit Sie Regex-Operatoren nicht entziehen müssen.

Wie in Ihrem fishFall können Sie diesen Ansatz jedoch nicht verwenden, wenn die zu suchende Sequenz die Bytes 0 oder 0xa (Newline in ASCII) enthält.

Jeff Schaller
quelle
Die Verwendung printf '%b' $(printf '\\%o ' $((0xef)) $((0xbe)) $((0xad)) $((0xde))) > hugohex'wäre die portabelste "reine Shell" -Methode. Natürlich: printf "efbeadde" | xxd -p -r > hugohexscheint die praktischste Methode zu sein.
Sorontar
4

Sie können die bytes.countMethode von Python verwenden, um die Gesamtzahl der nicht überlappenden Teilzeichenfolgen in einem Bytestring abzurufen.

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

Dieser Einzeiler lädt die gesamte Datei in den Speicher, ist also nicht der effizienteste, funktioniert aber und ist besser lesbar als Perl; D

Nick T
quelle
'lesbarer als Perl' ist nur einen Schritt von TECO entfernt - das ist IINM: 239I$ 190I$ 173I$ 222I$ HXA ERfile$Y 0UC <:S^EQA$; %C$> QC=(gd & r)
dave_thompson_085
Sie können mmap()eine Datei in Python ; das würde das Memory Commit reduzieren.
Toby Speight
1
tr "$(printf \\0xef)\n" \\n\\0 < infile |
grep -c "^$(printf "\0xbe\0xad\0xde")"
mikeserv
quelle
1

Ich denke, Sie können Perl verwenden, probieren Sie es aus:

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

Ersetzen Befehl sgibt Anzahl der Ersetzungen vorgenommen, -0777 Mittel nicht behandeln neue Linie als Sonderzeichen, e- Befehl ausführen, sayzu drucken , was Zeilenendmarke drucken nächste dann geht, nhatte ich nicht ganz begriffen, aber nicht funktioniert w / out - von docs:

veranlasst Perl, die folgende Schleife um Ihr Programm anzunehmen, wodurch es über Dateinamenargumente wie sed -n oder awk iteriert: LINE: while (<>) {... # Ihr Programm geht hierher}

Alexei Martianov
quelle