Introduction
I’ve always been curious about how disks are partitioned, and how the partition table looks like. This article will explain the format of the partition table and will provide a small tool (Partition Viewer) to demonstrate what we explained.
The information in this article apply for x86 systems, I don’t know about disk partitioning on other systems.
You are not required to have prior knowledge about disk partitioning; The goal of this article is to provide just the needed knowledge to understand disk partitioning, hence many details are left out for the readers to investigate by themselves.
Please note that this article does NOT cover the GPT (GUID Partition table).
Background
In this section I will introduce some concepts that are needed to understand the article.
When you purchase a new hard-disk with a given capacity, all you can do with it is read/write raw data from first address up to last address. You can achieve that with special tools that can read an unformatted/unpartitioned disk. The data you read/write have no standard structure but the structure you devise.
When we partition a disk, we are simply dividing the disk’s space into partitions (logical boundaries). For example a 10GB can be divided (theoratically) into as many partitions as one desires, say 3GB, 4GB and 3GB partitions.
[======10 GB============] (before partitioning)
[(3GB===)(4GB====)(3GB===)] (after partitioning)
Practically, disk partitioning is more than just dividing the disk’s space into smaller partitions, but also attaching a given structure to each partition.
Those structures are named “File systems”. You may have heard of FAT and NTFS file systems (supported by Microsoft Operating systems).
File systems are a way to allow you to store your data in an organized manner (managable by the host operating system). That is why disk-partitioning has rules and limits (discussed later). For example, you can create a 10GB partition but you cannot attach to it the FAT16 file system, since FAT16 can only manage up to 2GB of disk space. Each file system has its cons and pros, which are not discussed in this document.
To finalize the disk partitioning and file system relationship, you can imagine that a raw non-partitioned disk can be compared to a big room with no divisions where every employee scatters his stuff here and there, but when we divide (partitioning) that space with cubicals we have organized offices and rooms (File system).
Disk addressing and capacity limitation
In this section we will try to explain the disk addressing concepts.
Cylinder/Head/Sector (CHS) addressing
CHS stands for (C)ylinder/(H)ead/(S)ector addressing mode. Those three component together designates the target disk address.
Logical Block Addressing (LBA)
LBA is linear addressing mode, starting from address 0 up to the maximum addressable location in the disk.
As opposed to CHS addressing, LBA is much simpler and easier to understand and is used in modern operating systems.
MBR and partition table format
In this section we will explain about the MBR (Master Boot record) and the format of the partition table and where it is located on the physical disk.
The master boot record (MBR) is a structure that holds the boot loader program and the partition table. The MBR is always located at the very start of the disk, at LBA 0 or 0,0,1 in CHS addressing.
The length of the MBR structure is 512 bytes.
Offset (hex) | Size (in bytes) | Item |
0x0-0x1BD | 446 | Boot loader |
0x1BE-0x1FD | 64 | Partition table |
0x1FE-0x1FF | 2 | Signature (should be 0x55, 0xAA) |
Usually, the role of the MBR is to find which parition table is active, load its boot sector and execute it
The MBR loader can be infected with an MBR virus (sometimes refered to as bootkits) that runs even before the operating system boots (or the boot sector executes)
Boot loader
The boot loader is a program whose sole purpose is to interpret the partition table and decide which partition should be booted from.
The boot loader can be infected with a boot-sector virus that runs even before the operating system boots
Here are the usual steps that are done by the boot loader:
- Load the partition table
- Iterate through its entries looking for a partition entry marked as active
- Once an active partition is found, we load the correspondance boot sector of that partition
- If no active partitions are found then we halt
- Once an active partition is found and its corresponding boot sector is loaded, then MBR executes the MBR code
Boot sector
In the previous section we introduced the term “boot sector” which is not to be confused with MBR’s “boot loader”
The boot sector and the MBR are two different things.
A boot sector is yet another structure, 512 bytes in size, that is file-system specific.
Its function is to initiate the operating system and load it from the disk.
Since the MBR will load a boot sector and execute it, the least requirements for a boot sector are:
- Offset 0 should have a valid x86 instruction so that when MBR runs it, the code does not crash the system
- Offset 0x1FE should have the signature 0x55 0xAA (like the MBR)
In the case of MS-DOS, the boot sector program is responsible for loading IO.SYS and MS-DOS.SYS which will in turn load the COMMAND.COM
Partition Table
The partition table is 64 bytes long and is located inside the MBR at sector 0x1BE.
The partition table is an array of 4 partition table entries each of 16 bytes thus: 16×4 = 64.
The format of each parition table entry is the following:
Offset | Size | Item |
0x00 | 1 | Boot indicator; 0x80 = Active partition / 0x00 Inactive partition (boot_indicator) |
0x01 | 1 | partition start: head (chs_start.head) |
0x02 | 1 | partition start: sector (chs_start.sect) |
0x03 | 1 | partition start: cylinder (chs_start.cyl) |
0x04 | 1 | Partition ID (example ID=1 for FAT12) (system_indicator) |
0x05 | 1 | partition end: head (chs_end.head) |
0x06 | 1 | partition end: sector (chs_end.sector) |
0x07 | 1 | partition end: cylinder (chs_end.cyl) |
0x08 | 4 | Number of sectors before the beginning of this partition (sectors_before) |
0x0C | 4 | Number of sectos in this partition (number_of_sectors) |
You should note that the start and end CHS values are not as they are stored, instead, we have to extract their values from the bits.
real cylinder value = ((chs.sector >> 6) << 8) | (chs.cyl)
We take bits 6-7 from chs_sector which are the high bits of real_cyl_value and then combine them with chs_cyl which are the low bits of real_cyl_value.
The reason they are encoded that way, is because the BIOS INT 0x13/AH=0x02 function (which reads a sector) requires this encoding:
INT 13 - DISK - READ SECTOR(S) INTO MEMORY AH = 02h AL = number of sectors to read (must be nonzero) CH = low eight bits of cylinder number CL = sector number 1-63 (bits 0-5) high two bits of cylinder (bits 6-7, hard disk only) DH = head number DL = drive number (bit 7 set for hard disk) ES:BX -> data buffer
The number_of_sectors field describes how many sectors are in this partition. This field can be used to determine the length of the partition.
Similarly we can use (CHS_END – CHS_START) to determine the length of the partition.
The number of sectors before (at offset 8) designate where the partition data begins starting from the partition table definition. If in the MBR, an entry has sectors_before equal 63 then this means that the boot sector of that partition is located at sector 63
Primary and Extended partitions
A primary partition is a partition entry with an ID different than 5 and 15 and can have file system associated with it.
Extended partitions unlike primary partitions, have IDs 5 or 15 and cannot have a file system associated with them, instead they point to another partition table that have at most two partition table entries.
The second one can be an extended partition, thus nesting more paritions inside of it).
Extended partitions can be considered as just containers of other partitions. We will demonstrate partition table nesting next.
Partition table rules
The following partition table rules are take from other sources, I list only the most relevant ones (in the context of this article):
- Only partition can be active at a time
- In the MBR partition table: at most 4 primary partitions can be created, or 3 primary partitions and 1 extended partition
- In the extended partition table: there can be 0 or 1 extended partition link and 0 or 1 non-extended partition (thus two in total)
- Partition table entries can have any order (we can exchange entry 1 with 2, etc…)
Nested Partitions
Because of the structure of the partition table located in the MBR, we realize that we can have at most 4 partiton table entries in each partition table.
To overcome this limitation, special partition table entries are introduced that allow us to have an unlimited number of partitions that are linked together.
Inside the MBR, the extended partition entry describes the size of all the partitions that will be contained inside of it (using the nb_of_sectors field), whereas the NbSectorsBefore or CHS_START will be used to compute the link to the next partition table which can contain at most 2 partition table entries.
Those two entries are: (1) A normal partition entry (2) another extended partition entry pointing to the next linked partition.
Interpreting a partition table manually with a hex editor
In this section we will demonstrate how to interpret the partition table manually using a hex editor that can display disk sectors.
This screenshot shows the first sector of the disk.
The bytes marked in yellow are the boot loader code.
The bytes marked in red are the signature of the MBR which should always be 0x55 0xAA.
The green area shows a complete partition table entry (16 bytes):
- 00 : Boot indicator -> Inactive
- 01 01 00 : CHS_START -> Head = 1, Sector = 1, Cyl = 0
- 06: System Indicator -> FAT16B (>= 32 MB)
- FE 3F 0B: CHS_END -> Head=254, Sector = 63, Cyl = 11
- 3F 00 00 00: Sectors Before -> 63
- CD F0 02 00: Number of sectos -> 192717
Which means that this is a FAT16 partition of ~96MB.
In the same manner we can interpret the result of the table and read it as:
Entry # | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 0,1,1 | 6: FAT16B (>= 32 MB) | 11,254,63 | 63 | 192717 |
2 | 0 | 166,0,1 | 15: Win95 Extended (LBA) | 12543,254,63 | 2666790 | 26748225 |
3 | 0 | 13,0,1 | 23: Hidden IFS (HPFS/NTFS) | 154,254,63 | 208845 | 2281230 |
4 | 0 | 12543,0,1 | 28: Hidden Win95 FAT32 (LBA) | 12543,254,63 | 29415015 | 4128705 |
Notice that entry #2 is an extended partition (sysid = 15) of size: (26748225 * 512) = 13695091200 bytes = 13060 GB
This partition starts at sector 2666790 (sectors_before), let us go there and see the partition table pointed by it:
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 166,1,1 | 11: Win95 FAT32 | 178,254,63 | 63 | 208782 |
2 | 0 | 184,0,1 | 5: Extended | 209,254,63 | 289170 | 417690 |
As we said, now every partition table that is pointed from the initial extended parition table entry have at most two entries.
The first entry is a normal entry and the second entry (type = extended) is a link to another partition table.
To compute the LBA of the second entry, we add to the sectors before the “sectors before” value from the extended partition table entry of the MBR, that is: 2666790+289170 = sector 2955960.
Now we go there and read the partition table:
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 184,1,1 | 6: FAT16B (>= 32 MB) | 209,254,63 | 63 | 417627 |
2 | 0 | 240,0,1 | 5: Extended | 255,254,255 | 1188810 | 17462655 |
Next partition table is at: 2666790+1188810 = sector 3855600.
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 240,1,1 | 7: Installable File System (NTFS, HPFS) | 255,254,255 | 63 | 17462592 |
2 | 0 | 255,0,193 | 5: Extended | 255,254,255 | 19069155 | 3919860 |
Next partition table is at: 2666790+19069155 = sector 21735945.
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 255,1,193 | 6: FAT16B (>= 32 MB) | 255,254,255 | 63 | 3919797 |
2 | 0 | 255,0,193 | 5: Extended | 255,254,255 | 23246055 | 433755 |
Next partition table is at: 2666790+23246055 = sector 25912845.
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 255,1,193 | 130: Linux swap / Solaris | 255,254,255 | 63 | 433692 |
2 | 0 | 255,0,193 | 5: Extended | 255,254,255 | 23679810 | 3068415 |
Next partition table is at: 2666790+23679810 = sector 26346600.
Entry# | Boot Indicator | CHS_START | System ID | CHS_END | Sectors Before | Nb Sectors |
1 | 0 | 255,1,193 | 11: Win95 FAT32 | 255,254,255 | 63 | 3068352 |
0 | 0 | 0,0,0 | 0 | 0,0,0 | 0 | 0 |
No next entry; end of partition nesting.
Some Partition Table System IDs
The table below lists some of the known system ids (or file system ids).
ID | Name |
0x00 | Empty |
0x01 | FAT12 |
0x02 | XENIX root |
0x03 | XENIX usr |
0x04 | FAT16 <32MB |
0x05 | Extended |
0x06 | FAT16B (>= 32 MB) |
0x07 | Installable File System (NTFS HPFS) |
0x08 | AIX |
0x09 | AIX bootable |
0x0A | OS/2 Boot Manager |
0x0B | Win95 FAT32 |
0x0C | Win95 FAT32 (LBA) |
0x0E | Win95 FAT16 (LBA) |
0x0F | Win95 Extended (LBA) |
0x10 | OPUS |
0x11 | Hidden FAT12 |
0x12 | Compaq diagnostics |
0x14 | Hidden FAT16 <32MB |
0x16 | Hidden FAT16 |
0x17 | Hidden IFS (HPFS/NTFS) |
0x18 | AST SmartSleep |
0x1B | Hidden Win95 FAT32 |
0x1C | Hidden Win95 FAT32 (LBA) |
0x1E | Hidden Win95 FAT16 (LBA) |
0x24 | NEC DOS |
0x2C | WildFile/Adaptec GOBack |
0x39 | Plan 9 |
0x3C | PowerQuest Recoverable Partition |
0x40 | Venix 80286 |
0x41 | PPC PReP Boot |
0x42 | Veritas Logical Disk Manager |
0x4d | QNX4.x |
0x4e | QNX4.x 2nd part |
0x4f | QNX4.x 3rd part |
0x50 | OnTrack DM |
0x51 | OnTrack DM6 Aux1 |
0x52 | CP/M |
0x53 | OnTrack DM6 Aux3 |
0x54 | OnTrackDM6 |
0x55 | EZ-Drive |
0x56 | Golden Bow |
0x5c | Priam Edisk |
0x61 | SpeedStor |
0x63 | GNU HURD or SysV |
0x64 | Novell Netware 286 |
0x65 | Novell Netware (3.11 and 4.1) |
0x66 | Novell Netware 386 |
0x70 | DiskSecure Multi-Boot |
0x75 | PC/IX |
0x78 | XOSL |
0x80 | Old Minix |
0x81 | Linux/Minix v1.4b+ |
0x82 | Linux swap / Solaris |
0x83 | Linux native file system (Ext2/3) |
0x84 | OS/2 hiding type 04h partition |
0x85 | Linux extended |
0x86 | NT FAT volume set |
0x87 | NT IFS volume set |
0x8e | Linux LVM |
0x93 | Amoeba/Hidden Linux native file system (Ext2/3) |
0x94 | Amoeba BBT |
0x9f | BSD/OS |
0xA0 | IBM Thinkpad hibernation |
0xA5 | FreeBSD |
0xA6 | OpenBSD |
0xA7 | NeXTSTEP |
0xA9 | NetBSD |
0xB7 | BSDI fs |
0xb8 | BSDI swap |
0xbb | Boot Wizard hidden |
0xc1 | DRDOS / sec (FAT-12) |
0xc4 | DRDOS / sec (FAT-16 < 32M) |
0xc6 | Disabled NT FAT (FAT-16) volume set/DRDOS |
0xc7 | Syrinx / Disabled NT IFS volume set |
0xda | Non-FS data |
0xdb | CP/M / CTOS / … |
0xde | Dell Corporation diagnostic partition |
0xdf | BootIt |
0xe1 | DOS access |
0xe3 | DOS R/O |
0xe4 | SpeedStor |
0xeb | BeOS fs |
0xee | EFI GPT |
0xef | EFI (FAT-12/16/32) |
0xf0 | Linux/PA-RISC boot |
0xf1 | SpeedStor |
0xf4 | SpeedStor |
0xf2 | DOS secondary |
0xfd | Linux raid autodetect |
0xfe | LANstep |
0xff | Bad Track Table |
PartitionViewer Tool
For the sake of demonstration, I wrote a tool that ships with the following components:
- DiskSector.cpp/.h – A class to read/write raw disk sectors
- partitionmanager.cpp/.h – A class that parses partition tables
- MyDrawBar.cpp/.h – MFC static component to draw the partition tables
- MyHistory.cpp/.h – MFC static component to draw the partition tables
- PartitionViewer[Dlg].cpp/.h – The UI that uses the above helper classes
The PartitionViewer tool hardcodes only a small subset of the system IDs and colors:
static const MYHISTORYBARTYPECOLOR part_colors[] = { {0x0, RGB(255,255,255), "Free space"}, {0x6, RGB(100,10,0), "FAT16B (>= 32 MB)"}, {0xC, RGB(255,0,128), "Win95 FAT32 (LBA)"}, {0xE, RGB(0,110,110), "Win95 FAT16 (LBA)"}, {0x7, RGB(200,0,20), "Installable File System (NTFS, HPFS)"}, {0x0B, RGB(10,100,0), "Win95 FAT32"}, {0x83, RGB(20,200,0), "Linux native file system (Ext2/3)"}, {0x17, RGB(10,0,100), "Hidden IFS (HPFS/NTFS)"}, {0x82, RGB(101, 12, 130), "Linux swap / Solaris"}, {0x1C, RGB(90, 127, 30), "Hidden Win95 FAT32 (LBA)"}, {0x1E, RGB(0, 0, 255), "Hidden Win95 FAT16 (LBA)"}, {0x04, RGB(101, 12, 130), "FAT16 <32MB"} };
Feel free to extend that list.
This program requires admin privilege to work because it accesses the disk sectors.
Once executed:
the partitions will be visualized.
- Use the dropdown list to select another drive then press the “Represent” button
- Hover the mouse over the drawn area to display the partition information in the panel just below the drive dropdown list
Reference
I will like to acknowledge all the people who wrote all those articles, open source programs and references that made the writing of this article possible.
- Pierre’s field guide to partition table recovery (http://www.datarescue.com/laboratory/partition.htm)
- Boot-Manager Boot-US: Glossary (http://www.boot-us.com/glossary.htm)
- How It Works: Partition Tables – by Hale Landis
- Ralf Brown Interrupt List reference
Conclusion
I hope you found this article useful. In the future, another article can be written to cover the GPT.
Please leave your comments or suggestions below and I will try to answer questions or fix the code as much as my (limited) time and my knowledge allow.
Donation to support this blog and encourage me to write more useful blog entries are welcome:
Download the sources and compiled binaries from here
Thanks for posting this information. I find it very interesting.