Last active
May 5, 2026 02:32
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $ 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.| | |
| ^ ^ ^ ^^^ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $ 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 ........... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 "|"; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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