1
0
mirror of https://github.com/DoctorWkt/pdp7-unix.git synced 2026-01-13 07:20:05 +00:00

1582 lines
45 KiB
Perl
Executable File

#!/usr/bin/env perl
#
# a7out: user-mode simulator for PDP-7 Unix applications
#
# (c) 2016 Warren Toomey, GPL3
#
use strict;
use warnings;
use Fcntl qw(:seek);
use DateTime;
use Data::Dumper;
### Global variables ###
my $debug = 0; # Debug flag
my $singlestep = 0; # Are we running in single-step mode?
my $coverage = 0; # Print out code coverage
my %Breakpoint; # Hash of defined breakpoints
my @Mem; # 8K 18-bit words of main memory
my %Addr2Name; # Map addresses to names
my %Name2Addr; # Map names to addresses
my @CC; # Code coverage: what addrs executed instructions
my @FD; # Array of open filehandles
my @ISBINARY; # Array of filehandle flags: ASCII or binary files?
# Registers
my $PC = 010000; # Program counter
my $AC = 0; # Accumulator
my $LINK = 0; # Link register, either 0 or LINKMASK
my $MQ = 0; # MQ register
# Constants
use constant MAXINT => 0777777; # Biggest unsigned integer
use constant MAXPOSINT => 0377777; # Biggest signed integer
use constant MAXADDR => 017777; # Largest memory address
use constant LINKMASK => 01000000; # Mask for LINK register
use constant EAESTEP => 077; # EAE step count mask
use constant EAEIMASK => 0777700; # EAE instruction mask
use constant SIGN => 0400000; # Sign bit
use constant EPSILON => 99; # max delta for symbol display
### Main program ###
# Get any optional arguments
while ( defined( $ARGV[0] ) && ( $ARGV[0] =~ m{^-} ) ) {
# -d: debug mode
if ( $ARGV[0] eq "-d" ) {
$debug = 1;
shift(@ARGV);
}
# -b: set a breakpoint
if ( $ARGV[0] eq "-b" ) {
shift(@ARGV);
$Breakpoint{ lookup( shift(@ARGV) ) } = 1;
}
# -c: print out code coverage
if ( $ARGV[0] eq "-c" ) {
shift(@ARGV);
$coverage= 1;
}
# -n: read a namelist file
if ( $ARGV[0] eq "-n" ) {
shift(@ARGV);
load_names( shift(@ARGV) );
}
}
# Check the arguments
die("Usage: $0 [-c] [-d] [-b breakpoint] [-n namelist] a.outfile [arg1 arg2 ...]\n")
if ( @ARGV < 1 );
# Load the a.out file into memory
# and simulate it
load_code( $ARGV[0] );
set_arguments();
#dump_memory(0, MAXADDR, 0);
#exit(0);
simulate();
print_coverage() if ($coverage);
exit(0);
# Print out the code coverage
sub print_coverage {
foreach my $addr (0 .. MAXADDR ) {
printf("%06o: %d\n", $addr, $CC[$addr]) if ($CC[$addr]);
}
}
### Load the a.out file into memory
sub load_code {
my $filename = shift;
# Fill all the 8K words in memory with zeroes
foreach my $i ( 0 .. MAXADDR ) {
$Mem[$i] = 0;
}
# Set up some open filehandles
$FD[0] = \*STDIN;
$FD[1] = \*STDOUT;
$FD[8] = \*STDERR; # According to cat.s (uses d8 == 8)
# Open up the PDP-7 executable file
open( my $IN, "<", $filename ) || die("Unable to open $filename: $!\n");
my $c = getc($IN);
seek $IN, 0, 0;
if ((ord($c) & 0300) == 0200) { # handle "binary paper tape" format
my $addr = 010000; # user programs loaded at 4K mark
while ($addr <= 017777) {
my $result = read_word($IN);
last if ($result == -1);
$Mem[$addr++] = $result;
}
close($IN);
return;
}
while (<$IN>) {
chomp;
# Lose any textual stuff after a tab character
$_ =~ s{\t.*}{};
# Split into location and value, both in octal
my ( $loc, $val ) = split( /:\s+/, $_ );
# Convert from octal and save
$loc = oct($loc);
$val = oct($val);
$Mem[$loc] = $val;
}
close($IN);
}
### read a word from a file in paper tape binary format
### return -1 on EOF
sub read_word {
my $F = shift;
# Convert three bytes into one 18-bit word
return -1 if ( read( $F, my $three, 3 ) != 3 ); # Not enough bytes read
my ( $b1, $b2, $b3 ) = unpack( "CCC", $three );
return ((($b1 & 077) << 12 ) |
(($b2 & 077) << 6 ) |
($b3 & 077));
}
### Copy the arguments into the PDP-7 memory space, and build
### an array of pointers to these arguments. Build a pointer
### at MAXADDR that points at the array.
###
### Each argument string is four words long and space padded if the
### string is not eight characters long. These are stored below
### address MAXADDR. Below this is the count of words in the strings.
### Address MAXADDR points at the word count. Graphically (for two arguments):
###
### +------------+
### +--| | Location 017777 (MAXADDR)
### | +------------+
### | |............|
### | |............| argv[2]
### | |............|
### | +------------+
### | |............|
### | |............| argv[1]
### | |............|
### | +------------+
### | |............|
### | |............| argv[0]
### | |............|
### | +------------+
### +->| argc=12 |
### +------------+
###
sub set_arguments {
# Get the number of arguments including the command name
my $argc = scalar(@ARGV);
# We now know that argc will appear in memory
# 4*argc +1 below location MAXADDR
# Set argc to the number of words
my $addr = MAXADDR - ( 4 * $argc + 1 );
$Mem[MAXADDR] = $addr;
$Mem[ $addr++ ] = $argc * 4;
# Now start saving the arguments
foreach (@ARGV) {
# Truncate and/or space pad the argument
my $str = sprintf( "%-8s", substr( $_, 0, 8 ) );
# XXX: use ascii2words
# Store pairs of characters into memory
for ( my $i = 0 ; $i < length($str) ; $i += 2 ) {
my $c1 = substr( $str, $i, 1 ) || "";
my $c2 = substr( $str, $i + 1, 1 ) || "";
#printf("Saving %06o to %s\n", (ord($c1) << 9 ) | ord($c2), addr($addr));
$Mem[ $addr++ ] = ( ord($c1) << 9 ) | ord($c2);
}
}
}
### Load a namelist file
sub load_names {
my $filename = shift;
open( my $IN, "<", $filename ) || die("Unable to open $filename: $!\n");
while (<$IN>) {
chomp;
if ($_ =~ m{([a-z0-9.]+)\s+([0-7]+)}) {
my $i = oct($2);
$Name2Addr{$1} = $i;
$Addr2Name{$i} = $1;
}
}
close ($IN);
}
### Format an address
sub addr {
my $addr = shift;
my $oct = sprintf("%06o", $addr);
if (%Addr2Name) {
return "$oct ($Addr2Name{$addr})" if ($Addr2Name{$addr});
# XXX keep Addr2Name as a sorted array?
# prefer after to before
for (my $epsilon = 1; $epsilon <= EPSILON; $epsilon++) {
my $n = $Addr2Name{$addr-$epsilon};
return "$oct ($n+$epsilon)" if (defined($n));
}
for (my $epsilon = 1; $epsilon <= EPSILON; $epsilon++) {
my $n = $Addr2Name{$addr+$epsilon};
return "$oct ($n-$epsilon)" if (defined($n));
}
}
return $oct;
}
### convert string (symbol or octal) to address
sub lookup {
my $x = shift;
return oct($x) if ($x =~ m/^[0-7]+$/);
return $Name2Addr{$x} if (defined($Name2Addr{$x}));
return 0;
}
### Simulate the machine code loaded into memory
sub simulate {
# List of opcodes that we can simulate
my %Oplist = (
oct("000") => \&cal,
oct("004") => \&dac,
oct("010") => \&jms,
oct("014") => \&dzm,
oct("020") => \&lac,
oct("030") => \&add,
oct("024") => \&xor,
oct("034") => \&tad,
oct("044") => \&isz,
oct("050") => \&and,
oct("054") => \&sad,
oct("060") => \&jmp,
oct("064") => \&eae,
oct("074") => \&opr,
);
# List of opcodes that DON'T auto-increment
# locations 10-17 when we have the indirect bit
my %NoIncr = (
oct("000") => 1, # cal
oct("064") => 1, # eae
oct("074") => 1 # opr
);
# Loop indefinitely
while (1) {
# Get the instruction pointed to by PC and decode it
# Also do code coverage
my $instruction = $Mem[$PC]; $CC[$PC]++;
my $opcode = ( $instruction >> 12 ) & 074;
my $indirect = ( $instruction >> 13 ) & 1;
my $addr = $instruction & MAXADDR;
# Auto-increment locations 010 to 017 if $indirect
# and this is an instruction that does increment
if ( $indirect
&& ( $addr >= 010 )
&& ( $addr <= 017 )
&& !defined( $NoIncr{$opcode} ) )
{
$Mem[$addr]++;
$Mem[$addr] &= MAXINT;
}
# Work out what any indirect address would be
my $indaddr = ($indirect) ? $Mem[$addr] & MAXADDR : $addr;
# If this is a breakpoint, stop now and get a user command
if ( defined( $Breakpoint{$PC} ) ) {
$singlestep = 1;
printf( "break at PC %s\n", addr($PC) )
if ( ($debug) || ($singlestep) );
}
get_user_command() if ($singlestep);
printf( "PC %-20.20s L.AC %d.%06o MQ %06o: ", addr($PC),
($LINK ? 1 : 0), $AC & 0777777, $MQ )
if ( ($debug) || ($singlestep) );
#dprintf("PC %06o: instr %06o, op %03o, in %o, addr %06o indaddr %06o\n",
# $PC, $instruction, $opcode, $indirect, $addr, $indaddr );
# Simulate the instruction. Each subroutine updates the $PC
if ( defined( $Oplist{$opcode} ) ) {
$Oplist{$opcode}->( $instruction, $addr, $indaddr );
}
else {
printf( STDERR "Unknown instruction 0%06o at location %s\n",
$instruction, addr($PC) );
exit(1);
}
}
}
# Debug code: dump memory contents
# Print from $start to $end.
# Print empty locations if $yeszero
sub dump_memory {
my ( $start, $end, $yeszero ) = @_;
foreach my $i ( $start .. $end ) {
# Convert the word into possibly two ASCII characters
my $c1 = ( $Mem[$i] >> 9 ) & 0777;
$c1 = ( $c1 < 0200 ) ? chr($c1) : " ";
my $c2 = $Mem[$i] & 0777;
$c2 = ( $c2 < 0200 ) ? chr($c2) : " ";
printf( STDERR "%06o: %06o %s%s\n", $i, $Mem[$i], $c1, $c2 )
if ( $yeszero || $Mem[$i] != 0 );
}
}
# Load AC
sub lac {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "lac %s (value %06o) into AC\n", addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
$AC = $Mem[$indaddr];
$PC++;
}
# Deposit AC
sub dac {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "dac AC (value %06o) into %s\n", $AC, addr($indaddr) )
if ( ($debug) || ($singlestep) );
# Catch writes below the process' memory range
if ($indaddr < 010000 && !($indaddr >= 010 && $indaddr <= 020) ) {
$singlestep = 1;
dprintf("(****WRITE TO LOW MEMORY 0%o ****)\n", $indaddr);
printf( "break at PC %s\n", addr($PC) )
if ( ($debug) || ($singlestep) );
}
$Mem[$indaddr] = $AC;
$PC++;
}
# Add to AC, twos complement
sub tad {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "tad AC (value %06o) with addr %s (%06o)\n",
$AC, addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
$AC = $AC + $Mem[$indaddr];
$LINK = ( $LINK ^ $AC ) & LINKMASK;
$AC = $AC & MAXINT;
$PC++;
}
# Add to AC, ones complement
sub add {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "add AC (value %06o) with addr %s (%06o)\n",
$AC, addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
# This logic shamelessly borrowed from SimH
# https://github.com/simh/simh/blob/master/PDP18B/pdp18b_cpu.c
my $sum = $AC + $Mem[$indaddr];
if ( $sum > MAXINT ) { # end around carry
$sum = ( $sum + 1 ) & MAXINT;
}
if ( ( ( ~$AC ^ $sum ) & ( $AC ^ $sum ) ) & SIGN ) { # overflow?
$LINK = LINKMASK; # set link
}
$AC = $sum;
$PC++;
}
# And AC and Y
sub and {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "and AC (value %06o) with addr %s (%06o)\n",
$AC, addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
$AC &= $Mem[$indaddr];
$PC++;
}
# Xor AC and Y
sub xor {
my ( $instruction, $addr, $indaddr ) = @_;
dprintf( "xor %s (%06o)\n",
addr($indaddr), $Mem[$indaddr] );
$AC ^= $Mem[$indaddr];
$PC++;
}
# Skip if AC different to Y
sub sad {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "sad %s (%06o)\n", addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
if ($AC != $Mem[$indaddr]) {
dprintf( " adding 2 to PC\n");
} else {
dprintf( " adding 1 to PC\n");
}
$PC += ( $AC != $Mem[$indaddr] ) ? 2 : 1;
}
# Deposit zero in memory
sub dzm {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "dzm %s\n", addr($indaddr) )
if ( ($debug) || ($singlestep) );
$Mem[$indaddr] = 0;
$PC++;
}
# Index and skip if zero
sub isz {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "isz %s (value %06o)\n", addr($indaddr), $Mem[$indaddr] )
if ( ($debug) || ($singlestep) );
$Mem[$indaddr]++;
$Mem[$indaddr] &= MAXINT;
$PC += ( $Mem[$indaddr] == 0 ) ? 2 : 1;
}
# Jump
sub jmp {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "jmp %s\n", addr($indaddr) )
if ( ($debug) || ($singlestep) );
$PC = $indaddr;
}
# Jump to subroutine
sub jms {
my ( $instruction, $addr, $indaddr ) = @_;
printf( "jms %s\n", addr($indaddr) )
if ( ($debug) || ($singlestep) );
# Save the LINK and current PC into the $indaddr location
$Mem[ $indaddr++ ] = $PC + 1 | ( ($LINK) ? 0400000 : 0 );
$PC = $indaddr;
}
# OPR instructions
sub opr {
my ( $instruction, $addr, $indaddr ) = @_;
# hlt: halt simulation
if ( $instruction == 0740040 ) {
printf( STDERR "PC %s: program halted\n", addr($PC) );
dump_memory( 0, MAXADDR, 0 ) if ($debug);
exit(1);
}
# law: load word into AC
my $indirect = ( $instruction >> 13 ) & 1;
if ($indirect) {
dprintf( "law %06o into AC\n", $instruction );
$AC = $instruction;
$PC++;
return;
}
# List of skip opcode names for the next section
my @skipop = (
'', 'sma', 'sza', 'sza sma',
'snl', 'snl sma', 'snl sza', 'snl sza sma',
'skp', 'spa', 'sna', 'sna spa',
'szl', 'szl spa', 'szl sna', 'szl sna spa'
);
# This logic shamelessly borrowed from SimH
# https://github.com/simh/simh/blob/master/PDP18B/pdp18b_cpu.c
my $skip = 0;
my $i = ( $instruction >> 6 ) & 017; # decode IR<8:11>
dprintf( "%s", $skipop[$i] );
$skip = 1 if ( ( $i == 1 ) && ( $AC & SIGN ) != 0 ); # sma
$skip = 1 if ( ( $i == 2 ) && ( $AC & MAXINT ) == 0 ); # sza
$skip = 1
if ( ( $i == 3 )
&& ( ( ( $AC & MAXINT ) == 0 ) || ( ( $AC & SIGN ) != 0 ) ) )
; # sza | sma
$skip = 1 if ( ( $i == 4 ) && ($LINK) ); # snl
$skip = 1 if ( ( $i == 5 ) && ( $LINK || ( $AC >= SIGN ) ) ); # snl | sma
$skip = 1 if ( ( $i == 6 ) && ( $LINK || ( $AC == 0 ) ) ); # snl | sza
$skip = 1
if ( ( $i == 7 )
&& ( $LINK || ( $AC >= SIGN ) || ( $AC == 0 ) ) ); # snl | sza | sma
$skip = 1 if ( $i == 010 ); # skp
$skip = 1 if ( ( $i == 011 ) && ( ( $AC & SIGN ) == 0 ) ); # spa
$skip = 1 if ( ( $i == 012 ) && ( ( $AC & MAXINT ) != 0 ) ); # sna
$skip = 1
if ( ( $i == 013 )
&& ( ( $AC & MAXINT ) != 0 )
&& ( ( $AC & SIGN ) == 0 ) ); # sna & spa
$skip = 1 if ( ( $i == 014 ) && ( $LINK == 0 ) ); # szl
$skip = 1
if ( ( $i == 015 ) && ( $LINK == 0 ) && ( $AC < SIGN ) ); # szl & spa
$skip = 1
if ( ( $i == 016 ) && ( $LINK == 0 ) && ( $AC != 0 ) ); # szl & sna
$skip = 1
if ( ( $i == 017 )
&& ( $LINK == 0 )
&& ( $AC < SIGN )
&& ( $AC != 0 ) ); # szl & sna & spa
# Clear operations
if ( $instruction & 010000 ) { # cla
dprintf(" cla");
$AC = 0;
}
if ( $instruction & 004000 ) { # cll
dprintf(" cll");
$LINK = 0;
}
if ( $instruction & 000002 ) { # cml
dprintf(" cml");
$LINK = ($LINK) ? 0 : LINKMASK;
}
if ( $instruction & 000001 ) { # cma
dprintf(" cma");
$AC = ( $AC ^ MAXINT ) & MAXINT;
}
# Rotate instructions
$i = $instruction & 02030;
# Single rotate right
if ( $i == 020 ) {
dprintf(" rar");
my $newlink = ( $AC & 1 ) ? LINKMASK : 0;
$AC = ( $LINK | $AC ) >> 1;
$LINK = $newlink;
}
# Double rotate right
if ( $i == 02020 ) {
dprintf(" rtr");
my $msb = ( $AC & 1 ) << 17;
my $newlink = ( $AC & 2 ) ? LINKMASK : 0;
$AC = ( ( $LINK | $AC ) >> 2 ) | $msb;
$LINK = $newlink;
}
# Single rotate left
if ( $i == 010 ) {
dprintf(" ral");
my $newlink = ( $AC & SIGN ) ? LINKMASK : 0;
my $lsb = $LINK ? 1 : 0;
$AC = ( ( $AC << 1 ) | $lsb ) & MAXINT;
$LINK = $newlink;
}
# Double rotate left
if ( $i == 02010 ) {
dprintf(" rtl");
my $newlink = ( $AC & 0200000 ) ? LINKMASK : 0;
my $lsb = ( $AC & SIGN ) ? 1 : 0;
my $twolsb = $LINK ? 2 : 0;
$AC = ( ( $AC << 2 ) | $twolsb | $lsb ) & MAXINT;
$LINK = $newlink;
}
# Impossible left and right rotates: 02030 or 00030. Do nothing!
# Note: We didn't do the oas instruction above.
$PC += 1 + $skip;
dprintf("\n");
return;
}
# Extended arithmetic element instructions
sub eae {
my ( $instruction, $addr, $indaddr ) = @_;
my $step = $instruction & EAESTEP;
my $maskedinstr = $instruction & EAEIMASK;
if ( $instruction == 0653323 ) { # idiv: integer division
my $divisor= $Mem[ $PC+1 ];
dprintf( "idiv %06o by %06o (decimal %d by %d)\n",
$AC, $divisor, $AC, $divisor );
# Prevent division by zero :-)
my $quotient = ($divisor) ? int($AC / $divisor) : 0;
my $remainder = ($divisor) ? $AC % $divisor : 0;
$MQ= $quotient;
$AC= $remainder;
$PC+=2;
return;
}
if ( $instruction == 0640323 ) { # div: 36-bit unsigned integer division
my $divisor= $Mem[ $PC+1 ];
dprintf( "div MQ.AC %06o.%06o by %06o (decimal %d)\n",
$MQ, $AC, $divisor, $divisor );
# http://www.perlmonks.org/?node_id=718414 says that we won't
# lose accuracy before 2^53
my $dividend= ($MQ << 18) | $AC;
# Prevent division by zero :-)
my $quotient = ($divisor) ? $dividend / $divisor : 0;
my $remainder = ($divisor) ? $dividend % $divisor : 0;
$MQ= $quotient;
$AC= $remainder;
$PC+=2;
return;
}
if ( $maskedinstr == 0660500 ) { # lrss: long right shift, signed
# We ignore the MQ as it's not
# used by any user-mode programs
dprintf( "lrss %06o AC step %d\n", $AC, $step );
# Save the AC's sign into LINK
my $newlink = ( $AC << 1 ) & LINKMASK;
# $AC = ( ( $LINK | $AC ) >> $step ) & MAXINT;
$AC = ( ( ($newlink * -1) | $AC ) >> $step ) & MAXINT; # XXX Not sure if this is correct!?!?!
$LINK = $newlink;
$PC++;
return;
}
if ( $maskedinstr == 0660700 ) { # alss: AC left shift, signed
dprintf( "alss step %d\n", $step );
$AC = ( $AC << $step ) & MAXINT;
$LINK = ( $AC << 1 ) & LINKMASK;
$PC++;
return;
}
if ( ($maskedinstr == 0660600) ||
($maskedinstr == 0661600) ) { # llss: long left shift, signed
# Set the link to be the AC sign bit
$LINK= ($AC & SIGN) ? LINKMASK : 0;
$AC = 0 if ($maskedinstr & 01000); # PLB: ecla llss seen in adm,apr,bc,ds,sh
dprintf( "llss step %d\n", $step );
foreach my $i ( 1 .. $step ) {
my $MQmsb = ( $MQ & SIGN ) ? 1 : 0;
$AC = ( ( $AC << 1 ) | $MQmsb ) & MAXINT;
$MQ = ( ( $MQ << 1 ) | ( ($LINK) ? 1 : 0 ) ) & MAXINT;
}
$PC++;
return;
}
# lls: long left shift
if ( ($maskedinstr == 0640600) || ($maskedinstr == 0641600) ) {
dprintf( "lls step %d\n", $step );
# Clear AC if the 01000 bit is set
$AC=0 if ($maskedinstr == 0641600);
foreach my $i ( 1 .. $step ) {
my $MQmsb = ( $MQ & SIGN ) ? 1 : 0;
$AC = ( ( $AC << 1 ) | $MQmsb ) & MAXINT;
$MQ = ( ( $MQ << 1 ) | ( ($LINK) ? 1 : 0 ) ) & MAXINT;
}
$PC++;
return;
}
# lrs: long right shift
if (($maskedinstr & 0766777) == 0640500) {
dprintf( "lrs step %d\n", $step );
# Clear AC if the 01000 bit is set
$AC=0 if ($maskedinstr & 01000);
# Clear MQ if the 010000 bit is set
$MQ=0 if ($maskedinstr & 010000);
foreach my $i ( 1 .. $step ) {
my $MQmsb = ( $AC & 1 ) ? 0400000 : 0;
$AC = ( ( $AC >> 1 ) | ( ($LINK) ? 0400000 : 0 ) ) & MAXINT;
$MQ = ( ( $MQ >> 1 ) | $MQmsb ) & MAXINT;
}
$PC++;
return;
}
if ( $maskedinstr == 0640700 ) { # als: AC left shift
dprintf( "als AC step %d\n", $step );
$AC = ( $AC << $step ) & MAXINT;
$PC++;
return;
}
if ( $instruction == 0652000 ) { # lmq: load MC from AC
dprintf( "lmq AC %06o into MQ\n", $AC );
$MQ = $AC;
$PC++;
return;
}
if ( $instruction == 0641002 ) { # lacq: load AC from MQ
dprintf( "lacq MQ %06o into AC\n", $MQ );
$AC = $MQ;
$PC++;
return;
}
if ( $instruction == 0640002 ) { # lacq: OR AC with MQ
dprintf( "omq MQ %06o and AC %06o\n", $MQ, $AC );
$AC |= $MQ;
$PC++;
return;
}
if ( $instruction == 0653122 ) { # mul: unsigned multiply
# This logic shamelessly borrowed from SimH
# https://github.com/simh/simh/blob/master/PDP18B/pdp18b_cpu.c
my $MB= $Mem[ $PC+1 ];
my $eae_ac_sign;
dprintf("mul AC %06o by %06o (decimal %d by %d)\n", $AC, $MB, $AC, $MB);
if (($instruction & 0004000) && ($AC & SIGN)) { # IR<6> and minus?
$eae_ac_sign = $LINK; # set eae_ac_sign
} else {
$eae_ac_sign = 0; # if not, unsigned
}
$MQ = $MQ ^ MAXINT if ($eae_ac_sign); # EAE AC sign? ~MQ
my $oldlink= $LINK;
$LINK = 0; # Clear link
my $result= $AC * $MB;
$AC= ($result >> 18) & MAXINT;
$MQ= $result & MAXINT;
## foreach my $SC (1 .. $instruction & 077) { # Loop for SC times
## $AC = $AC + $MB
## if ($MQ & 1); # MQ<17>? add
## $MQ = ($MQ >> 1) | (($AC & 1) << 17);
## $AC = $AC >> 1; # Shift AC'MQ right
## }
if ($eae_ac_sign ^ $oldlink) { # Result negative?
$AC = $AC ^ MAXINT;
$MQ = $MQ ^ MAXINT;
}
$PC+=2;
return;
}
printf( STDERR "PC %s: Unknown eae instruction %06o\n",
addr($PC), $instruction );
exit(1);
}
# cal: used for system calls
sub cal {
my ( $instruction, $addr, $indaddr ) = @_;
# Syscalls that we can simulate
my %Syscallist = (
# 1: save
2 => \&sys_getuid,
3 => \&sys_open,
4 => \&sys_read,
5 => \&sys_write,
6 => \&sys_creat,
7 => \&sys_seek,
# 8 tell
9 => \&sys_close,
10 => \&sys_link,
11 => \&sys_unlink,
12 => \&sys_setuid,
13 => \&sys_rename,
14 => \&sys_exit,
15 => \&sys_time,
16 => \&sys_intrp,
17 => \&sys_chdir,
18 => \&sys_chmod,
19 => \&sys_chown,
# 20 badcal
# 21 syslog
# 22 badcal
# 23 capt
# 24 rele
25 => \&sys_status,
# 26 badcal
27 => \&sys_smes,
28 => \&sys_rmes,
29 => \&sys_fork,
);
# Simulate the syscall. Each syscall updates the $PC
if ( defined( $Syscallist{$addr} ) ) {
$Syscallist{$addr}->();
}
else {
printf( STDERR "PC %s: Unknown syscall %d\n", addr($PC), $addr );
exit(1);
}
}
# Exit system call
sub sys_exit {
dprintf( "exit system call, pid %06o\n", $$ );
print_coverage() if ($coverage);
exit(0);
}
# Getuid system call
sub sys_getuid {
$AC = $< & MAXINT;
# On PDP-7 Unix, the root user is user-id -1
$AC= MAXINT if ($AC==0);
dprintf( "getuid system call, uid %06o\n", $AC );
$PC += 1;
return;
}
# Setuid system call
sub sys_setuid {
# For now, do nothing
dprintf("setuid system call\n");
$PC += 1;
return;
}
# Intrp system call
sub sys_intrp {
# For now, do nothing
dprintf("intrp system call\n");
$PC += 1;
return;
}
# Fork system call
sub sys_fork {
# Fork and get the child's process-id back, or zero if we are the child
my $pid = fork();
$AC = $pid & MAXINT;
dprintf( "fork, got id %06o\n", $AC );
# The parent returns back to PC+1, the child returns to PC+2
$PC += ($pid) ? 1 : 2;
return;
}
# shell depends on smes hanging while child process exists
# https://www.bell-labs.com/usr/dmr/www/hist.html
# The message facility was used as follows: the parent shell, after
# creating a process to execute a command, sent a message to the new
# process by smes; when the command terminated (assuming it did not
# try to read any messages) the shell's blocked smes call returned an
# error indication that the target process did not exist. Thus the
# shell's smes became, in effect, the equivalent of wait.
sub sys_smes {
waitpid($AC,0);
dprintf("smes returning error\n");
$AC = -1;
$PC += 1;
}
# Rmes system call. We simply call wait and
# return the process-id in AC
sub sys_rmes {
my $pid = wait();
dprintf("rmes system call, got pid $pid\n");
$AC = $pid & MAXINT;
$PC += 1;
return;
}
# Close system call
sub sys_close {
# AC is the file descriptor
my $fd = $AC;
dprintf( "close: closing fd %d\n", $fd );
# Bump up the PC
$PC += 1;
# That filehandle is not open, set an error -1 in octal
if ( !defined( $FD[$fd] ) ) {
dprintf("close: fd $fd is not open\n");
$AC = MAXINT;
return;
}
close( $FD[$fd] );
$FD[$fd] = undef;
$ISBINARY[$fd] = 0; # For next time
$AC = 0;
return;
}
# Open something which could be a file or a directory
# Convert directories into files. Return the file handle and
# if the file is ASCII or binary.
sub opensomething {
my ( $readorwrite, $filename ) = @_;
my $tempfile = "/tmp/a7out.$$";
my $FH;
# If this is not a directory, open it and return the FH
if ( !-d $filename ) {
open( $FH, $readorwrite, $filename ) || return (undef);
# Opened for writing, so for now this is not binary
return ( $FH, 0) if ($readorwrite eq ">");
# Determine if the file is pure ASCII or contains 18-bit
# words encoded in 24-bit groups. We test the msb of the
# first character in the file. If it's on then it's a
# binary file and not ASCII.
# XXX: This means that we have to seek back to the beginning,
# which may be a problem on things like stdin.
my $ch = getc($FH);
my $isbinary = ( defined($ch) && ( ord($ch) & 0x80 ) ) ? 1 : 0;
binmode($FH) if ($isbinary);
seek( $FH, 0, SEEK_SET );
return ( $FH, $isbinary );
}
# It's a directory. The on-disk format for this was:
# d.i: .=.+1 " inode number
# d.name: .=.+4 " name (space padded)
# d.uniq: .=.+1 " unique number from directory inode
# followed by two unused words
# The code creates a temporary file and fills in the i-node numbers
# and space padded filenames from the directory. The file is closed
# opened read-only and unlinked, and the open filehandle is returned.
opendir( my $dh, $filename ) || return (undef);
open( $FH, ">", $tempfile ) || return (undef);
dprintf("Converting directory $filename\n");
my @list = sort( readdir($dh) );
foreach my $name (@list) {
# Get the file's i-node number and write it
my ( undef, $inode ) = stat($name);
print( $FH word2three($inode) );
# Convert the name into 8 characters, space padded
my $spaceword = sprintf( "%-8s", substr( $name, 0, 8 ) );
# Convert to four words and write each as three bytes
foreach my $word ( ascii2words($spaceword) ) {
print( $FH word2three($word) );
}
# Now write three zero words to pad to eight in total
print( $FH word2three(0) );
print( $FH word2three(0) );
print( $FH word2three(0) );
}
closedir($dh);
close($FH);
open( $FH, "<", $tempfile ) || return (undef);
binmode($FH);
#exit(0);
unlink($tempfile);
return ( $FH, 1 );
}
# Common code for creat and open
sub creatopen {
my ( $filename, $readorwrite ) = @_;
# Open the file
my ( $FH, $isbinary ) = opensomething( $readorwrite, $filename );
if ($FH) {
# Find a place in the @FD array to store this filehandle.
# 99 is arbitrary
foreach my $fd ( 0 .. 99 ) {
if ( !defined( $FD[$fd] ) ) {
$FD[$fd] = $FH;
$ISBINARY[$fd] = $isbinary;
$AC = $fd;
last;
}
}
}
else {
# No filehandle, so it's an error
dprintf("open failed: $!\n");
$AC = MAXINT;
}
}
# Open system call
sub sys_open {
# Open seems to have 2 arguments: PC+1 is a pointer to the filename.
# PC+2 seems to be 1 for write, 0 for read.
# Some programs seem to have a third argument always set to 0.
# AC is the opened fd on success, or -1 on error
# Get the start address of the string
# Convert this to a sensible ASCII filename
my $start = $Mem[ $PC + 1 ];
my $filename = mem2arg($start);
# Choose to open read-only or write-only
my $readorwrite = ( $Mem[ $PC + 2 ] ) ? ">" : "<";
dprintf( "open: base %06o, %s file %s\n", $start, $readorwrite, $filename );
# Bump up the PC
$PC += 3;
# Now open the file and return
creatopen( $filename, $readorwrite );
}
# Creat system call
sub sys_creat {
# Creat seems to have 1 argument: PC+1 is a pointer to the filename.
# Some programs seem to have a second argument always set to 0.
# AC is the opened fd on success, or -1 on error
# Get the start address of the string
my $start = $Mem[ $PC + 1 ];
# Convert this to a sensible ASCII filename
my $filename = mem2arg($start);
# Choose to open write-only
my $readorwrite = ">";
dprintf( "creat: base %06o, file %s\n", $start, $filename );
# Bump up the PC
$PC += 2;
# Now open the file and return
creatopen( $filename, $readorwrite );
}
# Read system call
sub sys_read {
# Read seems to have arguments: AC is the file descriptor, PC+1 is
# the pointer to the buffer and PC+2 is the number of words to read.
# Return the number of words read in AC on success, or -1 on error.
# Get the file descriptor, start address and end address
my $fd = $AC;
my $start = $Mem[ $PC + 1 ];
my $count = $Mem[ $PC + 2 ];
my $end = ( $start + $count - 1 ) & MAXADDR;
die("sys_read: bad start/end addresses $start $end\n") if ( $end < $start );
printf( "read: %d words into %s from fd %d\n", $count, addr($start), $fd )
if ( ($debug) || ($singlestep) );
# Bump up the PC
$PC += 3;
# That filehandle is not open, set an error -1 in octal
if ( !defined( $FD[$fd] ) ) {
dprintf("read: fd $fd is not open\n");
$AC = MAXINT;
return;
}
# Read each word in
my $FH = $FD[$fd];
$count = 0;
if (-t $FH) { # TTY?
my $char = getc($FH); # use Term::ReadKey for 'cbreak' mode??
if (defined($char)) {
$Mem[$start] = ord($char) << 9; # only ever returns one char
$AC = 1;
}
else {
$AC = 0; # EOF
}
return;
}
foreach my $addr ( $start .. $end ) {
if ( $ISBINARY[$fd] ) {
# Convert three bytes into one 18-bit word
my $result = read_word($FH);
last if ($result == -1);
$Mem[$addr] = $result;
$count++;
}
else {
# Convert two ASCII characters into one 18-bit word
my $c1 = getc($FH);
last if ( !defined($c1) ); # No character, leave the loop
my $word = ord($c1) << 9;
my $c2 = getc($FH);
$word |= ord($c2) if (defined($c2));
$Mem[$addr] = $word;
$count++;
} # ascii
}
# No error
$AC = $count;
return;
}
# Write system call
sub sys_write {
# Write seems to have arguments: AC is the file descriptor, PC+1 is
# the pointer to the buffer and PC+2 is the number of words to write
# Get the file descriptor, start address and end address
my $fd = $AC;
my $start = $Mem[ $PC + 1 ];
my $count = $Mem[ $PC + 2 ];
my $end = ( $start + $count - 1 ) & MAXADDR;
die("sys_write: bad start/end addresses $start $end\n")
if ( $count != 0 && $end < $start );
printf( "write: %d words from %s to fd %d\n", $count, addr($start), $fd )
if ( ($debug) || ($singlestep) );
# Bump up the PC
$PC += 3;
# That filehandle is not open, set an error -1 in octal
if ( !defined( $FD[$fd] ) ) {
dprintf("write: fd $fd is not open\n");
$AC = MAXINT;
return;
}
# Write each word out either in binary or in ASCII
my $FH = $FD[$fd];
foreach my $addr ( $start .. $end ) {
# First see if any "non-ASCII" bits are set in the word.
# If so, then this is a binary file
my $word= $Mem[$addr];
$ISBINARY[$fd]=1 if ($word & 0600600);
if ($ISBINARY[$fd]) {
print( $FH word2three($word) );
} else {
print( $FH word2ascii($word) );
}
}
# No error
$AC = 0;
return;
}
# Chmod system call
sub sys_chmod {
# Chmod gets the permission bits in AC and a pointer
# to the file's name in PC+1. s2.s has these instruction for chmod:
# lac u.ac; and o17 so only the lowest 4
# bits are the permission bits that can be set.
# I'm going to guess these (from v1 chmod manual):
# 01 write for non-owner
# 02 read for non-owner
# 04 write for owner
# 10 read for owner
my $mode = 0;
$mode |= 0002 if ( $AC & 01 );
$mode |= 0004 if ( $AC & 02 );
$mode |= 0220 if ( $AC & 04 );
$mode |= 0440 if ( $AC & 010 );
my $start = $Mem[ $PC + 1 ];
my $filename = mem2arg($start);
dprintf( "chmod %06o file %s\n", $mode, $filename );
# Do the chmod on the file
my $result = chmod( $mode, $filename );
# Set AC to -1 if no files were changed, else 0
$AC = ( $result == 0 ) ? MAXINT : 0;
$PC += 2;
return;
}
# Chown system call
sub sys_chown {
# Chown gets the numeric user-id in AC and a pointer
# to the file's name in PC+1.
# Get the start address of the string
# Convert this to a sensible ASCII filename
my $start = $Mem[ $PC + 1 ];
my $filename = mem2arg($start);
dprintf( "chown file %s to uid %06o\n", $filename, $AC );
# Do the chown, leave group-id untouched. Get number of files changed
my $result = chown( $AC, -1, $filename );
# Set AC to -1 if no files were changed, else 0
$AC = ( $result == 0 ) ? MAXINT : 0;
$PC += 2;
return;
}
# Chdir system call
sub sys_chdir {
# Chdir gets the directory name in PC+1
# Return 0 on success, -1 on error
# Convert this to a sensible ASCII filename
my $start = $Mem[ $PC + 1 ];
my $filename = mem2arg($start);
dprintf( "chdir %s\n", $filename );
# Bump up the PC
$PC += 2;
# Do nothing on chdir to "dd"
return (0) if ( $filename eq "dd" );
# Do the chdir
return ( chdir($filename) ? 0 : MAXINT );
}
# Unlink system call
sub sys_unlink {
# Unlink gets the file name in PC+1
# Return 0 on success, -1 on error
# Convert this to a sensible ASCII filename
my $start = $Mem[ $PC + 1 ];
my $filename = mem2arg($start);
dprintf( "unlink %s\n", $filename );
# Bump up the PC and do the unlink
$PC += 2;
return ( unlink($filename) ? 0 : MAXINT );
}
# Time system call
sub sys_time {
# Dennis' draft says: The call sys time returns in
# the AC and MQ registers the number of sixtieths of
# a second since the start of the current year.
# Get two Datetime objects set to now
my $dt = DateTime->now;
my $yearstart = DateTime->now;
# Set one object back to the beginning of the year
$yearstart->set( month => 1 );
$yearstart->set( day => 1 );
$yearstart->set( hour => 0 );
$yearstart->set( minute => 0 );
$yearstart->set( second => 0 );
# Get the duration in sixtieths of a second
my $duration = $dt->subtract_datetime_absolute($yearstart);
my $sixtieths = $duration->seconds() * 60;
# Set MQ to the high 18 bits and AC to the low 18 bits
$MQ = $sixtieths >> 18;
$AC = $sixtieths & 0777777;
dprintf( "time %06o %06o, %d sixtieths\n", $MQ, $AC, $sixtieths );
$PC += 1;
return;
}
# Status system call
sub sys_status {
# AC holds the pointer to the stat buffer
# PC+1 is the directory holding the entry
# PC+2 is the directory entry we want to stat.
# The statbuf is:
# word 0: permission bits
# words 1-7: disk block pointers
# word 8: user-id
# word 9: number of links
# word 10: size in words
# word 11: uniq, I have no idea what this is.
# word 12: i-number.
# The permission bits are:
# 200000 large file, bigger than 4096 words
# 000020 directory
# 000010 owner read
# 000004 owner write
# 000002 user write
# 000001 user write
# Get the directory and file names
# Convert this to a sensible ASCII filename
my $dirname = mem2arg($Mem[ $PC + 1 ]);
my $filename = mem2arg($Mem[ $PC + 2 ]);
dprintf( "status file %s/%s statbuf %06o\n", $dirname, $filename, $AC );
# Get the file's details
my ( undef, $ino, $mode, $nlink, $uid, undef, undef, $size ) =
stat("$dirname/$filename");
# Set up the statbuf if we got a result
if ($nlink) {
$Mem[ $AC + 8 ] = $uid & MAXINT;
$Mem[ $AC + 9 ] = (-$nlink) & MAXINT;
$Mem[ $AC + 10 ] = $size & MAXINT; # Yes, I know, not words
$Mem[ $AC + 12 ] = $ino & MAXINT;
my $perms = 0;
$perms = 01 if ( $mode & 02 ); # World writable
$perms |= 02 if ( $mode & 04 ); # World readable
$perms |= 04 if ( $mode & 0200 ); # Owner writable
$perms |= 010 if ( $mode & 0400 ); # Owner readable
$perms |= 020 if ( -d $filename ); # Directory
$perms |= 0200000 if ( $size > 4096 ); # Large file
$Mem[$AC] = $perms;
# Set AC to zero as we got something, else return -1
$AC = 0;
}
else {
$AC = MAXINT;
}
$PC += 3;
return;
}
# Seek syscall
sub sys_seek {
# Seek takes three arguments: AC is the fd, PC+1 is a signed count
# and PC+2 is how to seek: 0=from start, 1=from curptr, 2=from end
# of file. Return AC=0 if OK, -1 on error.
my $fd= $AC;
my $FH= $FD[$fd];
my $offset= $Mem[ $PC + 1 ];
# XXX For now, we always do SEEK_SET.
# If it's a binary file, we have to seek 3 bytes for every word,
# but for an ASCII file that's 2 bytes per word.
$offset *= ($ISBINARY[$fd]) ? 3 : 2;
my $result= seek($FH, $offset, SEEK_SET);
# Set the AC result
$AC= ($result)? 0: MAXINT;
$PC += 3;
return;
}
# Rename syscall
sub sys_rename {
# Rename takes two arguments: PC+1 is the current filename and
# PC+2 is the new filename. Returns AC=0 on success, AC=-1 on error.
#
my $oldname = mem2arg($Mem[$PC+1]);
my $newname = mem2arg($Mem[$PC+2]);
dprintf( "rename file %s to %s\n", $oldname, $newname );
my $result= rename($oldname, $newname);
# Set the AC result
$AC= ($result)? 0: MAXINT;
$PC += 3;
return;
}
# Link syscall
sub sys_link {
# Link takes two arguments: PC+1 is the current filename and
# PC+2 is the new filename. Returns AC=0 on success, AC=-1 on error.
# Yes, this is not strictly what PDP-7 Unix would have done.
#
my $oldname = mem2arg($Mem[$PC+1]);
my $newname = mem2arg($Mem[$PC+2]);
dprintf( "link file %s to %s\n", $oldname, $newname );
my $result= link($oldname, $newname);
# Set the AC result
$AC= ($result)? 0: MAXINT;
$PC += 3;
return;
}
# Convert an 18-bit word into a scalar which has three sixbit
# values in three bytes. Set the msb in the first byte
sub word2three {
my $val = shift;
my $b1 = ( ( $val >> 12 ) & 077 ) | 0x80;
my $b2 = ( $val >> 6 ) & 077;
my $b3 = $val & 077;
return ( pack( "CCC", $b1, $b2, $b3 ) );
}
# Convert an ASCII string into an array of 18-bit word values
# where two characters are packed into each word. Put NUL in
# if the string has an odd number of characters. Return the array
sub ascii2words {
my $str = shift;
my @words;
for ( my $i = 0 ; $i < length($str) ; $i += 2 ) {
my $c1 = substr( $str, $i, 1 ) || "\0";
my $c2 = substr( $str, $i + 1, 1 ) || "\0";
push( @words, ( ord($c1) << 9 ) | ord($c2) );
}
return (@words);
}
# Convert an 18-bit word into two ASCII characters and return them.
# Don't return NUL characters
sub word2ascii {
my $word = shift;
my $c1 = ( $word >> 9 ) & 0177;
my $c2 = $word & 0177;
my $result = "";
if (($c1 >= 1) && ($c1 <= 126)) { $result .= chr($c1); }
if (($c2 >= 1) && ($c2 <= 126)) { $result .= chr($c2); }
return ($result);
}
# Given the address of a four word argument string in
# memory, return a copy of the string in ASCII format.
# Lose any trailing spaces as well.
sub mem2arg {
my $addr = shift;
my $result = "";
$addr &= MAXADDR;
foreach ( 1 .. 4 ) {
# Stop if the address leave the 8K word address space
last if ( $addr > MAXADDR );
my $word = $Mem[ $addr++ ];
my $c1 = ( $word >> 9 ) & 0177;
my $c2 = $word & 0177;
$result .= chr($c1) . chr($c2);
}
$result =~ s{ *$}{};
return ($result);
}
# Print out debug messages
sub dprintf {
printf( STDERR @_ ) if ( ($debug) || ($singlestep) );
}
# Get one or more commands from the user and execute them
sub get_user_command {
my %Cmdlist = (
'b' => \&cmd_setbreak,
'break' => \&cmd_setbreak,
'd' => \&cmd_dump,
'dump' => \&cmd_dump,
'db' => \&cmd_delbreak,
'del' => \&cmd_delbreak,
'delete' => \&cmd_delbreak,
'?' => \&cmd_help,
'h' => \&cmd_help,
'help' => \&cmd_help,
's' => \&cmd_step,
'l' => \&cmd_listbreak,
'list' => \&cmd_listbreak,
'step' => \&cmd_step,
'q' => \&cmd_exit,
'x' => \&cmd_exit,
'quit' => \&cmd_exit,
'exit' => \&cmd_exit,
'c' => \&cmd_continue,
'continue' => \&cmd_continue,
'r' => \&cmd_showregs,
'regs' => \&cmd_showregs,
);
# Loop until we get a leave result
while (1) {
# Get a command from the user
# and split into command, start and end addresses.
# Convert addresses from octal
print("a7out> ");
chomp( my $line = <STDIN> );
my ( $cmd, $addr, $endaddr ) = split( /\s+/, $line );
$addr = lookup($addr) if ( defined($addr) );
$endaddr = lookup($endaddr) if ( defined($endaddr) );
# Run the command
my $leave;
if ( defined($cmd) && defined( $Cmdlist{$cmd} ) ) {
$leave = $Cmdlist{$cmd}->( $addr, $endaddr );
}
else {
printf( "%s: unknown command\n", $cmd || "" );
cmd_help();
}
return if ($leave);
}
}
# Exit the program
sub cmd_exit {
exit(0);
}
# Continue by disabling single-step
# and break out of the command loop
sub cmd_continue {
$singlestep = 0;
return (1);
}
# Step by staying in single-step
# but break out of the command loop
sub cmd_step {
return (1);
}
# Set a breakpoint
sub cmd_setbreak {
my $addr = shift;
$Breakpoint{$addr} = 1;
return (0);
}
# Delete a breakpoint
sub cmd_delbreak {
my $addr = shift;
delete( $Breakpoint{$addr} );
printf( "Delete breakpoint %06o\n", $addr );
return (0);
}
sub cmd_help {
print(" [b]reak <octal> set a breakpoint\n");
print(" [c]ontinue leave single-step and continue\n");
print(" [d]ump [<octal>] [<octal>] dump addresses in range\n");
print(" db <octal> delete a breakpoint\n");
print(" [del]ete <octal> delete a breakpoint\n");
print(" [l]ist list breakpoints\n");
print(" [r]egs print PC, LINK, AC and MQ regs\n");
print(" [s]tep single-step next instruction\n");
print(" ?, h, help print this help list\n");
print(" e[x]it, [q]uit exit the program\n");
return (0);
}
sub cmd_showregs {
my $link = ($LINK) ? 1 : 0;
printf( "PC: %s, L.AC %d.%06o, MQ: %06o\n", addr($PC), $link, $AC, $MQ );
return (0);
}
sub cmd_dump {
my ( $start, $end ) = @_;
# No arguments, so dump everything but not empty locations
if ( !defined($start) ) {
dump_memory( 0, MAXADDR, 0 );
return (0);
}
# Dump a limited range
$end = $start if ( !defined($end) );
dump_memory( $start, $end, 1 );
return (0);
}
sub cmd_listbreak {
print("Breakpoints:\n");
foreach my $addr ( sort( keys(%Breakpoint) ) ) {
printf( " %06o\n", $addr );
}
return (0);
}