Perl hex dump
last modified November 7, 2026
A hex dump shows binary data as hexadecimal numbers, usually 16 bytes per line,
with the offset of each line on the left and the printable characters on the
right. It is the standard way to inspect file formats, network packets and
encoding problems. Unix systems have tools such as xxd,
hexdump and od; in this article we build one in Perl,
step by step.
The core of every hex dump is the conversion of bytes to hexadecimal digits,
which Perl does with unpack. The reverse conversion uses
pack. The hex function, in contrast,
converts a hexadecimal string to a single number. Reading binary files is
covered in the Perl binary files article. The
examples were run with Perl 5.44 on Linux.
Bytes to hexadecimal
The H template of unpack produces two hexadecimal
digits per byte, high nybble first. A repeat count or a group lets us choose how
the digits are grouped.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $data = "Perl\x00\x7f\xff\n";
say unpack 'H*', $data;
say uc unpack 'H*', $data;
say join ' ', unpack '(H2)*', $data;
say join ':', unpack '(H4)*', $data;
say join ' ', map { sprintf '%02x', ord } split //, $data;
say unpack 'h*', "\x12\xab";
H* converts the whole string to one run of digits; wrapping it in
uc gives capital letters. The group (H2)* returns a
list with one two-digit item per byte, which join separates with
spaces; (H4)* groups two bytes. The sprintf version
gives the same result, but processes the string character by character and is
much slower on large data. The h template puts the low nybble
first, so byte 0x12 becomes 21.
$ perl tohex.pl 5065726c007fff0a 5065726C007FFF0A 50 65 72 6c 00 7f ff 0a 5065:726c:007f:ff0a 50 65 72 6c 00 7f ff 0a 21ba
Hexadecimal to bytes
pack 'H*' does the opposite: it turns a string of hexadecimal
digits into bytes.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $bytes = pack 'H*', '5065726c0a';
print $bytes;
say length $bytes, " bytes";
my $mac = 'DE:AD:BE:EF:00:2A';
(my $digits = $mac) =~ tr/://d;
my $raw = pack 'H*', $digits;
say join ',', unpack 'C*', $raw;
say unpack 'H*', pack 'H*', 'abc';
my @nums = map { hex } split ' ', 'ff 10 7f';
say "@nums";
The digits 5065726c0a become the five bytes of Perl
and a newline. Separators such as the colons in a MAC address must be removed
first; tr/://d deletes them. An odd number of digits is padded with
a zero nybble, so abc becomes the two bytes ab c0.
When we need numbers rather than bytes, the hex function converts
each item.
$ perl fromhex.pl Perl 5 bytes 222,173,190,239,0,42 abc0 255 16 127
Formatting a dump line
A classic dump line, as printed by hexdump -C, has an eight-digit
offset, sixteen bytes in two groups of eight, and the printable characters
between bars.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
sub dump_line {
my ($offset, $bytes) = @_;
my @hex = unpack '(H2)*', $bytes;
my $left = join ' ', @hex[0 .. ($#hex < 7 ? $#hex : 7)];
my $right = join ' ', @hex[8 .. $#hex];
(my $ascii = $bytes) =~ tr/\x20-\x7e/./c;
return sprintf "%08x %-23s %-23s |%s|", $offset, $left, $right, $ascii;
}
my $data = "Hello, binary world!\n\tTab\x00\x01\x02\xfe\xff";
for (my $off = 0; $off < length $data; $off += 16) {
say dump_line($off, substr $data, $off, 16);
}
printf "%08x\n", length $data;
The dump_line function splits the hex digits into the left and
right group. The %-23s format pads each group to its full width
(eight two-digit numbers and seven spaces), so the ASCII column stays aligned
on the last, shorter line. The ASCII column is built with the transliteration
tr/\x20-\x7e/./c: the c flag complements the range,
so every byte that is not a printable ASCII character becomes a dot. The final
line prints the total length, as hexdump does.
$ perl line.pl 00000000 48 65 6c 6c 6f 2c 20 62 69 6e 61 72 79 20 77 6f |Hello, binary wo| 00000010 72 6c 64 21 0a 09 54 61 62 00 01 02 fe ff |rld!..Tab.....| 0000001e
A hex dump tool
The complete tool reads a file or standard input in blocks and supports three
options: -s to skip bytes, -n to limit the number of
bytes, and -w to set the line width.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Fcntl qw(:seek);
use Getopt::Long;
my ($skip, $length, $width) = (0, undef, 16);
GetOptions(
's|skip=i' => \$skip,
'n|length=i' => \$length,
'w|width=i' => \$width,
) or die "Usage: $0 [-s skip] [-n length] [-w width] [file]\n";
my $fh;
if (@ARGV) {
open $fh, '<:raw', $ARGV[0] or die "Cannot open $ARGV[0]: $!\n";
} else {
$fh = \*STDIN;
binmode $fh;
}
if ($skip) {
seek $fh, $skip, SEEK_SET or die "Cannot seek: $!\n";
}
my $offset = $skip;
my $half = int($width / 2);
my $hexw = $width * 3 + 1;
while (1) {
my $want = $width;
if (defined $length) {
my $left = $skip + $length - $offset;
last if $left <= 0;
$want = $left if $left < $want;
}
my $buf;
my $n = read $fh, $buf, $want;
die "Read error: $!\n" unless defined $n;
last if $n == 0;
my @hex = unpack '(H2)*', $buf;
my $hex = join ' ', @hex;
$hex =~ s/^((?:\S\S ){$half})/$1 /;
(my $ascii = $buf) =~ tr/\x20-\x7e/./c;
printf "%08x %-*s |%s|\n", $offset, $hexw, $hex, $ascii;
$offset += $n;
}
printf "%08x\n", $offset;
Getopt::Long parses the options. Without a file argument the
program reads STDIN, which must be switched to binary mode with
binmode. Skipping is done with seek, so the program
does not have to read the skipped bytes. Each iteration reads at most one line
of bytes; with -n it reads only what is left of the limit. The
hex digits are joined with spaces and a substitution inserts an extra space
after the first half of the line. The %-*s format takes the
column width from the argument list, so the ASCII column is aligned for any
line width. We dump the 60-byte record file from the binary files article.
$ perl hexdump.pl users.dat 00000000 41 6c 69 63 65 20 20 20 20 20 20 20 22 04 e2 42 |Alice "..B| 00000010 6f 62 20 20 20 20 20 20 20 20 20 1b 05 c8 43 61 |ob ...Ca| 00000020 72 6f 6c 20 20 20 20 20 20 20 2d 0c 30 44 61 76 |rol -.0Dav| 00000030 65 20 20 20 20 20 20 20 20 34 01 9a |e 4..| 0000003c
Comparing with hexdump
The hexdump -C command produces exactly the same output. In the
dump we can see the space-padded names, the age byte (0x22 is 34)
and the 16-bit big-endian points (04 e2 is 1250).
$ hexdump -C users.dat 00000000 41 6c 69 63 65 20 20 20 20 20 20 20 22 04 e2 42 |Alice "..B| 00000010 6f 62 20 20 20 20 20 20 20 20 20 1b 05 c8 43 61 |ob ...Ca| 00000020 72 6f 6c 20 20 20 20 20 20 20 2d 0c 30 44 61 76 |rol -.0Dav| 00000030 65 20 20 20 20 20 20 20 20 34 01 9a |e 4..| 0000003c
Skipping and limiting
The -s and -n options select a part of the file. Here
we skip the first record, which is 15 bytes long, and show 20 bytes. The offsets
in the output are the real file offsets.
$ perl hexdump.pl -s 15 -n 20 users.dat 0000000f 42 6f 62 20 20 20 20 20 20 20 20 20 1b 05 c8 43 |Bob ...C| 0000001f 61 72 6f 6c |arol| 00000023
Reading standard input
When the input comes from a pipe, the tool reads standard input. The
-w option sets eight bytes per line, split into two groups of
four.
$ printf 'Perl\tbinary\n' | perl hexdump.pl -w 8 00000000 50 65 72 6c 09 62 69 6e |Perl.bin| 00000008 61 72 79 0a |ary.| 0000000c
Reversing a dump
A hex dump can be turned back into binary data. This is useful for patching a
file in a text editor: dump it, edit the hex digits, and convert the dump back,
which is what xxd -r does.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
binmode STDOUT;
my $expect = 0;
while (my $line = <>) {
next unless $line =~ /^([0-9a-f]{8}) (.*?)\s*\|/i;
my ($offset, $hex) = (hex $1, $2);
die "Gap at offset $offset, expected $expect\n" if $offset != $expect;
$hex =~ tr/ //d;
die "Bad hex digits on line $.\n" if $hex =~ /[^0-9a-f]/i;
my $bytes = pack 'H*', $hex;
print $bytes;
$expect += length $bytes;
}
The regular expression takes the offset and everything up to the bar that
starts the ASCII column; lines without that structure, such as the final total,
are skipped. The program checks that the offsets are continuous, removes the
spaces with tr/ //d, rejects anything that is not a hexadecimal
digit, and writes the bytes produced by pack 'H*'. We dump a
1,000-byte file of random data, convert the dump back, and compare the result
with the original.
$ perl hexdump.pl data.bin | perl undump.pl > restored.bin && cmp data.bin restored.bin && echo round trip ok round trip ok
Comparing binary files
A hex dump is also the basis of a binary diff, which lists the offsets where two files differ together with both byte values. The string XOR operator finds them quickly.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my ($f1, $f2) = @ARGV;
die "Usage: $0 file1 file2\n" unless defined $f2;
sub slurp {
my $file = shift;
open my $fh, '<:raw', $file or die "Cannot open $file: $!\n";
local $/;
return scalar <$fh>;
}
my ($d1, $d2) = (slurp($f1), slurp($f2));
my $len = length $d1 < length $d2 ? length $d1 : length $d2;
my $xor = substr($d1, 0, $len) ^. substr($d2, 0, $len);
my $count = 0;
while ($xor =~ /[^\x00]/g) {
my $off = $-[0];
printf "%08x %02x %02x\n", $off,
ord substr($d1, $off, 1), ord substr($d2, $off, 1);
$count++;
}
say "$count differing bytes";
say "sizes differ: ", length $d1, " vs ", length $d2 if length $d1 != length $d2;
The ^. operator XORs two strings byte by byte; it is enabled by
the bitwise feature, which use v5.34.0 turns on.
Equal bytes produce a zero byte, so every non-zero byte in the result marks a
difference. The regex finds them with /g, and $-[0]
holds the offset of each match. The second file is a copy of the random data
with three bytes changed.
$ perl diff.pl data.bin data2.bin 00000064 09 41 00000309 1d 42 0000030a cb 43 3 differing bytes
Best practices
- Use unpack for conversion:
unpack '(H2)*'is faster and simpler thansprintfon every character. - Read in binary mode: open files with
:rawand callbinmode STDINwhen reading a pipe. - Read in blocks: a dump tool should work on files of any size; never slurp the whole file.
- Show real offsets: when skipping data, start the offset column at the skip value, so it matches the file.
- Validate input when reversing: check offsets and hex
digits before writing bytes with
pack 'H*'. - Remember the nybble order:
His high nybble first, as everyone expects;his rarely what you want.
In this article we have converted bytes to hexadecimal and back with
unpack and pack, formatted dump lines with an ASCII
column, written a hex dump tool with skip, length and width options, converted a
dump back to binary data, and compared two binary files.
Source
perlfunc: unpack · perlfunc: pack · perlpacktut · perlop: Bitwise string operators
Author
List all Perl tutorials.