[comp.os.minix] booting minix from dos

martyl@rocksvax.UUCP (Marty Leisner) (10/08/87)

Here's a little ditty I put together based on build to allow booting
Minix from Ms-Dos.  I got tired of having to format a new floppy each time I
tried to build a boot disk (it seems this is important).  Since build has
caused me nothing but grief, I intend to boot from now on using an ms/dos 
disk.

It is a very hacked up implementation of build and treats the boot disk
as a virtual disk in high ram.  Not much attempt was made to optimize --
on a hard disk the load/boot takes maybe 10-20 seconds tops.

To autoboot off an ms-dos disk use an autoexec.bat file which looks like:
	loadram	kernel.out mm.out fs.out init.out
where all the *.out files are on your floppy.

The following implementation is based on Aztec C and some of the changes I
made to Minix to get it to boot. Off the top of  my head, the changes were:
	1) Aztec data area starts at address 2, not 0 so all the build 
	   data segment addresses had to be incremented by 2 (this is all
           controlled with an Aztec preprocessor defintion).
	2) The kernel main has to dynamically relocate itself instead of 
	   using a hard coded physical base address.   Some space is lost,
	   but on machines with greater than >512 k ram, this really is no 
	   problem.

	   I use this function to get the base_click in kernel/main/main()

		get_current_cs	proc near
			mov ax,cs
			ret

	3) There are a number of Aztec supplied functions here which
	   should be easy to port to other compilers.  Among these are:
		movblock(src offset, src click, dst offset, dst click, num_bytes)
		peekw, peekb, pokeb, pokew -- implementations to peek/poke
			words into memory, using click:offset pointers

		sysint(int #, in regs , out regs) -- interface to interrupts
		farcall(long ptr, in regs, out regs) -- executes a far
			call to specified address

	4) I pass in Bx to the kernel the IBM scan code for =.  You should
	  pass what's appropriate for your system.
	5)  My kernel gets the ds value of cs:4 instead of expecting
	    passed in.  This is since I had problems with Aztec with fsck.
	    Since the kernel ds value is know, it should be relatively
            easy to patch it in if necessary.
	6) It works fine on Dos 3.2.  Don't know about other systems.

