#!/usr/pkg/bin/perl
use warnings;
use strict;

# Use with Intel Hex files for Texas Instruments MSP430 microcontrollers.
# Replace ALL interrupt vectors with addresses generated randomly.
# Store jump instructions to the original interrupt handlers there.

# Written by Alexander Becher <alex@cyathus.de>, 2005.
# Use freely.

# $start and $end define the address range into which new vectors will point.

# The start address will be updated by the Ihex reading code below.
# It must be correctly aligned.
my $start	= 0x4000;

# This is where the interrupt table starts. Addresses >= $end will not be used!
my $end         = 0xffe0;

my $align       = 2;            # code addresses on MSP430 must be even

my %used;			# will contain used memory regions

my @interrupts;			# stores the original interrupt vector table

# Read an Intel hex file. With much help from BFD.
while (<>) {
    my ($len, $addr, $data, $checksum)
      = (/^:([[:xdigit:]]{2})([[:xdigit:]]{4})00([[:xdigit:]]*)([[:xdigit:]]{2})\cM?$/);

## Does not work? What did I do wrong??
#     if (m/^:			# colon
#           ([[:xdigit:]]{2})	# data length
#           ([[:xdigit:]]{4})	# address
#           00			# record type
#           ([[:xdigit:]]*)	# data
#           ([[:xdigit:]]{2})	# checksum
#           \cM?$/x		# CRLF
#        ) {

    if (!defined $addr || ($addr ne "FFE0" && $addr ne "FFF0")) {
	# update start of unused address region
	# assume ascending order of addresses
	# (i.e. it is not the case that there is a line in the hex file
	#  which contains code for address 0x4242 which is followed by a
	#  line which contains code for address 0x2323)

	# assume all data first, then interrupts, then trailing stuff
	if (defined $addr && defined $len) {
	    $start = align(hex($addr) + $len);
	}

	print;
	next;
    }

    # If we reach this point, we are reading the first or the second
    # line of the interrupt vector table.

    # Attention: MSP430 is little endian!
    while ($data =~ /([[:xdigit:]]{2})([[:xdigit:]]{2})/g) {
    	push @interrupts, hex "$2$1";
    }

    # If this is the second line, do The Real Work[tm].
    if (@interrupts && $addr eq "FFF0") {
	replace_interrupts(@interrupts);
    }
}

#
# Why this program exists ;->
#
sub replace_interrupts {
    my (@interrupts) = @_;

    foreach my $addr (@interrupts) {
	# Create a branch instruction to the original address of the handler
	my $instr = br($addr);

	# Get a new, unused address, using key material
	my $new_addr = new_addr(length $instr);

	# write a branch instruction there, mark appropriate addresses as used.
	code($new_addr, $instr);

	# point interrupt vector to new address (use Perl's loop aliasing)
	$addr = $new_addr;
    }

    print ihex(0xFFE0, map { rep_16bit_le($_) } @interrupts);
}

#
# Returns a new random unused address suitable for putting code of
# $length bytes there. Return value will be aligned.
#
sub new_addr {
    my ($length) = @_;

    my ($addr, $diff);

    $diff = ($end - $start) / $align;

    # create a new random address until an unused region of the desired
    # length is found
    do {
        my $n = int rand $diff;
	$addr = $start + $n * $align;
    } while (grep { $_ } map { isused($addr+$_) } (0..$length-1));
    # Woohoo, Perl's list processing rocks. ;-> The above was a crude
    # form of "Is any address in the region [$addr, $addr+$length) used?".

    return $addr;
}

sub isused { $used{shift()} }

sub mark_used {
    my ($addr, $len) = @_;

    foreach ($addr .. $addr + $len) {
	$used{$_} = 1;
    }
}

#
# Place $code at $addr (which must be aligned). Outputs an ihex line
# which says just that, and marks the memory area as used.
#
sub code {
    my ($addr, $code) = @_;

    my $l = length $code;
    mark_used($addr, $l);

    print ihex($addr, $code);
}

# MSP 430 branch instruction
sub br {  "\x30\x40" . rep_16bit_le(@_) }

# 16 bit little endian representation
sub rep_16bit_le { pack("v", @_) }

# ihex($addr, @data)
# write an ihex line, saying that $addr contains bytes @data
# @data should contain "\xXX" binary data
sub ihex {
    my ($addr, @data) = @_;

    @data = split //, join("", @data);

    my $type	= 0;		# data

    my $ret = "";

    while (@data) {
	my @bytes = @data >= 16 ? (splice @data, 0, 16) : (splice @data);

	my $c = @bytes;

	# XXX pack?
	my $data = join("", map { sprintf "%02X", ord $_ } @bytes);

	# checksum computation from GNU binutils bfd/ihex.c
	my $checksum = $c + $addr + ($addr >> 8) + $type;
	foreach (@bytes) {
	    $checksum += ord;
	}
	$checksum = (- $checksum) & 0xff;

	$ret .= sprintf(":%02X%04X%02X%s%02X\cM\cJ",
			$c, $addr, $type, $data, $checksum);

	$addr += 16;		# wrong for last iteration, but irrelevant then
    }

    return $ret;
}

# round up to the nearest multiple of $align
sub align {
    my ($n) = @_;
    if ($n % $align != 0) {
	$n += $align - ($n % $align);
    }
    return $n;
}
