[net.sources] Loadable Device Drivers for 4.2

hasiuk@spp2.UUCP (Lee Hasiuk) (02/03/86)

[This is an updated version of my earlier posting.  The major changes include:
1) Support for Sun Release 2.X, instead of 1.X
2) Support for block (bdevsw) devices
3) Use of -N flag to /bin/ld - uses less real kernel memory
4) Fixed bug in print-out of driver entry points]

This package includes the source distribution for loadable device drivers on the
Sun Workstation.  The principles involved should be applicable to any 
Unix 4.2 system.  

The idea behind the code is to allow a (super)user to 
develop and debug a device driver without having to continuously go through
the link-the-kernel/reboot-the-system cycle.

The basic units are:

1) A device driver 'template' which is configured into the kernel once,
   after several device specific definitions have been completed.  This 
   is called the 'ld' device driver.

2) A program which can link and load a device driver into a kernel which
   has been configured with the above template.  This is the 'load' program.

The programs are specifically designed to run on a Sun Workstation running
Unix 4.2 Release 2.0 through 2.2 (and can be easily changed to run on previous
releases the mb_device structure must be changed).  It should also be
possible to change the ld device driver template to work with any other 4.2
based system (VAX mods anyone?).

The load program is designed to be compatible with most 4.2 systems, and 
represents the majority of the loadable device driver code.

Also included is a demo device driver which can be loaded into your Sun,
once the ld driver has been installed (the ld driver must be compiled with
DEMO defined, since no hardware will exist when the demo is run), and
instructions for installing everything.

Despite the apparent simplicity of it all, I would NOT RECOMMEND that
novice Unix users attempt to use loadable device drivers.  Bugs in your 
code can and will crash your system.

If used properly, loadable device drivers will save you tremendous amounts
of time during driver development.  Remember that when you are done debugging
your driver, you will still have to make changes which allow you to install it
permanently, as described in the Sun manuals.

Files in the distribution:

1) load.c: loads a device driver into an appropriately built system.
2) ld.c: device driver to support loadable device drivers
3) lddefs.h: include file for ld.c and load.c with device specific definitions
4) demo.c: simple loadable device driver example

To install on a Sun running release 2.X:

1) Run the bottom half of this message through sh to extract the files.
2) Move ld.c and lddefs.h to /sys/sundev.
3) Add something resembling the following to the cdevsw structure in
   /sys/sun/conf.c (consult Sun Device Driver manuals for help):

(For cdevsw entries)
#include "ld.h"
#if NLD > 0
int	ldopen(),ldclose(),ldread(),ldwrite(),ldioctl(),ldmmap();
#else
#define	ldopen		nodev
#define	ldclose		nodev
#define	ldread		nodev
#define	ldwrite		nodev
#define	ldioctl		nodev
#define ldmmap		nodev
#endif
    {
	ldopen,		ldclose,	ldread,		ldwrite,	/*36*/
	ldioctl,	nodev,		nulldev,	0,
	seltrue,	ldmmap,
    },

*** or ***

(For bdevsw entries)
#include "ld.h"
#if NLD > 0
int	ldopen(),ldstrategy(),ldread(),ldwrite(),ldioctl(),ldsize(),lddump();
#else
#define ldopen		nodev
#define	ldstrategy	nodev
#define	ldread		nodev
#define	ldwrite		nodev
#define	ldioctl		nodev
#define	lddump		nodev
#define	ldsize		0
#endif
    { ldopen,	nulldev,	ldstrategy,	lddump,		/*7*/
      ldsize,	0 },

4) Add the following line to /sys/conf/files.sun:

sundev/ld.c		optional ld device-driver

5) Add the following configuration line to you current kernel configuration
   file in /sys/conf:

device		ld0 at mb0 csr 0xe4000 priority 2

  Note that the address e4000 was arbitrarily picked to not collide with other
  Sun devices, and should, in general, be changed to the address of your device.
  This also applies to the 'priority 2'.