Enhancements I'm thinking off are:
	1) loading the root file system into ram off an msdos file (a big file).
	Ideas on how to do it?
	2) How to get back to Ms/Dos when we exit Minix (is this possible -- 
	since low ram and the command interpreter are untouched, are there 
	any other problems -- i.e. hardware reinitialization?

Feel free to feed back any questions/comments/criticisms.


marty leisner
ARPA:  leisner.henr@xerox.com
UUCP:  martyl@rocksvax.uucp

-------------------------cut here----------------------------------
/* Build becomes loadram to load a.out files into ram image
 * and boot Minix.
 * Assumes kernel get DS off codesegment rather than
 * a constant passed in a register
 * Also takes into account misc. aztec C incompatabilities with
 * build.
 */

/* Based on build, Marty Leisner, 10/87
 * Uses Aztec C compiler with libraries.
 */
/* This program takes the previously compiled and linked pieces of the
 * operating system, and puts them together to build a boot image in ram.
 * The files are read and put on in ram in this order:
 *
 *      kernel:         the operating system kernel
 *      mm:             the memory manager
 *      fs:             the file system
 *      init:           the system initializer
 * padded out to a multiple of 16 bytes, and then concatenated into a
 * single file beginning 512 bytes into the file.  The first byte of sector 1
 * contains executable code for the kernel.  There is no header present.
 *
 * After the boot image has been built, build goes back and makes several
 * patches to the image file or diskette:
 *
 *
 *	2. Build writes a table into the first 8 words of the kernel's
 *	   data space.  It has 4 entries, the cs and ds values for each
 *	   program.  The kernel needs this information to run mm, fs, and
 *	   init.  Build also writes the kernel's DS value into address 4
 *	   of the kernel's TEXT segment, so the kernel can set itself up.
 *
 *      3. The origin and size of the init program are patched into bytes 4-9
 *         of the file system data space. The file system needs this
 *         information, and expects to find it here.
 *
 * Loadram is called by:
 *
 *      build kernel mm fs init 
 *
 * to get the resulting image into RAM.
 * It presents an option to actually execute the boot.
 */

/* Patched in so build knows magic numbers begin at word 2 of data segment, not word 0 
 * -- ml 8/87
 */

#define AZTEC	1

#ifdef AZTEC
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>

/* put stack below heap  to save space for minix */
unsigned _STKSIZ = 0x100;
unsigned _STKLOW = 1;
unsigned _STKRED = 50;
unsigned _HEAPSIZ = 0x100;
#endif

long PROG_ORG;		/* capitalized to be capatible with the rest of the
					     * code -- actually start of virtual disk
						 */
#define PROGRAMS     4          /* kernel + mm + fs + init  = 4 */
#define DS_OFFSET 4L            /* position of DS written in kernel text seg */
#define SECTOR_SIZE 512         /* size of buf */
#define READ_UNIT 512           /* how big a chunk to read in */
#define KERNEL_D_MAGIC 0x526F   /* identifies kernel data space */
#define FS_D_MAGIC 0xDADA	/* identifies fs data space */
#define CLICK_SHIFT 4
#define KERN 0
#define MM   1
#define FS   2
#define INIT 3

#define TEST_CARRY(x)		(x & 0x1)
typedef struct {
	unsigned AX;
	unsigned BX;
	unsigned CX;
	unsigned DX;
	int SI;
    int DI;
    int DS;
    int ES;
} REGS;

#define MAX_BLOCKS			200		/* 100 k */
#define MAX_IMAGE_CLICKS 	MAX_BLOCKS*32
#define BLOCK_TO_CLICK(i)		(starting_click + (((i) - 1) *32))
#define BLOCK_SIZE	SECTOR_SIZE

extern unsigned _dsval;		/* aztec supplies this */

/* Information about the file header. */
#define HEADER1 32              /* short form header size */
#define HEADER2 48              /* long form header size */
#define SEP_POS 1               /* tells where sep I & D bit is */
#define HDR_LEN 2               /* tells where header length is */
#define TEXT_POS 0              /* where is text size in header */
#define DATA_POS 1              /* where is data size in header */
#define BSS_POS 2               /* where is bss size in header */
#define SEP_ID_BIT 0x20         /* bit that tells if file is separate I & D */

#define BREAD	O_RDONLY

int image;                      /* file descriptor used for output file */
int cur_sector = 1;                 /* which 512-byte sector to be written next */
int buf_bytes;                  /* # bytes in buf at present */
char buf[SECTOR_SIZE];          /* buffer for output file */
char zero[SECTOR_SIZE];         /* zeros, for writing bss segment */

long cum_size;                  /* Size of kernel+mm+fs+init */

struct sizes {
  unsigned text_size;           /* size in bytes */
  unsigned data_size;           /* size in bytes */
  unsigned bss_size;            /* size in bytes */
  int sep_id;                   /* 1 if separate, 0 if not */
} sizes[PROGRAMS];

char *name[] = {"\nkernel", "mm    ", "fs    ", "init  " };

main(argc, argv)
int argc;
char *argv[];
{
  void controlc();
  int i;

  if (argc != PROGRAMS+1) pexit("four file names expected. ", "");

  signal(SIGINT, controlc);
  PROG_ORG = (long) create_image() << 4;              /* create the output file */


  /* Copy the 5 programs to the output file or diskette. */
  for (i = 0; i < PROGRAMS; i++) copy2(i, argv[i+1]);
  printf("                                               -----     -----\n");
#if PCIX | AZTEC
  printf("Operating system size  %29ld     %5lx\n", cum_size, cum_size);
#else
  printf("Operating system size  %29D     %5X\n", cum_size, cum_size);
#endif

  patch2();
  patch3();

	printf("enter a key to boot Minix\n");
	getchar();
	boot_minix();

}

void controlc()
{
	printf("caught quit signal -- aborting\n");
	exit();
}

copy1(file_name)
char *file_name;
{
/* Copy the specified file to the output.  The file has no header.  All the
 * bytes are copied, until end-of-file is hit.
 */

  int fd, bytes_read;
  char inbuf[READ_UNIT];

  if ( (fd = open(file_name, BREAD)) < 0) pexit("can't open ",file_name);

  do {
        bytes_read = read(fd, &inbuf, READ_UNIT);
        if (bytes_read < 0) pexit("read error on file ", file_name);
        if (bytes_read > 0) wr_out(&inbuf, bytes_read);
  } while (bytes_read > 0);
  flush();
  close(fd);
}


copy2(num, file_name)
int num;                        /* which program is this (0 - 4) */
char *file_name;                /* file to open */
{
/* Open and read a file, copying it to output.  First read the header,
 * to get the text, data, and bss sizes.  Also see if it is separate I & D.
 * write the text, data, and bss to output.  The sum of these three pieces
 * must be padded upwards to a multiple of 16, if need be.  The individual
 * pieces need not be multiples of 16 bytes, except for the text size when
 * separate I & D is in use.  The total size must be less than 64K, even
 * when separate I & D space is used.
 */

  int fd, sepid, bytes_read, count;
  unsigned text_bytes, data_bytes, bss_bytes, tot_bytes, rest, filler;
  unsigned left_to_read;
  char inbuf[READ_UNIT];
  
  if ( (fd = open(file_name, BREAD)) < 0) pexit("can't open ", file_name);

  /* Read the header to see how big the segments are. */
  read_header(fd, &sepid, &text_bytes, &data_bytes, &bss_bytes, file_name);

  /* Pad the total size to a 16-byte multiple, if needed. */
  if (sepid && ((text_bytes % 16) != 0) ) {
        pexit("separate I & D but text size not multiple of 16 bytes.  File: ", 
                                                                file_name);
  }
  tot_bytes = text_bytes + data_bytes + bss_bytes;
  rest = tot_bytes % 16;
  filler = (rest > 0 ? 16 - rest : 0);
  bss_bytes += filler;
  tot_bytes += filler;
  cum_size += tot_bytes;

  /* Record the size information in the table. */
  sizes[num].text_size = text_bytes;
  sizes[num].data_size = data_bytes;
  sizes[num].bss_size  = bss_bytes;
  sizes[num].sep_id    = sepid;

        printf("%s  text=%5u  data=%5u  bss=%5u  tot=%5u  hex=%4x  %s\n",
                name[num], text_bytes, data_bytes, bss_bytes, tot_bytes,
                tot_bytes, (sizes[num].sep_id ? "Separate I & D" : ""));


  /* Read in the text and data segments, and copy them to output. */
  left_to_read = text_bytes + data_bytes;
  

  while (left_to_read > 0) {
        count = (left_to_read < READ_UNIT ? left_to_read : READ_UNIT);
        bytes_read = read(fd, &inbuf, count);
        if (bytes_read < 0) pexit("read error on file ", file_name);
        if (bytes_read > 0) wr_out(&inbuf, bytes_read);
        left_to_read -= count;
  }


  
  /* Write the bss to output. */
  while (bss_bytes > 0) {
        count = (bss_bytes < SECTOR_SIZE ? bss_bytes : SECTOR_SIZE);
        wr_out(&zero, count);
        bss_bytes -= count;
  }
  close(fd);
}


read_header(fd, sepid, text_bytes, data_bytes, bss_bytes, file_name)
int fd, *sepid;
unsigned *text_bytes, *data_bytes, *bss_bytes;
char *file_name;
{
/* Read the header and check the magic number.  The standard Monix header 
 * consists of 8 longs, as follows:
 *      0: 0x04100301L (combined I & D space) or 0x04200301L (separate I & D)
 *      1: 0x00000020L (stripped file) or 0x00000030L (unstripped file)
 *      2: size of text segments in bytes
 *      3: size of initialized data segment in bytes
 *      4: size of bss in bytes
 *      5: 0x00000000L
 *      6: total memory allocated to program (text, data and stack, combined)
 *      7: 0x00000000L
 * The longs are represented low-order byte first and high-order byte last.
 * The first byte of the header is always 0x01, followed by 0x03.
 * The header is followed directly by the text and data segments, whose sizes
 * are given in the header.
 */

  long head[12];
  unsigned short hd[4];
  int n, header_len;

  /* Read first 8 bytes of header to get header length. */
  if ((n = read(fd,&hd, 8)) != 8) pexit("file header too short: ", file_name);
  header_len = hd[HDR_LEN];
  if (header_len != HEADER1 && header_len != HEADER2) 
        pexit("bad header length. File: ", file_name);

  /* Extract separate I & D bit. */
  *sepid = hd[SEP_POS] & SEP_ID_BIT;

  /* Read the rest of the header and extract the sizes. */
  if ((n = read(fd, head, header_len - 8)) != header_len - 8)
        pexit("header too short: ", file_name);

  *text_bytes = (unsigned) head[TEXT_POS];
  *data_bytes = (unsigned) head[DATA_POS];
  *bss_bytes  = (unsigned) head[BSS_POS];
}


	
wr_out(buffer, bytes)
char buffer[];
int bytes;
{
/* Write some bytes to the output file.  This procedure must avoid writes
 * that are not entire 512-byte blocks, because when this program runs on
 * MS-DOS, the only way it can write the raw diskette is by using the system
 * calls for raw block I/O.
 */

  int room, count, count1;
  register char *p, *q;

  /* Copy the data to the output buffer. */
  room = SECTOR_SIZE - buf_bytes;
  count = (bytes <= room ? bytes : room);
  count1 = count;
  p = &buf[buf_bytes];
  q = buffer;
  while (count--)  {
  	*p++ = *q++;
  }
  
  /* See if the buffer is full. */
  buf_bytes += count1;
  if (buf_bytes == SECTOR_SIZE) {
        /* Write the whole block to the disk. */
        write_block(cur_sector, &buf);
        clear_buf();
  }

  /* Is there any more data to copy. */
  if (count1 == bytes) return;
  bytes -= count1;
  buf_bytes = bytes;
  p = &buf;
  while (bytes--)  {
  	*p++ = *q++;
  }
}


flush()
{
  if (buf_bytes == 0) return;
  write_block(cur_sector, &buf);
  clear_buf();
}


clear_buf()
{
  register char *p;

  for (p = buf; p < &buf[SECTOR_SIZE]; p++) *p = 0;
  buf_bytes = 0;
  cur_sector++;
}


patch2()
{
/* This program now has information about the sizes of the kernel, mm, fs, and
 * init.  This information is patched into the kernel as follows. The first 8
 * words of the kernel data space are reserved for a table filled in by build.
 * The first 2 words are for kernel, then 2 words for mm, then 2 for fs, and
 * finally 2 for init.  The first word of each set is the text size in clicks;
 * the second is the data+bss size in clicks.  If separate I & D is NOT in
 * use, the text size is 0, i.e., the whole thing is data.
 *
 * In addition, the DS value the kernel is to use is computed here, and loaded
 * at location 4 in the kernel's text space.  It must go in text space because
 * when the kernel starts up, only CS is correct.  It does not know DS, so it
 * can't load DS from data space, but it can load DS from text space.
 */

  int i, j;
  unsigned short t, d, b, text_clicks, data_clicks, ds;
  long data_offset;

  /* See if the magic number is where it should be in the kernel. */
  data_offset = 512L + (long)sizes[KERN].text_size;    /* start of kernel data */
#ifdef AZTEC
	data_offset += 2L;	/* offset for Aztec linker	*/
#endif
  i = (get_byte(data_offset+1L) << 8) + get_byte(data_offset);
  if (i != KERNEL_D_MAGIC)  {
	pexit("kernel data space: no magic #","");
  }
  
  for (i = 0; i < PROGRAMS; i++) {
        t = sizes[i].text_size;
        d = sizes[i].data_size;
        b = sizes[i].bss_size;
        if (sizes[i].sep_id) {
                text_clicks = t >> CLICK_SHIFT;
                data_clicks = (d + b) >> CLICK_SHIFT;
        } else {
                text_clicks = 0;
                data_clicks = (t + d + b) >> CLICK_SHIFT;
        }
        put_word(data_offset + 4*i + 0L, text_clicks);
        put_word(data_offset + 4*i + 2L, data_clicks);
  }

  /* Now write the DS value into word 4 of the kernel text space. */
  if (sizes[KERN].sep_id == 0)
        ds = PROG_ORG >> CLICK_SHIFT;   /* combined I & D space */
  else
        ds = (PROG_ORG + sizes[KERN].text_size) >> CLICK_SHIFT; /* separate */
  put_word(512L + DS_OFFSET, ds);
}


patch3()
{
/* Write the origin and text and data sizes of the init program in FS's data
 * space.  The file system expects to find these 3 words there.
 */

  unsigned short init_text_size, init_data_size, init_buf[SECTOR_SIZE/2], i;
  unsigned short w0, w1, w2;
  int b0, b1, b2, b3, b4, b5, mag;
  long init_org, fs_org, fbase, mm_data;

  init_org  = PROG_ORG;
  init_org += sizes[KERN].text_size+sizes[KERN].data_size+sizes[KERN].bss_size;
  mm_data = init_org - PROG_ORG +512L;	/* offset of mm in file */
  mm_data += (long) sizes[MM].text_size;
  init_org += sizes[MM].text_size + sizes[MM].data_size + sizes[MM].bss_size;
  fs_org = init_org - PROG_ORG + 512L;   /* offset of fs-text into file */
  fs_org +=  (long) sizes[FS].text_size;
  init_org += sizes[FS].text_size + sizes[FS].data_size + sizes[FS].bss_size;
  init_text_size = sizes[INIT].text_size;
  init_data_size = sizes[INIT].data_size + sizes[INIT].bss_size;
  init_org  = init_org >> CLICK_SHIFT;  /* convert to clicks */
  if (sizes[INIT].sep_id == 0) {
        init_data_size += init_text_size;
        init_text_size = 0;
  }
  init_text_size = init_text_size >> CLICK_SHIFT;
  init_data_size = init_data_size >> CLICK_SHIFT;

  w0 = (unsigned short) init_org;
  w1 = init_text_size;
  w2 = init_data_size;
  b0 =  w0 & 0377;
  b1 = (w0 >> 8) & 0377;
  b2 = w1 & 0377;
  b3 = (w1 >> 8) & 0377;
  b4 = w2 & 0377;
  b5 = (w2 >> 8) & 0377;

  /* Check for appropriate magic numbers. */
  fbase = fs_org;
#ifdef AZTEC
	mm_data += 2L;
	fbase += 2L;
#endif
  mag = (get_byte(mm_data+1L) << 8) + get_byte(mm_data+0L);
  if (mag != FS_D_MAGIC) pexit("mm data space: no magic #","");
  mag = (get_byte(fbase+1L) << 8) + get_byte(fbase+0L);
  if (mag != FS_D_MAGIC) pexit("fs data space: no magic #","");

#ifdef AZTEC
	fbase -= 2L;	/* instead of changing the following numbers, just change the
			 * offset.
			 * fs/main.c has to also be changed to be compatible.
			 */
#endif

  put_byte(fbase+4L, b0);
  put_byte(fbase+5L, b1);
  put_byte(fbase+6L, b2);
  put_byte(fbase+7L, b3);
  put_byte(fbase+8L ,b4);
  put_byte(fbase+9L, b5);
}





pexit(s1, s2)
char *s1, *s2;
{
  printf("Loadram: %s%s\n", s1, s2);
  exit(1);
}


/******** virtual disk routines     *************/

unsigned starting_click;	/* click where image starts */



/* these routines will use block #1 (start of kernel) as the first logical 
 * block in the ram disk
 */


read_block (blocknr,user)
int blocknr;
char user[];
{
	/* read a virtual block from disk	*/
	if(blocknr < 1 || blocknr > MAX_BLOCKS)
		pexit("invalid block requested", __FUNC__);

	movblock(0, BLOCK_TO_CLICK(blocknr), user, _dsval, BLOCK_SIZE);
}



write_block (blocknr,user)
int blocknr;
char user[];
{
	/* write a block to virtual disk */

	if(blocknr < 1 || blocknr > MAX_BLOCKS)
		pexit("invalid block number requested", __FUNC__);

	movblock(user, _dsval, 0, BLOCK_TO_CLICK(blocknr), BLOCK_SIZE);
}




create_image ()
{
	starting_click = malloc_dos_memory(MAX_IMAGE_CLICKS);	/* should be enough space */
	printf("starting click = %x\n", starting_click);
	return starting_click;
}

/* modifed to just poke into virtual disk */
put_byte(offset, byte_value)
long offset;
unsigned char byte_value;
{
	unsigned segment, index;

	index = offset % SECTOR_SIZE;
	segment = BLOCK_TO_CLICK(offset / SECTOR_SIZE);
	pokeb(index, segment, byte_value);

}

put_word(offset, word_value)
long offset;
unsigned word_value;
{
	unsigned segment, index;

	index = offset % SECTOR_SIZE;
	segment = BLOCK_TO_CLICK(offset / SECTOR_SIZE);
	pokew(index, segment, word_value);
}

get_byte(offset)
long offset;
{
	unsigned segment, index;

	index = offset % SECTOR_SIZE;
	segment = BLOCK_TO_CLICK(offset/SECTOR_SIZE);
	return peekb(index, segment);
}

boot_minix()
{
	/* image has been loaded in ram at PROG_ORG:0 */
	REGS regs;

	regs.BX =  13;	/* ibm "=" scan code */

	farcall(0, starting_click, &regs, &regs);
}


/* alloc memory from DOS 
 * Returns segment address if it works.
 * Returns 0 if it fails.
 */
int malloc_dos_memory(num_clicks)
unsigned num_clicks;
{
	REGS regs;
	int flags;
	
	regs.AX = 0x4800;
	regs.BX = num_clicks;
	flags = sysint(0x21, 	&regs, &regs);
	if(TEST_CARRY(flags)) {
		printf("%s failed, AX = %x\n", __FUNC__, regs.AX);
		printf("largest available block = %x\n", regs.BX);
		return 0;
	}
	else return regs.AX;
		
	
}

-- 
marty leisner
xerox corp
leisner.henr@xerox.com
martyl@rocksvax.uucp

A-PIRARD%BLIULG11.BITNET@wiscvm.wisc.edu (Andre PIRARD) (10/13/87)

On this subject, here are two messages I posted to the INFO-IBMPC list.
Due to the tricky problem of resetting the hardware correctly before booting,
I feel this is hard to achieve from within any running system. Even more to
come back to it. Hence my suggestion for a boot disk used to choose another.
Of course, my suggestion to put it on a diskette depends heavily on how often
one switches systems. My "special program" can of course be used in one
hard disk partition of its own.
Because Minix people probably often do, there must be interest on this list.
I am not watching the list closely, more for a friend.
So, if anyone undertakes anything that way, I'd be glad to receive
a personal notice.   Thanks.

Date:         Wed, 19 Aug 87 13:37:44 ULG
From:         Andre PIRARD <A-PIRARD%BLIULG12.BITNET@wiscvm.wisc.edu>
Subject: Booting Another Partition from Within DOS

When MSDOS has booted from the active partition, is there a way (program)
to boot an inactive partition without FDISK reassignment then reset.
It seems tricky to perform the necessary resets (e. g. interrupt vectors)
and load the boot record without getting into very machine-dependent code.
But I might have missed something...  Thanks.

Date:         Thu, 03 Sep 87 11:18:47 ULG
From:         Andre PIRARD <A-PIRARD%BLIULG12.BITNET@wiscvm.wisc.edu>
Subject:      Booting Another Partition from Within DOS

>When MSDOS has booted from the active partition, is there a way (program)
>to boot an inactive partition without FDISK reassignment then reset.
>It seems tricky to perform the necessary resets (e. g. interrupt vectors)
>and load the boot record without getting into very machine-dependent code.
>But I might have missed something...  Thanks.

Well, I did.  In fact, there is no need to boot DOS. If performing the
resets is difficult, let's simply go through the BIOS reset itself!

The trick is to format a "special" diskette that contains a dummy
IBMBIO.COM (or whatever name) that, when loaded, determines the available
hard disks, reads their partition records and checks their validity (55AA
tag). For each valid partition record, the partition table is examined for
valid entries (both active or inactive) and the boot record they point to
is again examined for the validity tag 55AA.

This gives a list of bootable partitions that can be displayed on the
screen with a prompt for the user to choose one (no national keyboard key
used, just arrows and return). When done, the corresponding boot record is
loaded at 0:7C00 and given control.

That's all there is to it.

When there is need to boot a partition other than the active one, the spe-
cial diskette is inserted in drive A and the machine is reset with instant
access to whatever system available Unix, CP/M or several DOS versions.

But I have no time to program this. So I suggest the idea for an addition
of a very special and funny program to the IBMPC library.

sbanner1@uvicctr.UUCP (S. John Banner) (10/15/87)

  I have a program that I got from one of the servers on Bitnet, that
replaces the boot track of the hard-drive with a program that reads the
partition table, and then prompts you for which you want to boot from
(actually it finds the first DOS bootable, and Xenix bootable, but I am
sure that someone who knows about how the table is setup could modify it
appropriately).  It is written in assembler, and if people are interested,
when I get my harddrive working again (it died last week), I can post it.

                      S. John Banner

...!uw-beaver!uvicctr!sol!sbanner1
...!ubc-vision!uvicctr!sol!sbanner1
ccsjb@uvvm
sbanner1@sol.UVIC.CDN

pcm@ogcvax.UUCP (Phil Miller) (10/20/87)

I should think that there would be considerable interest in a program
which would allow you to boot your choice of operating systems.  By all 
means, please post it (once your machine is back on its, er, feet).

Phil Miller
Oregon Graduate Center
pcm@ogcvax