NetBSD: Playing with disklabels

8 min read Original article ↗

blog - git - desktop - contact


2026-09-25

Coming from DOS and Linux (and having largely ignored this on my OpenBSD systems -- yeah, shame on me), I'm not very familiar with the BSD disklabels. So let's have a look.

I'm not entirely certain if all BSDs use the exact same format and as far as I know there are differences among architectures, so let's be specific here: I'm looking at NetBSD 11 on x86_64.

I'll be running everything in a VM, so I can easily swap disks and inspect them.

To avoid conflicts with MBR systems, disklabels are stored at a different location. In my test VM, it can be found at the second sector:

bine-initial.png: Hex editor screenshot: At offset 0x200, we can see the disklabel magic number.

I was primarily interested in the format of this disklabel: What do the individual bytes mean? And where is that specificed/documented?


Table of contents:


Where to find documentation?

This question is easy to answer. Look at man diskabel (as you would "instinctively" do, because that's the tool to manipulate those labels), there's disklabel(5) in the SEE ALSO section: Section 5 describes file formats, so that's what we want to look at (man 5 disklabel).

For most of this post, though, I looked directly at /usr/include/sys/disklabel.h instead of the manual page.

Part 0: Where is the disklabel?

First things first. The manual page tells you where to find the disklabel: getlabelsector() and getlabeloffset() answer that. So let's write a little C program to see what they return.

netbsd# cat test.c
#include <stdio.h>
#include <util.h>

int
main()
{
    printf("getlabelsector() = %d\n", getlabelsector());
    printf("getlabeloffset() = %ld\n", getlabeloffset());

    return 0;
}

netbsd# cc -Wall -Wextra -o test test.c -lutil

Output:

netbsd# ./test
getlabelsector() = 1
getlabeloffset() = 0

So sector 1 (byte offset 512 in my case) is correct. It's not just some random data in the screenshot above but that is the disklabel.

The disklabel begins with a bunch of "metadata" about the disk, then the actual partition table follows at the end. Let's look at this metadata first.

A bit hard to illustrate this. Here's an overview first (open it in a new tab next to this text), and then we'll go through this one by one:

disklabel-metadata.png: Hex dump of my disklabel. Colors indicate the individual fields.

All of this is little endian on my Intel machine.

Generic disk/drive information:

  • 5745 5682: d_magic, Magic Number
  • 0f00: d_type, drive type = 0x0f = "logical disk"
  • 0000: d_subtype, specific to d_type
  • 6c64...0000 / "ld1": d_typename
  • 4d79...0000 / "My Cool Disk": d_packname, user enters this name

disklabel.h contains a table of the possible values for d_type.

Geometry/size:

  • 0002 0000: d_secsize, 512 bytes per sector
  • 3f00 0000: d_nsectors, 63 sectors per track
  • 1000 0000: d_ntracks, 16 tracks per cylinder
  • 0401 0000: d_ncylinders, 260 cylinders per unit
  • f003 0000: d_secpercyl, 1008 sectors per cylinder
  • 0000 0400: d_secperunit, 262144 sectors per unit (128 MiB)
  • 0000: d_sparespertrack, no spare sectors per track
  • 0000: d_sparespercyl, no spare sectors per cylinder
  • 0000 0000: d_acylinders, "alternative cylinders" (?)

Hardware parameters and timings, probably very irrelevant on x86_64:

  • 100e: d_rpm, 3600
  • 0100: d_interleave
  • 0000: d_trackskew
  • 0000: d_cylskew
  • 0000 0000: d_headswitch, head switch time in microseconds
  • 0000 0000: d_trkseek, track-to-track seek time in microseconds
  • 0000 0000: d_flags, "generic flags" :-)
  • 5x 0000 0000: d_drivedata, drive-type specific
  • 5x 0000 0000: d_space, reserved

End of this first part:

  • 5745 5682: d_magic2, Magic Number
  • c61d: d_checksum
  • 0400: d_npartitions, how many partitions follow in the array below
  • 0020 0000: d_bbsize, size of boot area at sn0 in bytes
  • 0020 0000: d_sbsize, max size of fs superblock in bytes

These are the values that I determined by manually reading the hex dump and comparing it with struct disklabel in the header file. As far as I can tell, it matches the output of the diskabel tool:

netbsd# disklabel ld1
# /dev/rld1:
type: ld
disk: ld1
label: My Cool Disk
flags:
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 16
sectors/cylinder: 1008
cylinders: 260
total sectors: 262144
rpm: 3600
interleave: 1
trackskew: 0
cylinderskew: 0
headswitch: 0           # microseconds
track-to-track seek: 0  # microseconds
drivedata: 0

