16 March 2026

Hello!

Recently MOP3 has received BIG updates: a PCI subsystem and an IDE driver, so now we can write to persistent storage and keep our files there! How awesome? Because of that, I needed to have a userspace utility app to manage drive partitions and filesystems. The app is called sdutil and it’s code can be found here: https://git.kamkow1lair.pl/kamkow1/mop3/src/branch/master/sdutil. It’s a very simple app, similar to old-school fdisk.

sdutil explained

sdutil or "storage device utility" is an app to manage filesystems and partitions on a device. Right now it supports DOS partitions/MBR, but I plan to add GPT in the future too!

Testing sdutil on real hardware

I always make sure everything works by testing my code on a real machine. QEMU is nice for quick, iterative development, but you have to run you code in a not so nice and sterile environment. That’s how some bugs come up, which you’d never encounter in an emulator.

To test sdutil I’d run the following sequence of commands:

  • List DOS partitions (should be empty)

  • Create 1 DOS partition (from LBA 1, to the end) and leave the rest uninitialized

  • Reboot, because partition rescan is unimplemented yet

  • Check if the kernel has picked up the new partitions

  • Format the partition as FAT32

  • Mount as a volume and test createing/editing/deleting files

  • Reboot and see if the changes persist

After step 2, I could NOT BOOT into my machine! WHAT???

Yeah…​

I could not boot, because there was no boot menu. It was simply gone! Here’s a photo (excuse the bad quality):

Missing boot menu ??????

How does the boot menu disappear?

So after some digging in my code and debugging, I’ve figured it out…​

The BIOS scans available devices, so that it can populate the boot menu with various options to boot from. It will see a SATA drive and so it will try to get it’s first sector and parse it as a Master Boot Record to find other bootable partitions. This is why GPT still has a legacy MBR btw. The issue here was that the American Megatrends' BIOS would try to parse my faulty MBR and instead of failing and just skipping the device, it would just hang before rendering the boot menu. WHY?? I don’t know, go ask AT.

Cleaning up the mess

So what’s the solution? My machine is bricked!

Well…​ Not quite. It is bricked as in the BIOS software breaks when parsing the MBR, but we can unbrick the PC if we manage to wipe the MBR out.

I had to carefuly remove the 32GB M.2 drive. Here’s what it looks like:

M.2 Drive

It’s well past february of 2026, so we’re fine ;).

Then I had to go to a local electronics shop and buy an M.2 SATA → USB adapter. The plan is to simply plug the M.2 drive like an USB stick into my dev machine and use dd to wipe it out.

130 PLN/ZŁ later I have this:

The adapter

The adapter

Now we can clear the drive with dd. It takes like 10 minutes to do so.

And now WE CAN FINALLY HAVE THE BOOT MENU!! LET’S GOOOO

Happiness

So why was the MBR bad?

When writing sdutil I’ve made a big and bold assumption that a modern BIOS would not care about CHS (Cylinder-Head-Sector) related fields of partition table entries (PTEs). I was only initializing LBA-related fields and it worked fine on QEMU, which uses SeaBIOS.

struct dos_pte {
  uint8_t drive_attrs;
  uint8_t chs_start_addr[3]; /* <-- uninitialized / zero */
  uint8_t part_type;
  uint8_t chs_last_sect_addr[3]; /* <-- uninitialized / zero */
  uint32_t start_lba; /* <-- initialized properly */
  uint32_t sector_count; /* <-- initialized properly */
} __attribute__ ((packed));

struct dos_mbr {
  uint8_t boot_code[440];
  uint8_t signature[4];
  uint8_t resv[2];
  struct dos_pte ptes[4];
  uint8_t valid_sign[2];
} __attribute__ ((packed));

What I did to fix this, was basically I’ve set up the CHS fields to mean the same as LBA. Here’s a simple conversion function in C:

static void lba_to_chs (uint32_t lba, uint8_t chs[3]) {
  uint32_t sectors_per_track = 63;
  uint32_t heads = 255;

  uint32_t cylinder = lba / (heads * sectors_per_track);
  uint32_t head = (lba / sectors_per_track) % heads;
  uint32_t sector = (lba % sectors_per_track) + 1;

  if (cylinder > 1023) {
    chs[0] = 254;
    chs[1] = 0xFF;
    chs[2] = 0xFF;
  } else {
    chs[0] = (uint8_t)head;
    chs[1] = (uint8_t)((sector & 0x3F) | ((cylinder >> 2) & 0xC0));
    chs[2] = (uint8_t)(cylinder & 0xFF);
  }
}

And now we just have to pass the CHS fields into this function. Now everything works! YAY!

Wrapping up

So what’s the conclusion for to day?

  1. Be careful when working with American Megatrends' BIOSes

  2. Be ready to spend money to save your drives

  3. Don’t assume that legacy fields are irrelevant