This tutorial will walk through an x86 BIOS bootloader that will display a trans flag (or any banded flag you'd like) on VGA graphics. This should work on any PC system with VGA compatible graphics, however your experience may vary. Since this is for x86 PC BIOS and not for UEFI, this may require enabling a "legacy boot" option on some newer systems.
Disclaimer: Although unlikely to occur, systems are odd and behavior is sometimes weird. I take no responsibility for any damage you do to any device, software, or anything else you do by following this tutorial. You perform all operations and run all code at your own risk.
Tutorial displaying on my 2006 Portege M400 laptop:
This tutorial assumes you are familiar on some level with programming and computer operation, and will not be covering basic concepts such as memory or machine code.
The boot process on x86 PCs is quite simple: the first 512 bytes of your selected boot drive is loaded into memory at address 0x7C00, and begins to run from that point. That's it. Everything else - kernels, partition tables, filesystems, OSes, etc have to be built up from this point, and are beyond the scope of this tutorial.
So what kind of environment are you in at this point? When booting, you start out in what's known as Real Mode, a basic 16-bit mode which all x86 processors support. This was the only mode on the first 16-bit x86 processors, and what all x86 processors begin execution in since the very first IBM PC. Memory addressing within Real Mode follows an important scheme which is not found in more modern programming. Addresses are 20-bit, arranged in 16-bit segments. These addresses are stored in two 16-bit registers, the segment register allowing storage of the upper 4-bits, and the second register storing the 16-bit offset within that segment. The segment register stores these extra bits by multiplying its value by 0x10 to get the hardware address, or shifting left one place value in hex. This segment and offset combination is most often written SEG:OFF. For example, with a segment of 0x1000 and an offset of 0x2000, written 1000h:2000h the hardware address will be (0x1000 * 0x10) + 0x2000 = 0x12000 - notice that the segment can overlap with the address "space" of the offset e.g. 3100h:2000h -> (0x3100 * 0x10) + 0x2000 = 0x33000. This kind of split addressing can be difficult to get used to, so keep it in mind when programming for real mode.
How can we do graphics? There's no C functions to call or graphics libraries to use, we have to interact with BIOS and the VGA card. In VGA, your computer runs in one of many graphics modes, different setups for the computer to be able to write to the screen. Text modes such as mode 03h allow you to write plain text to the screen just by setting ASCII values in memory. Graphics modes such as mode 13h allow manipulation of pixels, and this is what we will be using for this tutorial. In mode 13h, the screen has a resolution of 320x200, and each pixel is one byte, each pixel being one of 256 different colors. While this may seem limiting, VGA allows setting each color’s exact RGB value, to any 18-bit RGB color. This strategy of pixel values representing a color whose value is set elsewhere rather than each pixel having RGB values is known as indexed color, and along with being used for VGA graphics is also used for the GIF format among other formats, though indexed color is not commonly used in the present. VGA screen memory is mapped to hardware address 0xA0000, so changing what’s on screen just requires writing color values to this region. The memory is laid out left to right and top to bottom, 320*200 = 64000 bytes in total. Since the address 0xA0000 is larger than the maximum of a 16-bit pointer (0xFFFF), we use a segment and offset to access this address - A000h:0000h. Setting the first pixel on the screen is as simple as setting A000h:0000h to your desired color. So we have 256 color indices available, and can set them to whatever RGB colors we'd like - how do we do that? And how do we get into this graphics mode, anyway? The answer to both is the same - BIOS interrupts.
BIOS interrupts are a sort of low level PC function only accessible in real mode, that are called by setting CPU registers as arguments and then calling an interrupt. The two that will be used in this tutorial are INT 10,0 - Set Video Mode - and INT 10,10 - Set/Get Palette Registers, specifically the AL=12 "set block of DAC color registers" subfunction. These two interrupts will allow us to do everything necessary for this program. For a more complete list of interrupts, see this online list of BIOS and DOS interrupts, or the very extensive Ralf Brown's Interrupt List.
So now we know the three steps to drawing a flag in our boot sector:
- 1. Switch to VGA mode 13h
- 2. Set VGA colors we want
- 3. Write flag design to memory at A000h:0000h
For proper assembly, we need to tell our assembler that our code starts at 0x7C00 - see the x86 Booting section - and that we want 16-bit code, required for Real Mode. In NASM syntax, the following directives at the top of the file will achieve this:
[BITS 16] [ORG 0x7C00]
Getting Mode 13h
BIOS interrupt 10h AH=0h is used to switch video modes, called with int 0x10 when AH is set to 0. The AL register is the argument for the interrupt, containing the desired mode. Since AH is the high byte of AX, we can simply set AX to the desired mode, as it will always then set the upper byte of AX to 0 i.e. AX = 0x0013. After setting AX, we simply call interrupt 10h.
mov ax, 0x13 ; AX = 0013h int 0x10
Setting VGA colors
Getting our VGA colors set properly is more complicated than the VGA mode setting code, but still is only a few lines of assembly. The interrupt we will use, int 10h AH=10h AL=12h, sets multiple VGA DAC colors at the same time, from a list of RGB values. Although the color values are 18-bit, it expects the RGB values in one byte for each color, with the final 6 bits of each color determining the final color. For the trans flag, I used these values, though of course you can choose any colors you'd like (converting more standard 24-bit RGB to 18-bit RGB is left as an exercise to the reader):
16 33 3e 3d 2a 2e 3f 3f 3f
Our BIOS interrupt takes the value of the first color we'd like to set in BX - here we'll go with 0, the count of colors we'd like to set in CX, and a pointer to the set of colors we'd like in ES:[DX] - ES is the segment, DX is the offset, real mode memory segmentation. To make the later code simpler, instead of only setting 3 colors, we'll set each of the 5 bands of our flag to its own color - no rule says you can't set two colors to the same RGB value!
To define an array of bytes with the color values in NASM, place a label plus the db directive and your bytes like the following, placing and keeping it at the end of your code as not to accidentally get interpreted as CPU instructions:
colors: db 0x16, 0x33, 0x3e, 0x3d, 0x2a, 0x2e, 0x3f, 0x3f, 0x3f, 0x3d, 0x2a, 0x2e, 0x16, 0x33, 0x3e
Setting our BX to 0 and our CX to the count of colors is simple, and just can use mov instructions:
mov bx, 0 mov cx, 5
Finally, the pointer in ES:[DX]. To set the offset DX, simply move the label you created for the color array into DX. The ES (extra segment) register should be set to the same location as your CS (code segment), since the values we are fetching are within the same area as our code. Though many PCs have these equivalent on boot, some do not, so it is a good idea to ensure ES is equal to CS. Your full code should now look something like this:
[BITS 16] [ORG 0x7C00] mov ax, 0x13 int 0x10 ; switch to mode 13h mov ax, cs ; cannot copy from CS to ES directly, mov es, ax ; so use AX as an intermediate mov ax, 0x1012 ; select the right interrupt mov bx, 0 ; start with index 0 mov cx, 5 ; 5 colors total mov dx, colors ; set offset int 0x10 ; set VGA DAC registers from ES:[DX] colors: db 0x16, 0x33, 0x3e, 0x3d, 0x2a, 0x2e, 0x3f, 0x3f, 0x3f, 0x3d, 0x2a, 0x2e, 0x16, 0x33, 0x3e
We now both switch graphics modes and set the proper VGA colors! All that's left is to write our design to the screen.
Drawing the flag
At the heart of our drawing routine is a powerful instruction - rep stosb. This instruction may look intimidating to beginners, but is quite simple. The rep keyword means that this instructions is going to repeat an amount of times equal to the value of CX, a single instruction loop. stos stores a value from the AX/AL register to the memory location ES:[DI]. The b at the end means this is the byte version rather than the word version (w), meaning we're writing one byte at a time, CX times, from AL to ES:[DI]. This instruction increments DI with the writes, so we don't have to worry about incrementing that ourself.
So before this instruction we need to 1. Set the ES:[DI] pointer to the VGA memory at A000h:0000h, 2. Set CX to the amount of bytes to write, 3. Set AL to the correct color value. Since our colors are all in sequence, and start at 0, we can do this with a loop with AX, stopping when we've written all the colors we want, and with CX simply writing the amount of bytes in each band - 320 times the amount of lines each should cover. For the trans flag with 5 bands, this comes out to 12800 bytes per band. Setting the pointer and doing the loop may look something like this:
mov ax, 0xa000 ; cannot set ES directly, mov es, ax ; so use AX as intermediate mov di, 0 ; offset starts at 0 mov ax, 0 ; start with color 0 loop: mov cx, 12800 ; 12800 bytes per band rep stosb ; write CX bytes of AL inc ax ; increment color AL cmp ax, 5 ; compare AL to 5 jl loop ; if less than 5, jump to loop start
That's it! With this code, the operation is now finished, and the flag with the proper colors will be displayed on screen. However, there's a small part to add, a way to stop executing the program. If the program continues, it will interpret our colors at the end as instructions, and continue execution past those, to unpredictable consequences. To prevent this, we'll simply add an infinite loop after our draw.
The final code then should look something like this:
[BITS 16] [ORG 0x7C00] mov ax, 0x13 int 0x10 ; switch to mode 13h mov ax, cs ; cannot copy from CS to ES directly, mov es, ax ; so use AX as an intermediate mov ax, 0x1012 ; select the right interrupt mov bx, 0 ; start with index 0 mov cx, 5 ; 5 colors total mov dx, colors ; set offset int 0x10 ; set VGA DAC registers from ES:[DX] mov ax, 0xa000 ; cannot set ES directly, mov es, ax ; so use AX as intermediate mov di, 0 ; offset starts at 0 mov ax, 0 ; start with color 0 loop: mov cx, 12800 ; 12800 bytes per band rep stosb ; write CX bytes of AL inc ax ; increment color AL cmp ax, 5 ; compare AL to 5 jl loop ; if less than 5, jump to loop start jmp $ ; infinite loop colors: db 0x16, 0x33, 0x3e, 0x3d, 0x2a, 0x2e, 0x3f, 0x3f, 0x3f, 0x3d, 0x2a, 0x2e, 0x16, 0x33, 0x3e
Now the fun and dangerous part: compiling your code, writing to a disk, and running it! These instructions will be for generic Linux systems, but similar methods should apply to any system.
Compilation with NASM is extremely easy for this code: simply run NASM with it as an argument, no flags required! (you could do -o to choose an output filename here, by default NASM will use your file's name minus extension)
$ nasm flag_boot.nasm
Writing to a drive
Here's the part where you want to be careful, writing directly to the boot sector of a drive will damage any bootable system already present, do not run this onto an already bootable drive - or really any drive you care about data on unless you know what you're doing. I recommend using the following commands to check the boot sector of the drive you're writing to for anything important - hexdump will display a hex output with ASCII, objdump will display a disassembly, though ideally you should use a drive you're not worried about losing data on:
# dd bs=512 count=1 if=/dev/DISK of=FILE $ hexdump -C FILE $ objdump -D -bbinary -mi8086 FILE
If the first 100 bytes or so of the drive are blank and it doesn't look like there's any executable code in there, it should be fine to write to. Be careful! You don't want to write something you can't take back (Checking here also provides FILE as a backup of the drive's original boot sector, just in case). Look out for data near the end of the 512 byte file, that's where the MBR of the disk is located so you don't want to accidentally overwrite that. Make sure your assembled binary from NASM won't overwrite any data that matters.
To actually write the boot code to the drive, simply use dd (it might be a good idea to add a bs and count to limit how much you can write, just in case). Be careful that you're writing the assembled binary file, not the text assembly code, and that you're writing to the disk itself (e.g. /dev/sde), not a partition (e.g. /dev/sde1). Don't fuck up your important drives, please, double and triple check you're writing to the right device.
# dd if=flag_boot of=/dev/DISK
That's it! If you've followed this correctly, you should have a disk that will boot right to the design of your choice! Since this only occupies the boot sector, you should be able to write this to a drive and still have that drive function as a normal storage device. I hope you've come away with some new knowledge of x86 and PC operation, or at least a bit of fun!