A lot of this information feels quite outdated, at least on "modern" x86_64. (But MBR isn't much better in this regard, with all the CHS stuff still going on.) And much of it is indeed unused and just set to 0.

Part 2: The partition table

d_npartitions said there are four partitions. Here's an overview of this table, it immediately follows the "metadata" section:

disklabel-partitions-overview.png: Same hex dump as above, but the area where the partition table is located is highlighted.

This corresponds to partitions a, b, c, and d.

Let's take a closer look at partition a:

disklabel-partition-a.png: Same hex dump as above, individual fields of partition 'a' highlighted.

The individual fields:

  • 0000 0400: p_size, 262144 sectors
  • 0000 0000: p_offset, 0 sectors
  • 0000 0000: p_fsize, "filesystem basic fragment size" (?)
  • 07: p_fstype, filesystem type = 0x07 = 4.2BSD / ffs
  • 00: p_frag, "filesystem fragments per block" (?)
  • 0000: p_cpg or p_sgs: UFS or LFS specific

disklabel.h contains a table of the possible values for p_fstype.

There's really only three important bits of information, I think:

  • Start,
  • end,
  • and type of the partition.

Again, my interpretation matches the output of disklabel:

netbsd# disklabel ld1
...
4 partitions:
#        size    offset     fstype [fsize bsize cpg/sgs]
 a:    262144         0     4.2BSD      0     0     0  # (Cyl.      0 -    260*)
 d:    262144         0     unused      0     0        # (Cyl.      0 -    260*)

According to this mail by Martin Husemann, partition d is always there on x86_64 and describes the entire disk. Partition c would be the area usable for NetBSD. Why c isn't included in the output here, I'm not sure. (Actually, no, I think I know why: Size, offset, and type are all zero, so it gets hidden, I guess. But why didn't disklabel -I include c when I first created the disklabel?)

Verifying the checksum

The header file says:

uint16_t d_checksum;  /* xor of data incl. partitions */

So ... I guess we chunk up all this data into 16-byte pieces and then xor them all together? Let's try:

$ dd if=zwei.raw bs=512 count=1 skip=1 status=none |
    od -An -vt x2 -w2 |
    gawk '{ v = strtonum("0x" $1); cksum = xor(cksum, v) } END { printf("%04x\n", cksum) }'
0000

All zeroes. When you think about it: That's confirmation that everything is correct. :-) The actual checksum is one of those 16-byte chunks, so zero is the correct answer.

Exclude the checksum from the data:

$ dd if=zwei.raw bs=512 count=1 skip=1 status=none |
    od -An -vt x2 -w2 |
    sed 69d |
    gawk '{ v = strtonum("0x" $1); cksum = xor(cksum, v) } END { printf("%04x\n", cksum) }'
1dc6

There you go, that matches the little-endian c61d that we saw in the dump. (Obviously, because ... that's the line that I excluded ... yeah ... but you get the idea.)

I'm not familiar with NetBSD's code base yet, but I think this might be their code to compute the checksum (at least it does the same thing I did):

https://cvsweb.netbsd.org/bsdweb.cgi/src/sys/lib/libkern/dkcksum.c?rev=1.1.6.2;content-type=text%2Fplain

Exercise: Create some random partition in the hex editor

Let's do it the other way around: Given what we know now, open the hex editor and define partition i, size 1234 sectors, start at sector 5678, filesystem type ZFS.

Partition i is the ninth partition, so its entry should start at byte 788 on the disk:

512 + 0x94 + (9 - 1) * 16 = 788
\_/   \__/   \_____/   |
 |      |       |      \- Each label is 16 bytes in size, see
 |      |       |         disklabel.h.
 |      |       |
 |      |       \- We want to know the *start* of the ninth label.
 |      |
 |      \- Start of first partition entry, see hex dump above.
 |
 \- Start of disklabel on disk.

We're going to write these fields in the partition table (this is already little endian):

  • p_size = d204 0000 for 1234 sectors
  • p_offset = 2e16 0000 for offset 5678
  • p_fstype = 21, according to the table in disklabel.h

We also need to update d_npartitions to 9. All the partitions in between remain unused.

I'll be overwriting d_checksum with four zeros, so that we can just run the gawk snippet above to calculate the new checksum. Lo and behold, it is: 160f (little endian).

These are the bytes that I touched:

bine-edit.png: Screenshot of my hex editor, pink highlights show which bytes I changed.

Let's boot the VM again and see what we got:

netbsd# disklabel ld1
# /dev/rld1:
type: ld
disk: ld1
label: My Cool Disk
flags:
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 16
sectors/cylinder: 1008
cylinders: 260
total sectors: 262144
rpm: 3600
interleave: 1
trackskew: 0
cylinderskew: 0
headswitch: 0           # microseconds
track-to-track seek: 0  # microseconds
drivedata: 0

9 partitions:
#        size    offset     fstype [fsize bsize cpg/sgs]
 a:    262144         0     4.2BSD      0     0     0  # (Cyl.      0 -    260*)
 d:    262144         0     unused      0     0        # (Cyl.      0 -    260*)
 i:      1234      5678        ZFS                     # (Cyl.      5*-      6*)
disklabel: partitions a and i overlap

Looks good!

(The partitions obviously overlap, because a was already the entire disk.)

Conclusion

This was a fun afternoon and I indeed feel more comfortable with disklabels now. As usual, I really have to have a hands-on session like this in order to grow familiar with something.

As you can see, I marked some fields with (?), because their meaning isn't entirely clear to me yet. Maybe more on that some other day (or maybe not).