6) Edit the lddefs.h file to reflect your device.  The specific variables which
   you may want to change are:

   LD_SIZE if you think that your driver will take more than 32k.

   LD_DEV_SIZE to reflect the number of bytes which your device uses on the
   bus.

   LD_PROBE_OFFSET to reflect where your device gets poked at
   auto-configuration time.  You may also want to modify the ldprobe routine
   if your device needs special treatment at boot time.

   DEMO should be commented out if you aren't running the 'no hardware
   required demo'.  This basically tells auto-configuration that your
   device is there, even if it isn't.

7) Run /etc/config on your configuration file, cd to the configuration
   directory and run 'make depend' followed by 'make'.  (See the Sun reference
   manual on 'Building Sun Workstation Kernels with Config' for more info.)

8) Run /etc/mknod for the cdevsw or bdevsw position that step #3 used.
   If it was a character special device with major number #36, you would type:
cd /dev
/etc/mknod ld0 c 36 0
chmod 666 /dev/ld0

9) Move the new kernel to /vmunix and reboot your system.

10) Compile load.c:

cc -o load load.c

11) Compile and load your loadable driver.  For the demo driver you would:

cc -I/sys/h -I. -DKERNEL -DNDEMO=1 -c demo.c
<Become super user if you don't have write access to /dev/kmem>
load demo
<Ignore error message from /bin/ld about 'text base already given'; it
 is necessary in order to bypass a bug in Sun's 2.0/2.2 linker>

Try the demo driver:
cat /etc/hosts > /dev/ld0
cat /dev/ld0
<interrupt> to stop 

GOOD LUCK AND HAPPY DEVICE DRIVER MAKING!


Lee Hasiuk
{ucbvax, ihnp4, decvax}!trwrb!trwspp!spp2!hasiuk
(213)535-6772 

--------------------------------------------------------------------
CUT HERE
--------------------------------------------------------------------
#!/bin/sh
cat > load.c <<[EOF]
/* load.c, lzh, 1/31/86 */

/*
 * Link and load a loadable device driver into the running kernel.
 * The kernel must reside in the file defined by KERNEL, and the
 * running kernel must be accessible as the file defined by MEMORY.
 * The kernel must have been previously built with the loadable device 
 * template, which is customized to the device addresses and interrupt
 * priorities of the driver to be developed. 
 * 
 * This program is called with one argument, the prefix of the driver
 * which you have written (e.g. mt for the tape driver) and assumes that
 * the names for the read, write, etc. routines will be of the form
 * 'prefixroutine' (e.g. mtwrite, mtread, mtintr ...).
 * Before this program is run, a file of the form prefix.o (e.g. mt.o) 
 * must exist in the current directory.  This file contains the object code
 * for the driver, and would typically be generated by a cc command of the
 * following form (continuing the mt example):
 * cc -O -I/sys/h -DKERNEL -DNMT=1 -c mt.c
 *
 * The driver is linked against the symbol table of the kernel, so the driver
 * may take full advantage of all kernel services for device drivers.
 *
 */

#include <stdio.h>
#include <strings.h>
#include <fcntl.h>
#include <a.out.h>
#include <sys/wait.h>
#include "lddefs.h"

#define KERNEL "/vmunix"	    /* File which kernel resides in */
#define MEMORY "/dev/kmem"	    /* For access to running kernel */
#define USE_OMAGIC		    /* Use impure format executable */
#define NARG  ((char *)NULL)

struct nlist kernel_nl[4];
#define K_ld_magic	0
#define K_ld_driver	1
#define K_ld_dev	2
#define K_last		3

/* The symbols in the driver_nl structure must appear in the same order
   as the pointers to functions in the ld_dev structure in lddefs.h */
#define D_open		0
#define D_close		1
#define D_read		2
#define D_write		3
#define D_ioctl		4
#define D_mmap		5
#define D_intr		6
#define D_strategy	7
#define D_size		8
#define D_dump		9
#define D_last		10
#define N_ENTRY_POINTS  D_last
struct nlist driver_nl[N_ENTRY_POINTS + 1];

/*
 * Given a device prefix and a routine name, produce a .o file style symbol
 * name; e.g. entry_name("xy", "open") returns a pointer to a string which
 * contains "_xyopen".
 */
char *entry_name(prefix, routine)
char *prefix, *routine;
{
    char *malloc(), *name;

    if ((name = malloc((unsigned)(strlen(prefix)+strlen(routine)+2))) == NULL) 
	error(routine, "Can't get memory for entry point", 0, 1);
    (void)sprintf(name,"_%s%s", prefix, routine);
    return(name);
}

/*
 * Get symbols for the loadable device driver out of the kernel's namelist.
 * Open the kernel memory device, and check to see if the magic number is in
 * the correct location.  Return a file descriptor for the kernel memory device.
 */
get_kernel_namelist(ker, mem)
char *ker, *mem;
{
    int fd;
    unsigned long value;
    long lseek();
    struct nlist *n;

    /* Open kernel memory device */
    if ((fd = open(mem, O_RDWR, 0)) == -1) 
	error(mem, "open", 1, 1);

    /* Initialize the kernel namelist */
    kernel_nl[K_ld_magic].n_un.n_name = "_ld_magic";
    kernel_nl[K_ld_driver].n_un.n_name = "_ld_driver";
    kernel_nl[K_ld_dev].n_un.n_name = "_ld_dev";
    kernel_nl[K_last].n_un.n_name = "";
    nlist(ker, kernel_nl);
    
    /* Check the magic number in the currently running system */
    n = &kernel_nl[K_ld_magic];
    if (n->n_type == N_UNDF) 
	error(ker, "No loadable driver support in kernel", 0, 1);
    if (lseek(fd, (long)n->n_value, 0) == -1) 
	error(mem, "lseek", 1, 1);
    if (read(fd, (char *)&value, sizeof(value)) == -1) 
	error(mem, "read", 1, 1);
    if (value != LD_MAGIC) 
	error(mem, "No loadable driver support in running kernel", 0, 1);
    return(fd);
}

/*
 * Given a driver prefix, the name of the file containing the kernel and
 * the start address of the array in which the driver will run, link the
 * driver to resolve undefined symbols against the kernel, and remove the
 * relocation bits.  Returns the actual starting point where the driver
 * should be loaded, which must be the first byte of a page.
 */
link_driver(driver, ker, start)
char *driver, *ker;
unsigned long start;
{
    union wait status;
    char driver_obj[255], driver_start[255];
    int pagesize;

    /* Construct the name of the driver's object file */
    (void)strcpy(driver_obj, driver);
    (void)strcat(driver_obj, ".o");

    /* The starting point must be on a page boundary */
    pagesize = getpagesize();
    while ((start % pagesize) != 0)
	start++;

    /* Show user the link command which we will use */
    (void)sprintf(driver_start, "%x", start);
#ifdef USE_OMAGIC
    printf("ld -A %s -N -T %s -o %s %s\n", 
	   ker, driver_start, driver, driver_obj);
#else
    printf("ld -A %s -T %s -o %s %s\n", 
	   ker, driver_start, driver, driver_obj);
#endif

    /* Fork the link command */
    if (vfork() == 0) {
#ifdef USE_OMAGIC
	if (execl("/bin/ld", "ld", "-A", ker, "-N", "-T", driver_start,
		  "-o", driver, driver_obj, 0) == -1) {
#else
	if (execl("/bin/ld", "ld", "-A", ker, "-T", driver_start,
		  "-o", driver, driver_obj, 0) == -1) {
#endif
	    perror("/bin/ld");
	    _exit(1);
	}
    }

    /* Wait for the link to complete; ensure that exit status is zero */
    if (wait(&status) == -1) 
	error("link_driver", "wait", 1, 1);
    if (status.w_status != 0) 
	error(driver_obj, "Link failed, load aborted", 0, 1);

    /* Return the new starting address */
    return(start);
}

/*
 * Give a driver prefix, construct a list of known routine names for the
 * nlist command.  Resolve these names against the file containing the linked 
 * driver.  Insert each found entry point into the proper position
 * of the driver dispatch table, and display the kernel address of each one.
 */
get_driver_entry_points(name, table)
char *name;
unsigned long table[];
{
    int i;
    struct nlist *n;

    /* Build table for nlist of routine names */
    driver_nl[D_open].n_un.n_name = entry_name(name, "open");
    driver_nl[D_close].n_un.n_name = entry_name(name, "close");
    driver_nl[D_read].n_un.n_name = entry_name(name, "read");
    driver_nl[D_write].n_un.n_name = entry_name(name, "write");
    driver_nl[D_ioctl].n_un.n_name = entry_name(name, "ioctl");
    driver_nl[D_mmap].n_un.n_name = entry_name(name, "mmap");
    driver_nl[D_intr].n_un.n_name = entry_name(name, "intr");
    driver_nl[D_strategy].n_un.n_name = entry_name(name, "strategy");
    driver_nl[D_size].n_un.n_name = entry_name(name, "size");
    driver_nl[D_dump].n_un.n_name = entry_name(name, "dump");
    driver_nl[D_last].n_un.n_name = "";

    /* Resolve names against linked driver */
    nlist(name, driver_nl);

    /* Build the ld_dev structure and display the entry points */
    printf("Defined entry points:\n");
    for (i = 0; i < D_last; i++) {
	n = &driver_nl[i];

	/* Only include starting addresses for defined routines */
	if (n->n_type != N_UNDF) {
	    table[i] = n->n_value;
	    printf("%s: 0x%x\n", n->n_un.n_name, n->n_value);
	}
    }
}

/*
 * Given the name of the file containing the linked driver, a buffer to copy
 * the driver to, the starting address of the driver once it resides in the
 * kernel, and the available memory, allocate and read in
 * to memory the linked driver.  The driver is put into the buffer in the same 
 * exact layout that it will reside in the kernel, so that it may be simply
 * copied in with a 'write' command.  Return the actual amount of contiguous 
 * memory which the driver will occupy.
 */
read_driver(name, buf, start, available)
char *name, *buf;
unsigned long start;
int available;
{
    int fd, size, i;
    struct exec ex;
    char *text, *data, *bss;
    long lseek();

    /* Open the file containing the linked driver */
    if ((fd = open(name, O_RDONLY, 0)) == -1) 
	error(name, "open", 1, 1);

    /* Read the driver's header (exec structure) */
    if (read(fd, (char *)&ex, sizeof(struct exec)) == -1) 
	error(name, "read", 1, 1);

    /* Determine where the text, intialized data, and uninit. data will be */
    text = buf;
    data = buf + ex.a_text;

    /* Initialized data starts on segment boundary if demand load type */
    if (ex.a_magic == ZMAGIC) {
	for (start = start + ex.a_text; start % SEGSIZ != 0; start++)
	    data++;
    }
    
    /* Uninitialized data starts immediately after initialized */
    bss = data + ex.a_data;

    printf("Driver uses %d bytes of memory\n", size = bss + ex.a_bss - text);
    if (size > available) 
	error("Insufficient memory in kernel buffer for driver", NARG, 0, 1);

    /* Seek to the beginning of the text (code) for the driver, and read in */
    if (lseek(fd,(long)N_TXTOFF(ex), 0) == -1) 
	error(name, "lseek", 1, 1);
    if ((read(fd, text, (int)ex.a_text) != ex.a_text) ||
	(read(fd, data, (int)ex.a_data) != ex.a_data)) 
	error(name, "read", 1, 1);

    /* Zero the uninitialized data segment */
    for (i = 0; i < ex.a_bss; i++) 
	*bss++ = 0;

    /* Return size of driver */
    return(size);
}

/* 
 * Insert data of given size at the given position in the given file descriptor.
 */
insert(fd, where, buf, count)
int fd, count;
unsigned long where;
char *buf;
{
    long lseek();

    if (lseek(fd,(long)where, 0) == -1) 
	error("insert", "lseek", 1, 1);
    if (write(fd, buf, count) == -1) 
	error("insert", "write", 1, 1);
}

/*
 * Print an error message and quit.
 */
error(s1, s2, perr, estat)
char *s1, *s2;
int perr, estat;
{
    if (s1 != NULL)
	fprintf(stderr, "%s: ", s1);
    if (s2 != NULL)
	fprintf(stderr, "%s: ", s2);
    if (perr != 0)
	perror("");
    else
	fprintf(stderr,"\n");
    exit(estat);
}

main(argc, argv)
int argc;
char *argv[];
{
    int mem_fd;
    int size, available, i;
    unsigned long start, buffer_start;
    char *driver_name;
    unsigned long entry_points[N_ENTRY_POINTS];
    char driver_mem[LD_SIZE];

    if (argc < 2) {
	printf("Usage: %s [driver prefix (e.g. mt)]\n", argv[0]);
	exit(1);
    }
    driver_name = argv[1];

    /* Check the kernel namelist for globals that we need; open memory file */
    mem_fd = get_kernel_namelist(KERNEL, MEMORY);

    /* Get starting address of array in which we load the driver in kernel*/
    buffer_start = kernel_nl[K_ld_driver].n_value;

    /* Link the driver against the nearest usable address */
    start = link_driver(driver_name, KERNEL, buffer_start);

    /* Clear the old driver dispatch table */
    for (i = 0; i < N_ENTRY_POINTS; i++)
	entry_points[i] = 0;
    insert(mem_fd, kernel_nl[K_ld_dev].n_value, 
	   (char *)&entry_points[0], sizeof(entry_points));

    /* Get the addresses of the read, write, ioctl, interrupt, etc. routines */
    get_driver_entry_points(driver_name, entry_points);

    /* Determine how much space we still have for the driver */
    available = sizeof(driver_mem) - (start - buffer_start);

    /* Read the driver into our buffer */
    size = read_driver(driver_name, driver_mem, start, available);
    
    /* Just in case the user crashes the system right away, sync the disks */
    sync();

    /* Write the driver into the kernel */
    insert(mem_fd, start, driver_mem, size);

    /* Write the driver routine dispatch table into the kernel */
    insert(mem_fd, kernel_nl[K_ld_dev].n_value, 
	   (char *)&entry_points[0], sizeof(entry_points));

    /* That's all folks! */
    printf("'%s' driver successfully loaded.\n", driver_name);
    exit(0);
}
[EOF]
cat > ld.c <<[EOF]
/* ld.c, lzh, 01/31/86 */

/*
 * Kernel support for loadable device drivers
 */
#include "ld.h"
#if NLD > 0

#include "../h/param.h"
#include "../h/buf.h"
#include "../h/errno.h"
#include "../sundev/mbvar.h"
#include "../sundev/lddefs.h"

int ldnone(), ldprobe(), ldintr();

int ld_magic = LD_MAGIC;

struct ld_dev ld_dev;

char ld_driver[LD_SIZE];

struct mb_device *ldinfo[NLD];

struct mb_driver lddriver = {
    ldprobe, 0, 0, 0, 0, ldintr, LD_DEV_SIZE, "ld", ldinfo, 0, 0, 0,
};

ldprobe(reg)
caddr_t reg;
{
#ifndef DEMO
    if (poke(reg + LD_PROBE_OFFSET, 0)) 
	return(0);
#endif
    return(LD_DEV_SIZE);
}

ldintr()
{
    if (ld_dev.ld_intr != 0)
	return((*ld_dev.ld_intr)());
    else
	return(0);
}

ldopen(dev, flags)
register dev_t dev;
register flags;
{
    if (ld_dev.ld_open != 0)
	return((*ld_dev.ld_open)(dev, flags));
    else
	ld_error("open");
}

ldclose(dev)
register dev_t dev;
{
    if (ld_dev.ld_close != 0)
	return((*ld_dev.ld_close)(dev));
    else
	ld_error("close");
}

ldread(dev, uio)
register dev_t dev;
register struct uio *uio;
{
    if (ld_dev.ld_read != 0)
	return((*ld_dev.ld_read)(dev, uio));
    else
	ld_error("read");
}

ldwrite(dev, uio)
register dev_t dev;
register struct uio *uio;
{
    if (ld_dev.ld_write != 0)
	return((*ld_dev.ld_write)(dev, uio));
    else
	ld_error("write");
}

ldioctl(dev, cmd, data, flag)
register dev_t dev;
register int cmd;
register caddr_t data;
register int flag;
{
    if (ld_dev.ld_ioctl != 0)
	return((*ld_dev.ld_ioctl)(dev, cmd, data, flag));
    else
	ld_error("ioctl");
}

ldmmap(dev, off, prot)
register dev_t dev;
register off_t off;
register int prot;
{
    if (ld_dev.ld_mmap != 0)
	return((*ld_dev.ld_mmap)(dev, off, prot));
    else
	ld_error("mmap");
}

ldstrategy(bp)
register struct buf *bp;
{
    if (ld_dev.ld_strategy != 0)
	return((*ld_dev.ld_strategy)(bp));
    else
	ld_error("strategy");
}

ldsize(dev)
register dev_t dev;
{
    if (ld_dev.ld_size != 0)
	return((*ld_dev.ld_size)(dev));
    else
	ld_error("size");
}

lddump(dev, addr, blkno, nblk)
register dev_t dev;
register caddr_t addr;
register daddr_t blkno, nblk;
{
    if (ld_dev.ld_dump != 0)
	return((*ld_dev.ld_dump)(dev, addr, blkno, nblk));
    else
	ld_error("dump");
}

static
ld_error(s)
char *s;
{
    printf("ld0: %s routine called, but not defined!\n", s);
}

#endif
[EOF]
cat > lddefs.h <<[EOF]
/* ldreg.h, lzh, 01/31/86 */

/*
 * Structures and definitions for the loadable device driver
 */

#define LD_MAGIC 	96401815	/* Magic number */
#define LD_SIZE 	32768		/* Size of text and data of driver */
#define LD_DEV_SIZE	16		/* Size of device */
#define LD_PROBE_OFFSET 0xe		/* Offset from start of dev to probe */
#define DEMO				/* Device is always present */

struct ld_dev
{
	int	(*ld_open)();
	int	(*ld_close)();
	int	(*ld_read)();
	int	(*ld_write)();
	int	(*ld_ioctl)();
	int	(*ld_mmap)();
	int	(*ld_intr)();
	int	(*ld_strategy)();
	int	(*ld_size)();
	int	(*ld_dump)();
};
[EOF]
cat > demo.c <<[EOF]
/* demo.c, lzh, 10/07/85 */

/*
 * LOADABLE Demo Device driver 
 *
 * This device will continuously read back the last 1024 bytes written to it.
 */

#if NDEMO > 0
#include "../h/param.h"
#include "../h/dir.h"
#include "../h/user.h"
#include "../h/systm.h"
#include "../h/buf.h"
#include "../h/ioctl.h"
#include "../h/uio.h"
#include "../h/kernel.h"
#include "../sun/psl.h"
#include "../sundev/mbvar.h"

#define DEMOUNIT(dev) (minor(dev))
int demostrategy();
int demo_is_open = 0;
char demobuf[1024];

extern struct mb_device *ldinfo[];
#define demoinfo ldinfo

struct buf rdemobuf[NDEMO];

demoopen(dev, flag)
dev_t dev;
int flag;
{
    if (demo_is_open)
	return(ENXIO);
    return(0);
}

democlose(dev)
dev_t dev;
{
    demo_is_open = 0;
}

demoread(dev, uio)
dev_t dev;
struct uio *uio;
{
    if (DEMOUNIT(dev) >= NDEMO)
	return(ENXIO);
    return(physio(demostrategy, &rdemobuf[DEMOUNIT(dev)], dev, B_READ,
		  minphys, uio));
}

demowrite(dev, uio)
dev_t dev;
struct uio *uio;
{
    if (DEMOUNIT(dev) >= NDEMO)
	return(ENXIO);
    return(physio(demostrategy, &rdemobuf[DEMOUNIT(dev)], dev, B_WRITE,
		  minphys, uio));
}

demostrategy(bp)
register struct buf *bp;
{
    register int i;
    char *cp = bp->b_un.b_addr;

    for (i = 0; i < bp->b_bcount; i++)
	if (bp->b_flags & B_READ)
	    *cp++ = demobuf[i%1024];
	else
	    demobuf[i%1024] = *cp++;
    iodone(bp);
}

demointr()
{
    return(0);
}
#endif
[EOF]
# END OF LOADABLE DEVICE DRIVER DISTRIBUTION