Programming direct OS/2 Disk I/O

From EDM2
Jump to: navigation, search

By Roger Orr

Introduction

Those who remember the early days of OS/2 and the initial descriptions of the DOS compatibility box will recall the discussions about what was NOT going to be supported by OS/2, in particular the ability to perform physical writes to the disk.

There were good reasons for this - under OS/2 multiple processes can be accessing the disk simultaneously but DOS programs which access the disk directly assume it won't be altered while they are active.

This item had two sides to it - the good news was that a number of virus programs wouldn't work in the DOS box; but the bad news was that file unerase programs like Norton unerase would not work either. This was fairly bad news - many people found themselves occasionally booting DOS solely to run an unerase program after an 'accident'.

However it seems following the discussion about being unable to write directly to the disk many people were left with the overall impression that "under OS/2 you cannot physically access the disk".

This is NOT true - OS/2 application programs can read and, with some restrictions, write to the disk bypassing the OS/2 file system. Now of course there are not that many times when you are going to want to do this, but there are a few. I felt it would be a good idea to discuss the overall concepts since although it is mostly documented the information is split up over several sections and some of the interrelations are not spelled out.

Finally I provide a simple example program for some of the functions discussed; which reads and writes an entire floppy disk to/from a disk file. This can be used, for example, to create backup copies of installation diskettes.

Overview of disk structure

The best place to begin is with a brief discussion of the way PC disks are structured for those who may not already know.

The standard PC supports two types of disk - fixed (or 'hard') disks and diskettes (or 'floppy disks').

A typical hard disk comprises several 'platters' which rotate together and each has its own magnetic heads to read and write the data.

A floppy disk comprises only one platter, and the diskette drive uses either one or two heads (depending on whether it is single or double-sided).

In all cases the head or heads are moved together in parallel across the surface(s) of the disk, and the head number therefore indicates which surface of which platter is being used.

Each platter is divided into concentric circular rings of data, which are known as the tracks. For any given position of the heads each head is able to read or write one such track. The tracks which can be accessed without moving the heads form a cylindrical shape since they are stacked vertically above each other, and so the collection of tracks is referred to as a cylinder. The track is identified by specifying the cylinder and the head. [Sometimes the words track and cylinder are used interchangeably.]

Finally each track is split up into a number of sectors each represented by an arc of a circle.

Each portion of the disk is therefore uniquely identified by the cylinder, head and sector values - often abbreviated to its "CHS value". Note that cylinder and head numbers start with zero, but sector numbers start with one.

For a diskette the disk begins with a sector (with CHS value 0,0,1) known as the boot record which contains the startup code that is loaded and executed when you boot the system from a floppy; and also describes the rest of the disk.

The description of the disk in the boot sector is known as the Bios Parameter Block and it details a number of things; including the number of bytes per sector, size of the root directory and the number of heads.

Floppy disks are always formatted using the FAT format.

Fixed disks are more complicated. They are comprised of one or more disk partitions each one of which is a logical unit; so that for example you may have a 150Mbyte drive formatted as three logical drives referred to as drives C:, D: and E:.

How is this achieved?

The first sector on a fixed disk contains a 'master boot record', which contains the bootup code for the system when it is started from the hard disk, and a table called the partition table describing the way the disk is divided into logical units.

The partition table in the master boot record describes the primary partition - the C: drive - and an optional extended partition containing additional logical drives. These additional logical drives are chained together by further partition tables situated before each drive on the disk.

Under older versions of DOS and OS/2 there were restrictions on the size of a logical disk, and although these restrictions are now removed it is still often useful to split one large disk into several logical drives to facilitate disk maintenance and backup. For example drive C: may contain the operating system and compiler; drive D: work files, and drive E: utility programs.

Each logical disk may be formatted either as a FAT or an HPFS partition if it is to be used by OS/2, or partitions can also be reserved for other purposes, e.g. Xenix, and formatted accordingly.

Like the floppy disk the first sector in this logical disk contains a boot record - but note that it is a boot record for the logical drive rather than for the physical drive so values given in the Bios Parameter Block are relative to the partition in which the logical drive is contained rather than relative to the actual start of the disk. To make things more manageable for the device driver which issues physical I/O requests, all extended volumes must start and end on a cylinder boundary.

Disk I/O function - method 1

The simplest method of reading and writing a disk under OS/2 is to use the standard file systems API - DosOpen, DosRead, DosWrite and DosClose. This treats the disk like one big file, and is a very intuitive method.

The file name used on the DosOpen call is that of the drive, for example "C:", and the open mode must include the 'OPEN_FLAGS_DASD' flag to inform the system that you are opening the entire disk and didn't just make a mistake (!).

For example:

usMode = OPEN_FLAGS_DASD |
         OPEN_ACCESS_READWRITE |
         OPEN_SHARE_DENYREADWRITE;
DosOpen( "C:", &hdisk, &usAction, 0L,
         FILE_NORMAL, FILE_OPEN,
         usMode, 0L );

This opens the C: drive for exclusive access, and at this point you can read the drive using DosRead with the 'file' handle returned in hdisk.

The access rights involved are those for the disk-as-a-file and are treated separately from those applying to any open files on the disk. This means that you can open the disk for 'exclusive access' mode even if files are open on the disk; but you can not open the disk if another process has previously opened the same disk.

The data accessed by DosRead starts with the boot sector and takes in the entire logical drive. Note that this method of accessing the drive does not include the partition table itself.

Many of the standard file system functions work on the disk, in the same way as they would on a file. For example DosChgFilePtr will move the logical read head around in the disk.

In order to actually WRITE to the disk you need to do a bit more work - the file handle must be used to 'lock' the drive. For this to work no other files can be open on the drive; which means that the disk cannot be written to if it is in use. The access mode of the files which are open is irrelevant - it is enough that they are open.

The drive is locked by a call to DosDevIOCtl using the file handle returned from the DosOpen, with category 8 (IOCTL_DISK) and function 0 (DSK_LOCKDRIVE). See the 'squirt' program below for an example.

This method has the virtue of simplicity - the complications of actual disk geometry such as heads, sectors, etc. are hidden and only the data itself is visible. In addition the data starts with the boot sector for both floppy and hard disks and so the difference between their layouts is hidden.

Disk I/O function - method 2

The other way to access the disk is using DosDevIOCtl functions - a range of these is available at a slightly more disk specific level to do such things as read, write, format or verify on a track and sector basis.

These functions work on the entire logical disk - for example C:. Knowledge of the disk's geometry is used to allow physical manipulation of the disk which includes permitting access to the extended partition table for the logical drive.

The DosDevIOCtl takes a file handle which is obtained as for the first method described above.

For example:

TRACKLAYOUT track;              /* See bsedev.h */
BYTE buff[ 512 ];

/* set up track info to read boot sector */
track.bCommand = 1;             /* track layout starts with 1 */
track.usFirstSector = 0;        /* zeroth item in layout table */
track.cSectors = 1;
track.TrackTable[0].usSectorSize = 512;
track.usHead = 0;
track.usCylinder = 0;
track.TrackTable[0].usSectorNumber = 1;

rc = DosDevIOCtl( buff, &track,
                  DSK_READTRACK, IOCTL_DISK, hdisk);

This code fragment reads the boot sector for a floppy disk into the buffer at buff. Note that for a hard disk it would read the partition table not the boot sector.

The disk can be locked as described under method one. Unlike the above case both reading and writing are allowed without having to lock the disk but locking the disk helps you sleep better at night - there is no other way of guaranteeing that another process under OS/2 won't try to read or write to the disk while you are updating it.

A good simple virus protection hint would be to start a program early on in the boot process which obtains an exclusive access file handle to all your physical disks and prevents rogue programs from clobbering your disk!

Disk I/O function - method 3

The final method - for a hard disk only - allows access to the underlying physical disk which includes the partition table, all the logical drives, and any other partitions or unpartitioned space.

The DosPhysicalDisk API is used to obtain a file handle which can then be used by DosDevIOCtl to perform a set of functions which roughly mirror those available using DosDevIOCtl on a logical disk.

For example:

HFILE hdisk;
static CHAR szDrive[] = "1:";
rc = DosPhysicalDisk( INFO_GETIOCTLHANDLE,
             (PBYTE) &hdisk, sizeof( hdisk ),
             szDrive, sizeof( szDrive ) );

This obtains a handle to the first partitionable (=hard) disk on the system. The drive "2:" can be used for the second such disk, if it exists. (The INFO_COUNT_PARTITIONABLE_DISKS function can be used with this API to obtain a count of how many physical disks there are).

Only one handle at a time can be granted by DosPhysicalDisk for each physical disk. As for logical disk handles there is a DosDevIOCtl command to lock the disk; which succeeds if no other files or disk handles are open.

DosPhysicalDisk is mostly used by programs which are changing the way the disk is partitioned, such as FDISKPM. If you are going to write to the disk using this interface make very sure you know what you are doing or you may make all the data on your disk inaccessible...

I have used this interface when installing OS/2 to copy the disk partitioning information from one machine to another machine (with an identical disk!) to aid automatic installation. If you try something similar yourself, note that once you re-write the partition table chain on disk - you must REBOOT!

Example program

The example program is called 'squirt' and allows you to read and write the floppy drive A: to a file. This means you can make a disk 'image' of a floppy disk on the hard disk, and re-create the floppy disk at will. The image includes the boot sector and so even OS/2 or DOS boot disks can be saved. Since this is an example program some of the error checking which might be present in a commercial application is missing. For example there are no prompts to confirm before overwriting the floppy disk, and no checks that the file being restored to disk is the same size as the disk being restored onto.

For example:

squirt e:\diskimge\disk1.img

Would copy the contents of the diskette in drive A: to the file specified, overwriting it if necessary.

I believe the code should be clear once the rest of the article has been read, so I won't go into detail about how it works.

The crux of the program is the first DosOpen, which obtains a file handle for the A: drive.

Source for SQUIRT

#define         INCL_BASE
#define         INCL_DOSDEVIOCTL  /* may or may not be part of INCL_BASE! */

#include        <os2.h>

#include        <stdio.h>
#include        <string.h>
#include        <stdlib.h>

/*****************************************************************************/
/* init: initialise the file handles                                         */
/*****************************************************************************/

USHORT init( char *disk, char *file, HFILE *phdisk, HFILE *phfile,
             BOOL reverse )
  {
  BYTE cmd = 0;
  USHORT usAction;
  USHORT rc;

  /* Open the disk as a file so we can read/write it directly */
  rc = DosOpen( disk, phdisk, &usAction, 0L, FILE_NORMAL,
               FILE_OPEN,
       OPEN_FLAGS_DASD | OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE, 0L );

  if ( rc != 0 )
     {
     fprintf( stderr, "Unable to open disk %s - error %u\n", disk, rc );
     return rc;
     }

  /* Lock the drive to prevent problems with other users */
  rc = DosDevIOCtl( NULL, &cmd, DSK_LOCKDRIVE, IOCTL_DISK, *phdisk );
  if ( rc != 0 )
     {
     fprintf( stderr, "Unable to lock drive %s\n", disk );
     return rc;
     }

  rc = DosOpen( file, phfile, &usAction, 0L, FILE_NORMAL,
               FILE_OPEN | (reverse ? 0 : FILE_CREATE ),
               OPEN_SHARE_DENYREADWRITE |
                  ( reverse ? OPEN_ACCESS_READONLY : OPEN_ACCESS_WRITEONLY ),
               0L );

  if ( rc != 0 )
     {
     fprintf( stderr, "Unable to open file %s - error %u\n", file, rc );
     }

  return 0;
  }

/*****************************************************************************/
/* docopy: perform a block file copy from 'infile' to 'outfile'              */
/*****************************************************************************/

USHORT docopy( HFILE infile, HFILE outfile )
  {
  USHORT rc;
  USHORT lenread;
  USHORT lenwritten;
  PSZ buffer;
  ULONG written = 0;
#define BUFFERSIZE 0x8000

  buffer = malloc( BUFFERSIZE );
  if ( buffer == NULL )
     {
     fprintf( stderr, "Insufficient memory for I/O buffer\n" );
     return ERROR_NOT_ENOUGH_MEMORY;
     }

  for (; ; )
     {
     rc = DosRead( infile, buffer, BUFFERSIZE, &lenread );

     if ( rc != 0 )
        {
        fprintf( stderr, "Error %u reading\n", rc );
        break;
        }
     if ( lenread == 0 )
        {
        printf( "Finished: %lu bytes transferred\n", written );
        break;
        }
     if ( ( rc = DosWrite( outfile, buffer, lenread, &lenwritten ) ) != 0 )
        {
        fprintf( stderr, "Error %u writing\n", rc );
        break;
        }
     if ( lenread != lenwritten )
        {
        fprintf( stderr, "Unexpected EOF on output\n" );
        rc = ERROR_DISK_FULL;
        break;
        }
     written += lenwritten;

     printf( "Transferred %lu bytes\r", written );
     fflush( stdout );
     }

  return rc;
  }
/*****************************************************************************/
/* M A I N   P R O G R A M                                                   */
/*****************************************************************************/

int main ( int argc, char **argv )
  {
  HFILE hsrc;                    /* file handle for source disk/file        */
  HFILE hdest;                   /* file hande for destination              */
  BOOL update = FALSE;           /* TRUE when updating the diskette         */
  USHORT rc;


  argc--;
  argv++;
  if ( argc == 2 )
     {
     if ( stricmp( *argv, "-u" ) == 0 )
        {
        argv++, argc--;
        update = TRUE;
        }
     }

  if ( argc != 1 )
     {
     printf( "Syntax: SQUIRT [-u] filename\n" );
     return 1;
     }

  if ( update )
     rc = init( "A:", *argv, &hdest, &hsrc, update );
  else
     rc = init( "A:", *argv, &hsrc, &hdest, update );

  if ( rc == 0 )
     {
     rc = docopy( hsrc, hdest );
     }

  return rc;
  }

Compilation

I used Microsoft C 6.00 installed for OS/2, and invoked the compiler as:

cl /W4 squirt.c

Compiling for OS/2 2.0

I have presented the code for 'squirt' which compiles under Microsoft C 6.00 for OS/2 1.x since I was unsure what version of OS/2 most people are now working with. I know a number of people are compiling for OS/2 2.0, and in order to change the code for OS/2 2.0 and IBM C Set/2 the following changes must be made:

  1. Change USHORT to ULONG throughout.
  2. Replace the DosDevIOCtl statement with:
DosDevIOCtl( *phdisk, IOCTL_DISK, DSK_LOCKDRIVE,
             &cmd, 1, NULL,
             NULL, 0, NULL );

I invoked the compiler as:

icc squirt2.c

Conclusion

OS/2 does allow you to perform physical I/O on disks in an application. It has incorporated a simple method to prevent 'rogue' programs from doing too much damage, but which does allow direct access when required.

Although there are few occasions when this functionality is required it is worth being aware of the possibilities.

Additionally the increased understanding of the mechanism employed by OS/2 enables more effective use to be made of its disk and file capabilities.

I have a slight concern over security, however. There are not as yet many (any ?) reported OS/2 viruses actually 'in the wild' despite the high profile given to DOS viruses in the media, but there do appear to be a few potential holes in the way OS/2 allows direct disk access which might be taken advantage of by future virus programs.

Various methods to block these accesses can be devised, but it would be more reliable if OS/2 itself could be configured to prevent such access except where explicitly authorised. In addition, there should be a method to mark an entire logical disk as 'read-only' to prevent accidental erasure.

Roger Orr - 12-Apr-92