Building a Disk Partition Analyzer in Go
Ever wondered what happens when you run fdisk -l or open Disk Management on Windows? These tools read raw disk data to show you partition information. Today we’ll build our own partition analyzer in Go that can parse both MBR and GPT partition tables.
Before writing code, we need to understand what we’re parsing. Every disk starts with a partition table that tells the operating system how the disk is divided up.
The Master Boot Record (MBR)
The MBR is the legacy partitioning scheme, limited to 4 primary partitions and 2TB disks. It lives in the first 512 bytes of the disk:
- Bytes 0-445: Boot code
- Bytes 446-509: Four 16-byte partition entries
- Bytes 510-511: Magic signature (0x55AA)
Each partition entry contains: - Boot flag (1 byte) - Starting CHS address (3 bytes) - Partition type (1 byte) - Ending CHS address (3 bytes) - Starting LBA (4 bytes) - Size in sectors (4 bytes)
The GUID Partition Table (GPT)
GPT is the modern replacement for MBR, supporting 128 partitions and disks larger than 2TB:
- LBA 0: Protective MBR (partition type 0xEE)
- LBA 1: GPT header (512 bytes)
- LBA 2+: Partition entries (128 bytes each)
The GPT header starts with “EFI PART” signature and contains metadata about the partition table, including the number of partitions and their location.
Setting Up Our Go Project
Let’s start by defining the data structures we’ll need:
package main
import (
"encoding/binary"
"fmt"
"os"
)
const (
MBR_SIGNATURE = 0xAA55
GPT_SIGNATURE = "EFI PART"
SECTOR_SIZE = 512
)
// MBR Partition Entry (16 bytes)
type MBRPartition struct {
Status uint8
StartCHS [3]uint8
Type uint8
EndCHS [3]uint8
StartLBA uint32
SizeBlocks uint32
}
// GPT Header (first 92 bytes we care about)
type GPTHeader struct {
Signature [8]byte
Revision uint32
HeaderSize uint32
HeaderCRC32 uint32
Reserved uint32
CurrentLBA uint64
BackupLBA uint64
FirstUsableLBA uint64
LastUsableLBA uint64
DiskGUID [16]byte
PartitionTableLBA uint64
NumPartitions uint32
PartitionEntrySize uint32
PartitionTableCRC uint32
}
// GPT Partition Entry (128 bytes)
type GPTPartition struct {
TypeGUID [16]byte
PartitionGUID [16]byte
StartLBA uint64
EndLBA uint64
Attributes uint64
Name [72]byte // UTF-16LE
}
These structs map directly to the on-disk format. Notice how we use fixed-size arrays for binary data and specific integer types that match the disk layout.
The encoding/binary package will help us convert the raw bytes into these structured types, handling endianness correctly.
Reading and Validating the MBR
Now let’s implement the core logic to read a disk image and determine what type of partition table it uses:
func main() {
if len(os.Args) != 2 {
fmt.Printf("Usage: %s <disk_image_file>\n", os.Args[0])
os.Exit(1)
}
filename := os.Args[1]
file, err := os.Open(filename)
if err != nil {
fmt.Printf("Error opening file: %v\n", err)
os.Exit(1)
}
defer file.Close()
// Read first sector (MBR)
mbr := make([]byte, SECTOR_SIZE)
n, err := file.Read(mbr)
if err != nil || n != SECTOR_SIZE {
fmt.Printf("Error reading MBR: %v\n", err)
os.Exit(1)
}
// Check MBR signature
signature := binary.LittleEndian.Uint16(mbr[510:512])
if signature != MBR_SIGNATURE {
fmt.Println("Invalid MBR signature")
os.Exit(1)
}
fmt.Printf("Disk Image: %s\n", filename)
fmt.Println("=" + string(make([]byte, len(filename)+12)))
// Check if it's GPT by looking at partition type of first entry
firstPartType := mbr[446+4] // First partition type byte
if firstPartType == 0xEE {
// This is GPT
fmt.Println("Partition Table Type: GPT")
readGPTPartitions(file)
} else {
// This is MBR
fmt.Println("Partition Table Type: MBR")
readMBRPartitions(mbr)
}
}
This main function handles the basic workflow by first parsing the command line to expect a single argument representing the disk image file. It then performs essential file operations by opening the specified file and reading the first 512 bytes, which corresponds to sector 0 of the disk. The function validates the Master Boot Record by checking for the magic signature 0xAA55 located at bytes 510-511, ensuring the sector contains valid boot record data.
The partition table detection process involves examining the first partition’s type byte to determine the disk format. If this byte contains the value 0xEE, it indicates the presence of a GPT protective MBR. The key insight is that GPT disks maintain backward compatibility by including an MBR in sector 0, but this MBR contains only a single partition entry of type 0xEE that spans the entire disk. This protective mechanism prevents older disk management tools from accidentally corrupting the more advanced GPT partition data structure.
Parsing MBR Partitions
Let’s implement the MBR parser first since it’s simpler:
func readMBRPartitions(mbr []byte) {
fmt.Println("\nPartitions:")
fmt.Printf("%-4s %-8s %-12s %-12s %-12s %s\n",
"#", "Status", "Type", "Start LBA", "Size", "Description")
fmt.Println(string(make([]byte, 70)))
partCount := 0
for i := 0; i < 4; i++ {
offset := 446 + (i * 16)
status := mbr[offset]
partType := mbr[offset+4]
startLBA := binary.LittleEndian.Uint32(mbr[offset+8 : offset+12])
sizeBlocks := binary.LittleEndian.Uint32(mbr[offset+12 : offset+16])
if partType != 0 {
partCount++
statusStr := "Inactive"
if status == 0x80 {
statusStr = "Active"
}
sizeGB := float64(sizeBlocks*SECTOR_SIZE) / (1024 * 1024 * 1024)
typeDesc := getMBRTypeDescription(partType)
fmt.Printf("%-4d %-8s 0x%-10X %-12d %-12.2f %s\n",
i+1, statusStr, partType, startLBA, sizeGB, typeDesc)
}
}
if partCount == 0 {
fmt.Println("No partitions found")
}
}
The MBR parser iterates through all 4 possible partition entries, starting at byte 446. For each entry, we extract the status byte (0x80 means bootable), partition type, starting LBA, and size in sectors. We convert the size to GB for better readability.
Parsing GPT Partitions
GPT parsing is more complex because we need to read the header first to understand the partition table layout:
func readGPTPartitions(file *os.File) {
// Read GPT header from LBA 1
file.Seek(SECTOR_SIZE, 0)
headerBytes := make([]byte, 512)
n, err := file.Read(headerBytes)
if err != nil || n != 512 {
fmt.Printf("Error reading GPT header: %v\n", err)
return
}
// Parse GPT header
var header GPTHeader
header.Signature = *(*[8]byte)(headerBytes[0:8])
header.Revision = binary.LittleEndian.Uint32(headerBytes[8:12])
header.HeaderSize = binary.LittleEndian.Uint32(headerBytes[12:16])
header.NumPartitions = binary.LittleEndian.Uint32(headerBytes[80:84])
header.PartitionEntrySize = binary.LittleEndian.Uint32(headerBytes[84:88])
header.PartitionTableLBA = binary.LittleEndian.Uint64(headerBytes[72:80])
// Verify GPT signature
if string(header.Signature[:]) != GPT_SIGNATURE {
fmt.Println("Invalid GPT signature")
return
}
fmt.Printf("GPT Revision: %d.%d\n", header.Revision>>16, header.Revision&0xFFFF)
fmt.Printf("Number of Partitions: %d\n", header.NumPartitions)
// Read partition entries
file.Seek(int64(header.PartitionTableLBA*SECTOR_SIZE), 0)
fmt.Println("\nPartitions:")
fmt.Printf("%-4s %-12s %-12s %-12s %s\n",
"#", "Start LBA", "End LBA", "Size", "Name")
fmt.Println(string(make([]byte, 60)))
partCount := 0
for i := uint32(0); i < header.NumPartitions; i++ {
partBytes := make([]byte, header.PartitionEntrySize)
n, err := file.Read(partBytes)
if err != nil || uint32(n) != header.PartitionEntrySize {
break
}
// Check if partition entry is used (non-zero type GUID)
allZero := true
for j := 0; j < 16; j++ {
if partBytes[j] != 0 {
allZero = false
break
}
}
if !allZero {
partCount++
startLBA := binary.LittleEndian.Uint64(partBytes[32:40])
endLBA := binary.LittleEndian.Uint64(partBytes[40:48])
// Convert UTF-16LE name to string (simplified)
name := ""
for j := 56; j < 56+72; j += 2 {
if partBytes[j] == 0 && partBytes[j+1] == 0 {
break
}
if partBytes[j+1] == 0 {
name += string(partBytes[j])
}
}
if name == "" {
name = "Unnamed"
}
sizeGB := float64((endLBA-startLBA+1)*SECTOR_SIZE) / (1024 * 1024 * 1024)
fmt.Printf("%-4d %-12d %-12d %-12.2f %s\n",
partCount, startLBA, endLBA, sizeGB, name)
}
}
if partCount == 0 {
fmt.Println("No partitions found")
}
}
The GPT parser operates through a systematic approach that begins with header parsing by reading LBA 1 and extracting essential fields such as the number of partitions and their storage location. Then performs signature verification to ensure the header starts with the “EFI PART” identifier, confirming the presence of a valid GPT structure. Unlike the MBR format which is limited to four fixed partition entries, GPT supports dynamic partition reading with the capability to handle up to 128 partitions, providing significantly more flexibility for disk organization.
The parser must also handle UTF-16 name conversion since GPT partition names are stored in UTF-16LE format, requiring basic conversion logic to make them readable. The most challenging aspect involves detecting which partitions are actually in use, as GPT reserves space for all possible partition entries regardless of whether they contain data. The system determines partition validity by examining the first 16 bytes of each entry, which contains the type GUID. Unused partition entries will have all-zero type GUIDs, while valid partitions contain meaningful GUID values that identify their purpose and format.
Adding Partition Type Descriptions
To make our output more readable, we need a lookup table for MBR partition types:
func getMBRTypeDescription(partType uint8) string {
descriptions := map[uint8]string{
0x00: "Empty",
0x01: "FAT12",
0x04: "FAT16 <32M",
0x05: "Extended",
0x06: "FAT16",
0x07: "HPFS/NTFS/exFAT",
0x0B: "W95 FAT32",
0x0C: "W95 FAT32 (LBA)",
0x0E: "W95 FAT16 (LBA)",
0x0F: "W95 Ext'd (LBA)",
0x11: "Hidden FAT12",
0x14: "Hidden FAT16 <32M",
0x16: "Hidden FAT16",
0x17: "Hidden HPFS/NTFS",
0x1B: "Hidden W95 FAT32",
0x1C: "Hidden W95 FAT32 (LBA)",
0x1E: "Hidden W95 FAT16 (LBA)",
0x82: "Linux swap",
0x83: "Linux",
0x85: "Linux extended",
0x8E: "Linux LVM",
0xA0: "Hibernation",
0xA5: "FreeBSD",
0xA6: "OpenBSD",
0xA8: "Darwin UFS",
0xA9: "NetBSD",
0xAB: "Darwin boot",
0xAF: "HFS / HFS+",
0xBE: "Solaris boot",
0xBF: "Solaris",
0xEB: "BeOS fs",
0xEE: "GPT",
0xEF: "EFI (FAT-12/16/32)",
0xFB: "VMware VMFS",
0xFC: "VMware VMKCORE",
0xFD: "Linux raid autodetect",
}
if desc, exists := descriptions[partType]; exists {
return desc
}
return "Unknown"
}
This lookup table covers the most common partition types you’ll encounter. The 0xEE type is particularly important as it indicates a GPT protective MBR.
Usage
Now that we have a complete partition analyzer, let’s see it in action. Compile the program:
go build -o partition-analyzer main.go
Test it on various disk images:
# Analyze a disk image
./partition-analyzer /dev/sda
./partition-analyzer disk.img
# Example output for MBR disk:
Disk Image: mbr-disk.img
=======================
Partition Table Type: MBR
Partitions:
# Status Type Start LBA Size Description
----------------------------------------------------------------------
1 Active 0x7 2048 50.00 HPFS/NTFS/exFAT
2 Inactive 0x83 104857600 25.50 Linux
3 Inactive 0x82 158007296 2.00 Linux swap
# Example output for GPT disk:
Disk Image: gpt-disk.img
=======================
Partition Table Type: GPT
GPT Revision: 1.0
Number of Partitions: 128
Partitions:
# Start LBA End LBA Size Name
------------------------------------------------------------
1 2048 1050623 512.00 EFI System Partition
2 1050624 210765823 100.00 Microsoft Basic Data
3 210765824 214960127 2.00 Linux swap
Beyond the Basics
This partition analyzer provides a solid foundation for more advanced disk utilities. You could extend it to:
- Validate checksums: GPT headers and partition tables have CRC32 checksums
- Handle extended partitions: MBR supports logical partitions within extended partitions
- Parse filesystem headers: Read filesystem metadata from each partition
- Repair corrupted tables: Use backup GPT header to recover from corruption
- Support other architectures: Handle big-endian systems and different sector sizes
The core concepts remain the same: read binary data, validate signatures, and parse structured information. Understanding these fundamentals gives you the tools to work with any disk format or binary file structure.
Building tools like this gives you deep insight into how operating systems manage storage. Every time you format a drive, create a partition, or boot your computer, similar code is reading and writing these exact data structures.
You can find code for this project on https://github.com/rezmoss/partition-analyzer.
Web assembly Version
Want to try this partition analyzer without installing anything? I’ve compiled this exact Go code to WebAssembly and created a browser-based disk analyzer tool that runs entirely in your browser. Upload any disk image and analyze its partition table instantly - no server uploads required, everything runs locally using the same logic we built here. It’s a perfect demonstration of how systems programming tools can be made accessible through modern web technologies while maintaining the performance of compiled code.