ZetCode

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.

tohex.pl
#!/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.

fromhex.pl
#!/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.

line.pl
#!/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.

hexdump.pl
#!/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.

undump.pl
#!/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.

diff.pl
#!/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

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Perl tutorials.