When working on a project, there are times where you do not want to rely on some piece of software that a person wrote years ago and since has been deprecated - this was pretty much the case when I took up a project with some others at university. The goal was to write some software that would take a compiled assembly file and convert it into a bootable ISO file.

A portion of the software (specfically, what this post covers) is available on my GitHub repo - this post also goes into a bit more detail about how it was created and the theory behind everything.

To preface, this is a very barebones utility and only covers the El Torito specification.


The Start

To have some material to work with, I made a bootable ISO using some existing tech - NASM and MagicISO.

The steps are:

  1. Make a text file, I’ll name mine example.txt, and put this assembly code in there
mov ah, 0x0e
mov al, 'C'
int 0x10
jmp $

We’ll know it works when the system boots it and prints the character C to the screen.

  1. Run this text file through NASM: nasm -f bin example.txt -o example.bin

  2. Make the ISO using whatever utility you want to use, in this case I’m using MagicISO

  3. Check if a virtual (or physical) machine is able to boot using the ISO file

Examining & Understanding

For the rest of the writeup, I’ll be using HxD to have a look at the files, but any hex editor will do just fine.

If you open the freshly baked ISO using a hex editor, you’ll see a whole lot of zeroes until you scroll to address 0x8000 – according to the documentation, everything before this address is classed as ‘system reserved space’ though it seems that you can still write and use stuff in there anyways.

It’s important to note that ISO files have sectors which can be defined within the ISO itself, however for CD/DVD ones they are 0x800 / 0d2048 bytes large by default.


Sector 16 - 0x8000 to 0x87FF

sector 16

We’ve stumbled upon the Primary Volume Descriptor, which according to the osdev wiki contains a lot of information for reading the rest of the file but at the moment we’re just interested in the bare minimum information required to get it to boot.

To understand what exactly is going on, lets compare the data we have with the table from osdev wiki:

Partial Primary Volume table from the osdev wiki

Partial Primary Volume table from the osdev wiki

From this we can see that we only really need to worry about setting the proper Type Code, Standard Identifier and Version - the rest seems to be extra data that would help to know but isn’t 100% necessary - of course it’s there for a reason, but we’ll gloss over it for now.

Interesting to note that MagicISO sets the Volume Identifier to the date & time the ISO file was created, even though there are options later where that can be specified - not sure why it’s done this way but it doesn’t seem to make any difference.

Sector 17 - 0x8800 to 0x8FFF

sector 17

This is the Boot Record Volume Descriptor - from what I can tell, this is how the computer knows that the ISO is bootable and it also contains a pointer to the start of the boot catalog which is pretty handy.

Boot Record Volume Descriptor from the documentation

Boot Record Volume Descriptor from the documentation

Another important note is that the absolute pointer is encoded as a little endian value, so since it is a double-word it takes up 4 bytes like so: 1A 00 00 00, but due to the encoding, it’s actually 0x0000001A. To work out where the pointer ends up, multiple the value by the sector size, in our case it’s 0x800 / 0d2048 - for example: 0x0000001A * 0x800 = 0xD000 – this is an important value to remember as we’ll come back to it!!

Sector 18 - 0x9000 to 0x97FF

This seems to be a supplementary volume descriptor - similar to the primary one as we saw previously but with a different Type Code - I think that this is relevant for the Joliet expansion, but I’m not interested in it at the moment so we’ll skip it for now.

Sector 19 - 0x9800 to 0x9FFF

sector 19

Nice and simple, this is the Volume Set Terminator - this is placed at the end of the entire block of volume descriptors meaning that it wont always be in the same sector between two different ISOs.

Volume Set Terminator as described on the osdev wiki

Volume Set Terminator as described on the osdev wiki

Sector 20 to 25 - 0xA000 to 0xCFFF

Sector 20 & 21 contain some information, but I haven’t been able to figure out exactly what it is. Apart from this, the other sectors are empty. we’ll skip this section too.

Sector 26 - 0xD000 to 0xD7FF

Remember that value from earlier? Well this is the address it was pointing to, so what’s here?

sector 26

This is described as the Validation Entry alongside the Initial/Default entry - they’re pretty important so lets have a look at them.

Validation entry - 0xD000 to 0xD01F

Validation Entry from the documentation

Validation Entry from the documentation

The Validation Entry only spans from 0xD000 to 0xD01F, and with the help of the table, it is pretty easy to understand. I believe that the Checksum Word will always be zero, as the offset 0x2 to 0x3 will also always be zero.

Initial/default entry - 0xD020 to 0xD02F

Initial/Default Entry from the documentation

Initial/Default Entry from the documentation

There’s a handful of entries in the table that I dont quite understand but I’ll try to do my best to explain them:

  • Boot Indicator is a flag which represets whether the data which the entry points to is bootable or not.
  • Boot Media Type is an indicator to the computer whether or not the image should be emulated as a specific type of media.
  • Load Segment refers to the memory position that the computer jumps to once it has finished powering on, as described here and here.
  • System Type - this one escapes me, because I’m not sure what it means by the “partition table found in the boot image”. The image seems to work just fine if the byte is set to zero, so that is what I have done in my implementation.
  • Sector Count tells the computer how many segments to store in memory, however according to this source, a bootloader is 512 bytes long and since our sectors are 2048 bytes long, we should be fine with setting this value to 0x1.
  • Load RBA is how we actually know where the data is - if we do the same calculation we did before to workout the address of this sector, we’ll get the address for the data sector like so (remember that it’s little endian encoding): 1B 00 00 00 becomes 0x0000001B, so 0x0000001B * 0x800 = 0xD800, so 0xD800 is the sector where our data begins.

Sector 27 - 0xD800 to 0xDFFF

sector 27

Oh look! That’s our data. This is the assembly code from that BIN file we created at the beginning - if the ISO is created properly, then the machine which runs it should simply print the character C to the console. There isn’t any fancy entries or formatting for this, it’s literally your code now.


Does it work?

Yeah! Here it is working on Hyper-V:


…and the same ISO on ProxMox: