For an alternative version of sector level I/O get my D_DISK Library, a MUCH improved version of DISK I/O.
(I've created a summary of DOS specific C library functions listing the differences between three major C Compilers -- Borland, Microsoft and Watcom; DJGPP will be added later.)
This document currently only shows how to implement the very basics.
Over the next few chapters I will be presenting a comprehensive code base
for actually doing useful things. I will also explain DOS drive structures
and data formats and other technical details.
I HAVE TRIED VERY HARD TO KEEP THESE DOCUMENTS
CORRECT BUT
I CANNOT GIVE ANY GARAUNTEES THAT THERE ARE NOT ANY ERRORS |
After the sector size is known which interrupt to use to read the drive must be determined. Not all drives are supported by both BIOS and DOS calls. Only standard floppies and hard drives are supported by the BIOS.
But once the basic dimensions are known, and the interrupt chosen to
read the drive, just being able to arbitrarily read the drive is not much
use unless you can match sectors to data. To access data the dimensions
must be determined and the incremental algorithm must be performed when
reads extend across the boundaries.
typedef unsigned char BYTE;
typedef unsigned short UINT16;
typedef unsigned long UINT32;
Coding Note | Data sizes are often very important. Data structure packing is always extremely important and must be packed on byte boundaries for DOS. |
DOS treats a drive as only having a continuous number of sectors; a small percentage of the total number of sectors for it's system data and the rest of the sectors for file data. The file data sectors are sub-grouped into clusters.
The drive dimensions are used to know where to read (or write) and to perform incremental movement. System or file data can run over boundaries; any data block can start on the last sector of a track and extend to the first sector of the next track.
To access a sector you just supply the head, track and sector and a buffer of the appropriate size:
UINT16 cur_head;
/* current head */
UINT16 cur_track;
/* current track */
UINT16 cur_sector;
/* current sector */
BYTE *sec_buf;
/* sector buffer */
For the drive physical dimensions you need:
UINT16 max_head;
/* number of heads */
UINT16 max_track;
/* number of tracks per head */
UINT16 max_sector;
/* number of sectors per track */
UINT16 sec_size;
/* bytes per sector */
This physical access to a drive is done through the BIOS INT 13h.
UINT32 log_sector; /*
current logical sector */
BYTE *sec_buf;
/* sector buffer */
For the drive logical dimensions all you need are:
UINT32 num_sectors; /* total
number of sectors */
UINT16 sec_size;
/* bytes per sector */
This logical access to a drive is done through the DOS INTs 25h and 26h.
But because DOS originally chose UINT16 for the data type of the number of sectors, which is too small for the total number of sectors for drives greater than 32MB, you also need to know the drive size to handle the two different ways DOS reads drive sectors.
UINT32 drive_size; /* drive size in bytes */
This limits the drive size to 4GB.
UINT32 hidden_secs; /* number
of hidden sectors on */
/* harddrives - Partition info, first track */
UINT16 reserved_secs; /* number of reserved
sectors */
/* Boot sector(s) */
UINT16 secs_fat;
/* sectors per FAT */
UINT16 num_fats;
/* number of FATs */
UINT16 dir_sector;
/* start of root directory */
UINT16 data_sector; /* first
data sector */
UINT16 secs_cluster; /* sectors
per cluster */
UINT16 dir_sectors; /* number
of sectors of root */
UINT16 dir_entries; /* number
of directory entries */
UINT16 cluster_size; /* cluster
size in bytes */
UINT16 num_clusters; /* maximum
cluster no. */
These values must always be calculated. Floppies don't have hidden_secs; reserved_secs is usually one for the boot sector but can be more.
To put this visually:
hidden | reserved (boot) | FAT | FAT copy | root directory | data |
Before a drive sector can be read the sector size needs to be known. Assuming a sector size of 512 bytes will cover most drives, but not all. The type of interrupt used to read a drive sectors depends on the drive type; only standard floppies and hard drives can be read by the BIOS. The sector size and the drive type is determined at the same time.
The DOS function 440dh, code 60h, (get device parameters) gives almost everything needed to start reading a drive. The same information is contained in a drive's boot sector. But the sector size is needed to read the boot sector.
(Note: The get device parameters function is not always going to be supported depending on the drive type and DOS version. The DOS function 36h (get disk free space) can be used if get device parameters is not supported, and then the boot sector is read.)
The DOS function 440dh, code 60h uses a DEVICEPARAMS structure:
int getdevparams(int drive, struct DEVICEPARAMS *dp)
{
union REGS regs;
struct SREGS sregs;
regs.x.bx = drive;
/* 0 = default, 1 = A:, 2 = B:, etc. */
regs.x.ax = 0x440D;
/* function */
regs.x.cx = 0x0860;
/* sub-function */
regs.x.dx = FP_OFF(dp);
sregs.ds = FP_SEG(dp);
intdosx(®s,®s,&sregs);
if (regs.x.cflag)
return regs.x.ax;
return 0;
}
Coding Note | For 16-bit compilers the Large Memory model should be used;
if not, __far pointers should be used or
the FP_OFF/FP_SEG macros should be removed
and the data offset set as:
regs.x.dx = (unsigned)dp;
|
sec_size = dp.sec_size;
if (dp.num_sectors != 0)
{
drive_size = (UINT32)dp.num_sectors
* dp.sec_size;
num_sectors = dp.num_sectors;
}
else
{
drive_size = dp.huge_sectors
* dp.sec_size;
num_sectors = dp.huge_sectors;
}
hidden_secs = dp.hidden_sectors;
secs_cluster = dp.secs_cluster;
reserved_secs = dp.reserved_secs;
num_fats = dp.num_fats;
dir_entries = dp.dir_entries;
secs_fat = dp.secs_fat;
max_sector = dp.secs_track;
max_head = dp.num_heads;
dir_sectors = dir_entries / (sec_size
/ 32);
data_sector = (secs_fat * num_fats)
+ dir_sectors + reserved_secs-1;
dir_sector = (secs_fat * num_fats) +
reserved_secs;
cluster_size = sec_size * secs_cluster;
num_clusters = (UINT16)
((num_sectors - (secs_fat
* num_fats) - dir_sectors-1) / secs_cluster)+1;
max_track = (UINT16)
(((num_sectors + hidden_secs)
/ max_head) / max_sector)-1;
[example program]
DOS Note | If dp.num_sectors (a UINT16) is zero dp.huge_sectors (a UINT32) is used to hold the actual number of sectors. This is how DOS handled larger disk drives. |
(Note: More than one sector can be read at a time, depending on what is to be done, as long as the buffer size is the appropriate size.)
int dosread(int drive, UINT32 sector, BYTE *buffer)
{
union REGS regs;
struct SREGS sregs;
regs.x.ax = drive;
regs.x.dx = sector;
regs.x.cx = 1;
regs.x.bx = FP_OFF(buffer);
sregs.ds = FP_SEG(buffer);
int86x(0x25,®s,®s,&sregs);
if (regs.x.cflag)
return regs.h.al;
return 0;
}
For drives greater than 32MB, a Disk Control Block structure must be used:
int dosread(int drive, UINT32 sector, BYTE *buffer)
{
union REGS regs;
struct SREGS sregs;
struct DCB Dcb;
struct DCB *dcb = &Dcb;
regs.x.ax = drive;
dcb->sector = sector;
dcb->number = 1;
dcb->buffer = buffer;
regs.x.cx = 0xffff;
regs.x.bx = FP_OFF(dcb);
sregs.ds = FP_SEG(dcb);
int86x(0x25,®s,®s,&sregs);
if (regs.x.cflag)
return regs.h.al;
return 0;
}
To write a sector just use 0x26 instead of 0x25. However, the write interrupt is not supported by Windows.
(Notes: The int86x() function does handle
the popping of the flags register which DOS leaves on the stack with this
function.)
Coding Note | The macros FP_OFF and FP_SEG differ between compilers. Microsoft's only work with pointers and will not work with & (address of operator). This is the reason for using both a structure and a pointer to a structure in this and the following examples. |
int biosread(int drive, UINT16 track, UINT16 sec,
UINT16 head, BYTE *buffer)
{
unsigned i;
struct diskinfo_t blk;
if (drive > 2)
/* floppys: 0 - 7Fh, hards: 80h - FFh */
drive += (0x80 - 3);
else
drive--;
blk.drive = drive;
blk.track = track;
blk.sector = sector;
blk.head = head;
blk.nsectors = 1;
blk.buffer = buffer;
_bios_disk(_DISK_READ,&blk);
i = _bios_disk(_DISK_STATUS,&blk);
i >>= CHAR_BIT;
/* AH contains result; AL sectors read */
return i;
}
To write a sector use _DISK_WRITE instead of _DISK_READ.
int ioctlread(int drive, UINT16 track, UINT16 sec,
UINT16 head, BYTE *buffer)
{
union REGS regs;
struct SREGS sregs;
struct RWBLOCK Blk;
struct RWBLOCK *blk = &Blk;
blk->special = 0;
blk->head = head;
blk->track = track;
blk->sector = sec - 1;
blk->nsecs = 1;
blk->buffer = buffer;
regs.x.bx = drive;
/* 0 = default, 1 = A:, 2 = B:, etc. */
regs.x.ax = 0x440D;
/* function */
regs.x.cx = 0x0861;
/* sub-function */
regs.x.dx = FP_OFF(blk);
sregs.ds = FP_SEG(blk);
intdosx(®s,®s,&sregs);
if (regs.x.cflag)
return regs.x.ax;
return 0;
}
To write a sector use Sub-function 0x0841 instead of 0x0861.
The DOS function 36h uses a FREESPACE structure:
int getfreespace(int drive, struct FREESPACE *fs)
{
union REGS regs;
regs.x.dx = drive;
regs.x.ax = 0x3600;
intdos(®s,®s);
if (regs.h.al == 0xFF)
return 1;
fs->secs_cluster = regs.x.ax;
fs->avail_clusters = regs.x.bx;
fs->sec_size = regs.x.cx;
fs->num_clusters = regs.x.dx;
return 0;
}
The sizes we need are:
sec_size = fs.sec_size;
drive_size = (UINT32)fs.num_clusters * fs.secs_cluster
* fs.sec_size;
The next section provide the function to read the
boot
sector.
The biosread() version needs a special check for the location of the boot sector: for floppies it resides at track 0, sector 1, and head 0; for hard drives it is at track 0, sector 1, and head 1.
drive = 0;
if (argc == 2)
drive = atoi(argv[1]);
if (drive == 0) {
printf("usage: dosread
<drivenum>");
return 1;
}
if ((i = getdevparams(drive,&dp))
!= 0) {
printf("DEVICEPARAMS
not supported: error == %02x",i);
return 1;
}
if ((buffer = malloc(dp.sec_size)) ==
NULL)
abort();
if (dp.num_sectors != 0)
drive_size = (UINT32)dp.num_sectors
* dp.sec_size;
else
drive_size = (UINT32)dp.huge_sectors
* dp.sec_size;
if (drive_size <= (UINT32)32L * 1024L
* 1024L)
i = dosread(drive,0,buffer);
else
i = dosread32(drive,0,buffer);
if (i != 0)
printf("DOS Read error:
%02x",i);
else
display(drive,(struct
BOOTSECTOR *)buffer);
return 0;
}
drive = 0;
if (argc == 2)
drive = atoi(argv[1]);
if (drive == 0) {
printf("usage: biosread
<drivenum>");
return 1;
}
if ((i = getdevparams(drive,&dp))
!= 0) {
printf("DEVICEPARAMS
not supported: error == %02x",i);
return 1;
}
if ((buffer = malloc(dp.sec_size)) ==
NULL)
abort();
if (drive <= 2)
i = biosread(drive,0,1,0,buffer);
/* floppy disk boot sector */
else
i = biosread(drive,0,1,1,buffer);
/* hard disk boot sector */
if (i != 0)
printf("BIOS Read
error: %02x",i);
else
display(drive,(struct
BOOTSECTOR *)buffer);
return 0;
}
floppy | hard drive | ramdrive1 | |
Get Disk Free Space (INT 21/36) | X | X | X |
Get Device Parameters (INT 21/440D) | X | X |
floppy | hard drive | ramdrive1 | |
dosread (INT 25/26) | X | X | X |
biosread (INT 13) | X | X | |
ioctlread (INT 21/440D) | X | X2 |
DOS | Windows 3.1/95 | Windows NT | |
dosread (INT 25/26) | FHR | FH3 | FH |
biosread (INT 13) | FH4 | FH4 | F |
ioctlread (INT 21/440D) | FH2 | FH2 | FH2 |
1 | may include other drives created by a loadable device driver |
2 | translations will make the IOCTL read invalid for hard drives |
3 | read only for hard drives |
4 | physical drives only |
Presented here are just the basics: you figure out the basic drive parameters,
determine which read/write functions to use, and calculate where on the
drive DOS places it's data. Simple? Well, sort of. Wait until you see the
next part.
PART II