Skip to content

Instantly share code, notes, and snippets.

@s1037989
Last active May 5, 2026 02:32
Show Gist options
  • Select an option

  • Save s1037989/9a250ff2a8e738edb235b6c6666c9bb9 to your computer and use it in GitHub Desktop.

Select an option

Save s1037989/9a250ff2a8e738edb235b6c6666c9bb9 to your computer and use it in GitHub Desktop.
A minimal Perl program that compares two binary files byte-for-byte, groups differing bytes into ranges, and prints each differing range in hexdump-like form for both files.
#!/usr/bin/env perl
# chmod +x bindiff.pl
# ./bindiff.pl old.bin new.bin
# Difference range: 0x00000010 - 0x0000001f
# old.bin:
# 00000010 41 42 43 00 ff 10 20 7e |ABC... ~|
# new.bin:
# 00000010 41 42 44 00 fe 10 21 7e |ABD...!~|
use v5.36;
use autodie;
my ($file_a, $file_b) = @ARGV;
die "Usage: $0 file-a file-b\n" unless $file_a && $file_b;
open my $A, '<:raw', $file_a;
open my $B, '<:raw', $file_b;
my $offset = 0;
my @segments;
my $current;
while (1) {
my $ra = read($A, my $ba, 1);
my $rb = read($B, my $bb, 1);
last if !$ra && !$rb;
my $different =
!$ra || !$rb ||
unpack('C', $ba) != unpack('C', $bb);
if ($different) {
$current //= {
start => $offset,
end => $offset,
a => '',
b => '',
};
$current->{end} = $offset;
$current->{a} .= $ra ? $ba : '';
$current->{b} .= $rb ? $bb : '';
}
elsif ($current) {
push @segments, $current;
undef $current;
}
$offset++;
}
push @segments, $current if $current;
for my $seg (@segments) {
printf "\nDifference range: 0x%08x - 0x%08x\n",
$seg->{start}, $seg->{end};
say "$file_a:";
dump_segment($seg->{start}, $seg->{a});
say "$file_b:";
dump_segment($seg->{start}, $seg->{b});
}
sub dump_segment ($base, $data) {
my $width = 16;
for (my $i = 0; $i < length($data); $i += $width) {
my $chunk = substr($data, $i, $width);
my @bytes = unpack('C*', $chunk);
my $hex = join ' ', map { sprintf '%02x', $_ } @bytes;
$hex .= ' ' x ($width - @bytes);
my $ascii = join '', map {
$_ >= 32 && $_ <= 126 ? chr($_) : '.'
} @bytes;
printf "%08x %-47s |%s|\n",
$base + $i,
$hex,
$ascii;
}
}
$ xxd -r -p > tiny <<'EOF'
7f454c46020101000000000000000000
02003e00010000007800400000000000
40000000000000000000000000000000
00000000400038000100000000000000
01000000050000000000000000000000
00004000000000000000400000000000
81000000000000008100000000000000
001000000000000031ffb83c0000000f05
EOF
cat tiny > tiny1 ; date +%Y%m%d%H%M%S >> tiny ; sleep 2 ; date +%Y%m%d%H%M%S >> tiny1
$ perl sha1sum.pl /tmp/tiny 129:14
4e532f04a7b565e75210e2fb6bcd2c8fc254a9c54ec365091a438c3311a79c9e
$ perl sha1sum.pl /tmp/tiny1 129:14
4e532f04a7b565e75210e2fb6bcd2c8fc254a9c54ec365091a438c3311a79c9e
$ CONTEXT=4 perl udiff.pl /tmp/tiny /tmp/tiny1
--- /tmp/tiny
+++ /tmp/tiny1
-000088 38 32 32 30 37 35 36 0a |8220756.|
+000088 38 32 32 30 38 32 39 0a |8220829.|
^ ^ ^ ^^^
$ printf "%s\n" 1:a 2:b 5:e 7:a 10:d | perl -Mojo -E 'print b(<STDIN>)->split("\n")->map("split",":")->reduce(sub{$a->[$b->[0]] = pack "H*", $b->[1]; $a}, c)->map(sub{$_||b("\x00")})->join("")' | xxd
00000000: 00a0 b000 00e0 00a0 0000 d0 ...........
#!/usr/bin/env perl
use v5.36;
use autodie;
my ($old, $new) = @ARGV;
die "usage: $0 OLD NEW\n" unless defined $old && defined $new;
open my $diff, '-|', 'bash', '-lc',
'diff -u <(xxd -p -c1 "$1") <(xxd -p -c1 "$2")',
'hexdiff', $old, $new;
my (@old_row, @new_row);
my ($old_pos, $new_pos);
my @pending;
while (<$diff>) {
next if /^--- / || /^\+\+\+ /;
if (/^\@\@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? \@\@/) {
flush_pending();
flush();
$old_pos = $1 - 1;
$new_pos = $2 - 1;
next;
}
next unless defined $old_pos && /^([-+ ])([0-9a-f]{2})$/i;
my ($tag, $hex) = ($1, lc $2);
if ($tag eq ' ') {
flush_pending();
push @old_row, [$old_pos++, $hex];
push @new_row, [$new_pos++, $hex];
}
else {
push @pending, [$tag, $hex];
}
flush() if @old_row >= 16 || @new_row >= 16;
}
flush_pending();
flush();
sub flush_pending {
return unless @pending;
my @del = grep { $_->[0] eq '-' } @pending;
my @add = grep { $_->[0] eq '+' } @pending;
if (@del == @add) {
for my $i (0 .. $#del) {
push @old_row, [$old_pos++, $del[$i][1]];
push @new_row, [$new_pos++, $add[$i][1]];
}
}
else {
for my $d (@del) {
push @old_row, [$old_pos++, $d->[1]];
push @new_row, [undef, undef];
}
for my $a (@add) {
push @old_row, [undef, undef];
push @new_row, [$new_pos++, $a->[1]];
}
}
@pending = ();
}
sub flush {
return unless @old_row || @new_row;
dump_row('-', \@old_row);
dump_row('+', \@new_row);
@old_row = ();
@new_row = ();
}
sub dump_row ($prefix, $row) {
return unless @$row;
my ($addr) = map { $_->[0] } grep { defined $_->[0] } @$row;
$addr //= 0;
printf "%s%08x ", $prefix, $addr;
for my $i (0 .. 15) {
my $hex = $row->[$i] ? $row->[$i][1] : undef;
printf "%-3s", defined $hex ? $hex : "";
print " " if $i == 7;
}
print " |";
for my $i (0 .. $#$row) {
my $hex = $row->[$i][1];
if (!defined $hex) {
print " ";
next;
}
my $byte = hex $hex;
print $byte >= 32 && $byte <= 126 ? chr($byte) : ".";
}
say "|";
}
#!/usr/bin/env perl
use strict;
use warnings;
use Digest::SHA qw(sha256_hex);
my ($file, $ranges_str) = @ARGV;
die "Usage: $0 <file> <offset:length,...>\n" unless $file;
# Parse ranges
my @ranges;
if ($ranges_str) {
@ranges = sort { $a->[0] <=> $b->[0] }
map { [ split /:/ ] }
split /,/, $ranges_str;
}
open my $fh, '<:raw', $file or die "open: $!";
my $sha = Digest::SHA->new(256);
my $pos = 0;
my $bufsize = 65536;
for my $r (@ranges) {
my ($skip_start, $skip_len) = @$r;
my $skip_end = $skip_start + $skip_len;
# Hash data before the skipped range
while ($pos < $skip_start) {
my $to_read = $skip_start - $pos;
$to_read = $bufsize if $to_read > $bufsize;
my $read = read($fh, my $buf, $to_read);
last unless $read;
$sha->add($buf);
$pos += $read;
}
# Skip the range
seek($fh, $skip_len, 1) or die "seek failed: $!";
$pos = $skip_end;
}
# Hash remaining data
while (read($fh, my $buf, $bufsize)) {
$sha->add($buf);
}
close $fh;
print $sha->hexdigest, "\n";
#!/usr/bin/env perl
use v5.36;
no feature 'bitwise';
use autodie;
my $CONTEXT = $ENV{CONTEXT} // 3;
my ($old_file, $new_file) = @ARGV;
die "usage: $0 OLD NEW\n" unless defined $old_file && defined $new_file;
my @old = read_hex_bytes($old_file);
my @new = read_hex_bytes($new_file);
my @ops = lcs_diff(\@old, \@new);
say "--- $old_file";
say "+++ $new_file";
print_hunks(\@ops, $CONTEXT);
sub read_hex_bytes ($file) {
open my $fh, '<:raw', $file;
local $/;
return map { sprintf "%02x", $_ } unpack "C*", <$fh>;
}
sub lcs_diff ($a, $b) {
my ($m, $n) = (scalar @$a, scalar @$b);
my @dp;
$dp[$_][$n] = 0 for 0 .. $m;
$dp[$m][$_] = 0 for 0 .. $n;
for (my $i = $m - 1; $i >= 0; $i--) {
for (my $j = $n - 1; $j >= 0; $j--) {
$dp[$i][$j] =
$a->[$i] eq $b->[$j]
? $dp[$i + 1][$j + 1] + 1
: max($dp[$i + 1][$j], $dp[$i][$j + 1]);
}
}
my @ops;
my ($i, $j) = (0, 0);
while ($i < $m || $j < $n) {
if ($i < $m && $j < $n && $a->[$i] eq $b->[$j]) {
push @ops, [' ', $i, $j, $a->[$i], $b->[$j]];
$i++;
$j++;
}
elsif ($i < $m && ($j == $n || $dp[$i + 1][$j] >= $dp[$i][$j + 1])) {
push @ops, ['-', $i, undef, $a->[$i], undef];
$i++;
}
else {
push @ops, ['+', undef, $j, undef, $b->[$j]];
$j++;
}
}
return @ops;
}
sub print_hunks ($ops, $context) {
my @changed = grep { $ops->[$_][0] ne ' ' } 0 .. $#$ops;
return unless @changed;
my @hunks;
my ($s, $e);
for my $i (@changed) {
my $hs = max(0, $i - $context);
my $he = min($#$ops, $i + $context);
if (!defined $s || $hs > $e + 1) {
push @hunks, [$s, $e] if defined $s;
($s, $e) = ($hs, $he);
}
else {
$e = max($e, $he);
}
}
push @hunks, [$s, $e];
for my $h (@hunks) {
print_hunk($ops, @$h);
}
}
sub print_hunk ($ops, $s, $e) {
my @cols;
my $i = $s;
while ($i <= $e) {
my $tag = $ops->[$i][0];
if ($tag eq ' ') {
push @cols, [$ops->[$i][1], $ops->[$i][3], $ops->[$i][4]];
$i++;
next;
}
my (@del, @add);
while ($i <= $e && $ops->[$i][0] ne ' ') {
push @del, $ops->[$i] if $ops->[$i][0] eq '-';
push @add, $ops->[$i] if $ops->[$i][0] eq '+';
$i++;
}
if (@del == @add) {
for my $n (0 .. $#del) {
push @cols, [$del[$n][1], $del[$n][3], $add[$n][4]];
}
}
else {
push @cols, [$_->[1], $_->[3], undef] for @del;
push @cols, [$_->[2], undef, $_->[4]] for @add;
}
}
for (my $i = 0; $i < @cols; $i += 64) {
# my @row = @cols[$i .. min($i + 63, $#cols)];
my $left = max(0, $i - $CONTEXT);
my $right = min($#cols, $i + 63 + $CONTEXT);
my @row = @cols[$left .. $right];
my $a = dump_row('-', \@row, 1);
my $b = dump_row('+', \@row, 2);
next if substr($a,1) eq substr($b,1);
say sprintf "-%s", substr($a,1);
say sprintf "+%s", substr($b,1);
say sprintf " %s", ((substr($a,1)^substr($b,1))=~s/./ord $& ? "^" : " "/ger);
}
}
sub dump_row ($prefix, $row, $side) {
my $addr = $row->[0][0];# & ~0xf;
my $diff = sprintf "%s%06x ", $prefix, $addr;
# for my $col (@$row) {
# printf "%-3s", defined $col->[$side] ? $col->[$side] : "";
# }
$diff .= sprintf "%-47s", join ' ', map { defined $_->[$side] ? $_->[$side] : " " } @$row;
$diff .= sprintf " |";
for my $col (@$row) {
my $hex = $col->[$side];
if (!defined $hex) {
$diff .= sprintf " ";
}
else {
my $byte = hex($hex);
$diff .= sprintf $byte >= 32 && $byte <= 126 ? chr($byte) : ".";
}
}
$diff .= "|";
}
sub max ($a, $b) { $a > $b ? $a : $b }
sub min ($a, $b) { $a < $b ? $a : $b }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment