The Partition Table

From EDM2
Jump to: navigation, search

Written by Andy Pitonyak

Introduction

I was becoming annoyed that I could not access my FAT32 partition from within OS/2 and I decided that I would write a driver that would allow me to do this. I have not come close to accomplishing this but Henk Kelder has, so if you have any interest in this then check out his beta driver at http://ourworld.compuserve.com/homepages/hkelder/. I did, however, start poking around my hard drive and studying my partition tables. I would like to say that this was an easy process, but it was not, so I decided to share my experiences with others to hopefully save them the aggravation that I faced. Another good resource can be found at http://www.uruk.org/~erich/, look for the GRUB stuff by Erich Boleyn (erich@uruk.org).

[Note: here is a link to the source code for this article. Ed.]

Reading The Partition Table

Before worrying about interpreting the partition table, I had to be able to read it. After browsing through the online help which came with the Borland compiler, I opted to use DosDevIOCtl() category 9, function 0x64h, read sectors. The call is essentially

DosDevIOCtl(handle, 9L, 0x64, parmList, parmSize, &szParam,
            data, szData, &szDataU);

I had some difficulties dealing with the parameter list so I encapsulated it in the class IOCtlParam. The structure is as follows:

 Size	Description
 Byte	Command Information
 Word	Head
 Word	Cylinder
 Word	First Sector
 Word	Number of Sectors
 Bytes	Track Layout Tables

The command information must be entirely zero except for the first bit where one means that the sectors are stored consecutively starting with sector one and a zero means that at least one of these conditions is not true. I must admit that I have no idea how to determine what the track layout is, and I always assume that the sector size is 512 bytes. Although these values have worked on every system which I have tried, the existence of the variable parameters implies to me that this is not a safe assumption. Further information on this will be gladly accepted but this is not the thrust of this article. My initialization code is similar to the following:

   command = 1; // Assume consecutive
   head = 0;
   cylinder = 0;
   firstSector = 0;
   numSectorsToDo = 1;
   for (WORD i=0; i<numSectors(); ++i) {
      bytes[2*i] = i+1;
      bytes[2*i+1] = 512;
   }

Note that the DosDevIOCtl function does not move the heads so you can not read more than a single track worth of information with one command.

The primary boot partition is located at head zero, cylinder zero, sector zero which is the first sector on the drive.

The Partition Table

So now it is time to read the partition table. The program which I wrote, contains the ability to dump physical sectors on the hard drive. Note that physical drives are accessed with the numbers where drive 1 refers to the first physical drive, usually drive C:. The parameters to obtain a dump include "dump <physical drive> <first sector> <number of sectors>". To see the partition table, type the following:

   [d:\myos2\devsrc\myprogs\drive\edm]driveinf DUMP 1 0 1
 
   Drive 1: sector 0
 
      0 - fa b8 30 00 8e d0 bc 00 01 fb fc b8 00 00 8e d8
     10 - 8e c0 be 00 7c bf 00 7e b9 00 02 f3 a5 b8 22 7e
     20 - 50 c3 be cf 7e bb be 7f 80 7f 04 0a 74 45 83 c3
   <... data deleted ...>
    190 - 7c bf 05 00 b8 00 00 cd 13 b8 01 02 cd 13 73 03
    1a0 - 4f 7f f1 c3 00 00 00 00 00 00 00 00 00 00 00 00
    1b0 - 00 00 00 00 00 00 01 00 00 00 00 00 33 cc 00 01
    1c0 - 01 01 06 fe 7f 05 00 3f 00 00 86 fa 3f 00 00 00
    1d0 - 41 06 05 fe bf 0f 86 39 40 00 8a 34 41 00 80 01
    1e0 - 01 00 0a fe 3f 00 3f 00 00 00 82 3e 00 00 00 00
    1f0 - 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
   [d:\myos2\devsrc\myprogs\drive\edm]

The actual layout of the partition table is as follows:

   Offset	Description
   0x000	Boot code
   0x1BE	16 BYTE Entry in the partition table
   0x1CE	16 BYTE Entry in the partition table
   0x1DE	16 BYTE Entry in the partition table
   0x1EE	16 BYTE Entry in the partition table
   0x1FE	2 BYTE IDcode(0xAA55) identifies partition sector

For my purposes, the first 0x1BE bytes are not very interesting since I am not currently looking at the boot code. If you have any interest in this, check out http://www.uruk.org/~erich/. Each table entry is represented as follows:

   Offset	Description
   BYTE	Partition status, 0x80 = Active, 0x00 = inactive
   BYTE	First head used by partition
   BYTE[2]	First sector and cylinder used by partition
   BYTE	Partition type
   BYTE	Last head used by partition
   BYTE[2]	Last sector and cylinder used by partition
   ULONG	Location of boot sector
   ULONG	Number of sectors for partition

These items are encapsulated with the following structures.

   typedef struct {
      BYTE  status_;
      BYTE  startHead_;
      BYTE  startSecCyl_[2];
      BYTE  type_;
      BYTE  endHead_;
      BYTE  endSecCyl_[2];
      ULONG bootSec_;
      ULONG numSec_;
   } PartitionEntryStruct;
 
   typedef struct {
      BYTE  bootCode_[0x1BE];
      PartitionEntryStruct entries_[4];
      WORD  iDCode_;
   } PartitionSectorStruct;

The order that the partition table entries appear in the partition table is not defined, so do not assume that they appear in any particular order. The example shown above has the following values:

   Status Hd sec/cyl Typ Hd sec/cyl Boot Sector Num Sectors
   ====== == ======= === == ======= =========== ===========
     00   01  01 01  06  fe  7f 05  00 3f 00 00 86 fa 3f 00
     00   00  41 06  05  fe  bf 0f  86 39 40 00 8a 34 41 00
     80   01  01 00  0a  fe  3f 00  3f 00 00 00 82 3e 00 00
     00   00  00 00  00  00  00 00  00 00 00 00 00 00 00 00

Which correspond to:

   Partition at offset 0
   Stat Type                 Head Cyl  Sect Head Cyl  Sect Boot Sct Num Sct
   ==== ==================== ==== ==== ==== ==== ==== ==== ======== =======
   0x00 0x06=DOS >32M          1    1    1  254  261   63    16128  4192902
   0x00 0x05=Extended          0  262    1  254  527   63  4209030  4273290
   0x80 0x0a=Boot Mgr          1    0    1  254    0   63       63    16002
   0x00 0x00=Empty             0    0    0    0    0    0        0        0

The status byte of the partition can have two values, 0, which means the partition is not the active primary partition, and 0x80 (high bit set) which means that this is the active primary partition. The next byte indicates on which head the partition starts. The next two bytes indicate the sector and the cylinder in a compressed format. The sector is held in the first six bits (bits 0 through five) and bits 6 through 15 contain the cylinder. The sector is given by (*startSecCyl_) & (0x3F) and the packed cylinder is obtained with (startSecCyl_[1]+((startSecCyl_[0] & 0xC0) << 2). The type byte indicates what the partition really is. I do not know what all of the valid values are, but I have gleaned the following types:

 Value	Type                Value  Type
 0x00	Empty               0x07   OS/2 HPFS
 0x01	FAT-12              0x0A   Boot Manager
 0x02	XENIX               0x0B   FAT-32
 0x03	XENIX               0x64   Novell
 0x04	FAT-16              0x75   PCIX
 0x05	Extended Partition  0xDB   CPM/Concurrent
 0x06	DOS >32Meg          0xFF   BBT

The next three bytes indicate the end head, sector, and cylinder of the partition. The following four bytes contain the boot sector and the final four contain a count on the number of sectors used.

Although this is not required, partitions generally start with head 0 and sector 1. This leaves some unused sectors. These extra sectors can hide either a boot sector virus or boot manager code.

An extended partition table can contain as many as two secondary partition entries (logical drives), and two extended partitions. If there are three partitions inside of a single extended partition, then extra partition tables will be used. These extra partition tables will be referenced as extended partitions inside of an existing extended partition.

In the primary partition table, the boot sector indicates where the extended partition is located (for extended partitions). If the entry is already inside of an extended partition then the referenced extended partition is at the location of the stated boot sector plus the offset of the main enclosing extended partition. Consider the following pseudo code to traverse the partition tables.

   printPartitionInformation(absoluteSector, offset) {
     table = readSector(absoluteSector);
     PrintThisInformation(table);
     for (i=0; i<4; ++i)
       if (table->entries_[i].type_ == 0x05) {
         sect = table->entries[i].bootSec_;
         newOffset = offset == 0 ? sect : offset;
         printPartitionInformation(sect + offset, newOffset);
       }
   }

An initial call of printPartitionInformation(0, 0) gets things started. Note that for the first partition table, the offset is zero. The offset is then changed once to the start of the first partition table. Any subsequent embedded partition tables will still use the same offset.

Source Files

I created five source files which I have attempted to document. I wanted to have a nice method of printing error messages so I created errprnt.hpp and errprnt.cpp. These files contain a single function

printErrorMessage(APIRET error, char *msg)

This routine accepts a standard error number and it prints both the associated text message and a supplied text message. By using this routine, I was not forced to continually lookup error codes.

The file drvstrct.hpp contains structures which are useful for certain low level routines. This includes the PartitionEntryStruct and the PartitionSectorStruct for example.

DriveInf.hpp and DriveInf.cpp contain the primary code used for these examples.

I used the Borland C++ compiler for OS/2 version 2. Strangely, it reports itself as Version 4.11. I use the following command line to compile my code:

bcc driveinf.cpp errPrnt.cpp

or

icc /sp1 driveinf.cpp errPrnt.cpp

I lost my Visual Age stuff during a move from Germany, so I do not have my documentation, and thus I have not done much testing with the IBM Visual Age compiler. If you want to use this with the IBM compiler, be certain to use the /SP1 compile option to align the data on 1 byte boundaries as opposed to the default four byte boundaries. You could also use compiler directives.

After the program has been compiled, you can determine how many partitionable drives are present in the system.

[d:\test]driveinf num physical
Number of partitionable drives = 2

You can print the partition tables for a physical drive on the system.

   [d:\test]driveinf partition print 2:
 
   Partition info for physical drive 2
   *********************************************************
 
   Partition at offset 0
   Stat Type                 Head Cyl  Sect Head Cyl  Sect Boot Sct Num Sct
   ==== ==================== ==== ==== ==== ==== ==== ==== ======== ========
   0x00 0x05=Extended          0    1    1   63  531   32     2048  1087488
   0x00 0x00=Empty             0    0    0    0    0    0        0        0
   0x00 0x00=Empty             0    0    0    0    0    0        0        0
   0x00 0x00=Empty             0    0    0    0    0    0        0        0
 
   Partition at offset 2048
   Stat Type                 Head Cyl  Sect Head Cyl  Sect Boot Sct Num Sct
   ==== ==================== ==== ==== ==== ==== ==== ==== ======== ========
   0x00 0x0b=FAT-32            1    1    1   63  531   32       32  1087456
   0x00 0x00=Empty             0    0    0    0    0    0        0        0
   0x00 0x00=Empty             0    0    0    0    0    0        0        0
   0x00 0x00=Empty             0    0    0    0    0    0        0        0

There is also an option to query the device parameters for a physical drive. This uses DosDevIOCtl( ) category 9 function 0x63. This will fail if the drive can not be locked.

   [d:\test]driveinf.exe params physical 1:
 
   *********************************************************
   queryPhysicalDeviceParams() for physical drive 1
 
   cyl = 0 heads = 0 sect = 0
 
   Error: (5) ACCESS_DENIED
    from queryPhysicalDeviceParams()
   [d:\test]driveinf.exe params physical 2:
 
   *********************************************************
   queryPhysicalDeviceParams() for physical drive 2
 
   cyl = 528 heads = 255 sect = 63

There is also a dump command which takes three parameters, the logical drive, the first sector to read and the number of sectors. An example of this is shown early on as

driveinf DUMP 1 0 1

which reads from the first logical drive numbered 1, the first sector numbered 0, and one sector is read.

Wrap-up

The code samples are pretty straight forward and should give you a good start on reading and interpreting your own partition tables. In the development of this code, I used the Gamma Tech Utilities to poke around my harddisks; this is highly recommended. I also used the book "PC Intern: The Encyclopedia of System Programming", the sixth edition by Michael Tischer and Bruno Jennrich published by Abacus.

If you have any questions or comments on the code, drop me a line at andyp@primatech.com.