This article is written for everyone interested in design of different things, particularly, developers working with high-level languages (Java, C, C++, etc.), who sometimes need to do low-level programming in Windows. Our example of low-level programming is based around system loading, i.e. we will show how you can develop a bootloader.

We will devote part of our bootloader programming tutorial to describe what happens after a computer is turned on (the process of system loading). After introducing these theoretical aspects, we will walk you through the process of writing a piece of software that initiates system booting process, so that you can personally create bootloader.

 

Contents:

1. About bootloaders

2. Diving deeper

  1. Picking up language to develop bootloader
  2. Selecting compilers
  3. System booting process

3. Implementation

  1. Architecture overview
  2. Setting up environment
  3. BIOS interruptions and screen cleaners
  4. “Mixed code” technique
  5. CString implementation
  6. CDisplay implementation
  7. Types.h implementation
  8. BootMain.cpp implementation
  9. StartPoint.asm implementation

4. Assembling

  1. COM file creation
  2. Automation of assembly process

5. Demonstration and testing

  1. Testing bootloader
  2. Testing on virtual machine
  3. Testing on physical machine

6. Debugging

7. Short Summary

8. Additional references

 

1. About bootloaders

A bootloader is a piece of software located in the first sector (also known as the Master Boot Record) of a hard drive, where system booting starts. This is the place where BIOS comes in: when a machine is powered up, it reads the data contained in the first sector and processes it to the system memory. However the first sector doesn’t have to be the boot sector, as it is more of a historical legacy, which developers have preserved till present days; so for now we will stick to the first sector as well.

2. Diving deeper

This section describes the expertise and software required to develop your own bootloader. We will also drop a few words about system booting.

2.1 Picking up language to develop bootloader

During the initial stages of computer operation, BIOS takes control over the machine hardware via the functions called interruptions (see the link at the end of the article to learn more about interruptions). Knowing at least some basics of Assembler would be a great plus as implementation of interruptions is available only in Assembler. However, it is not a requirement and here’s why: our “mixed code” technique allows mixing low-level language commands and high-level constructions, which simplifies our task (we will talk more about optimizing our work further in the article).

Our bootloader tutorial is primarily based on C++ low-level programming. It is not a surprise that low-level programming in C has gained much popularity, so being an expert in C will allow you to learn the elements of C++ pretty quickly. Generally speaking, your expertise is enough, but in this case it wouldn’t be possible to use the coding examples given in this article as is and you will need to modify them for bootloader programming in C.

Java and C# will not be fit to the task, unfortunately, as they produce intermediate code after compilation. And in addition, a special virtual machine is used to perform conversions from intermediate code into language understood by a processor. So the code execution becomes possible only after a conversion, which doesn’t allow to take advantage of the “mixed code” technique; and without it, our task becomes much more complicated.

So in summary, what you need is the knowledge of C or C++ and, if possible, at least basics of Assembler.

2.2 Selecting compilers

To use the advantages of the “mixed code” technique, we require no less than two compilers. The first compiler is the core compiler that will be used for Assembler and C or C++. The second compiler is a linker: its task is to join the *.obj files to create a single executable file.

Let’s discuss the details. A processor functions in the 16-bit real mode, which has certain limitations, and in the 32-bit safe mode with the full available functionality. On the start, that is when the system is powered on, a processor operates in a real mode, that is why building a program and creating an executable file requires a compiler and a linker for Assembler that work in the 16-bit mode. A C or C++ compiler is required only to create *.obj files in the real mode.

Please note that the latest compilers are not suitable for our task as they are designed to run in a safe mode (32-bit) only.

Out of all 16-bit compilers I tested, both free and paid ones, my choice fell on Microsoft’s products, which I used to build all low-level language code examples and other cited code. The package for Microsoft Visual Studio 1.52 contains what we need: a compiler and a linker for Assembler, C, and C++. You can visit the company’s official site to get the package.

Here are the linkers and compilers I’ve selected to make your search process easier:

Assembler Compiler

ML 6.15

16-bit compiler by Microsoft

DMC

free compiler by Digital Mars

 

TASM

16-bit compiler by Borland

C/C++ Compiler

CL

16-bit compiler

BCC 3.5

16-bit compiler by Borland

Linker

LINK 5.16

16-bit linker for creation of *.com files

LINK

free linker designed to work with the DMC compiler

TASM

16-bit linker for creation of *.com files by Borland

All examples of low-level language code and other code pieces in this article were built with the Microsoft tools.

2.3 System booting process

To get a clearer picture of what we need to do, we need to understand the system booting process.

Let’s look at the diagram illustrating the interaction of system components during this process:

Bootloader Tutorial: System booting Process

  1. BIOS reads the first sector of the hard disk drive.
  2. BIOS passes the control to Master Boot Record located at the address 0000:7c00, which triggers the OS booting process. More information about the structure of the Master Boot Record is available here.

3. Implementation

The following section of the article is devoted to low-level programming. We will concentrate directly on bootloader programming to develop our own bootloader.

3.1 Architecture overview

The source code for bootloader cited here is solely for training purposes. Its structure is rather simple and it has the following functions:

  1. Perform the loading from the address 0000:7c00 to the system memory.
  2. Call the BootMain function, which was written using a high-level language.
  3. Display a simple “Hello world” message on the screen.

See the following image accompanied by description for the architecture of the program:

Windows Bootloader Structure

The first element is StartPoint. It is written in a low-level language. As high-level languages lack the required instructions, this element is created using Assembler only. Its task is to instruct the compiler to use specific memory model and list the address at which the loading to RAM must be performed after data from a disk was read. In addition, it fixes processor registers. After its role has been fulfilled, it eventually passes the control over to BootMain, an element written in high-level language.

BootMain takes control right after StartPoint. It is an entity similar to main, which is a primary function in which all program operations take place.

And finally, the CDisplay and CString come in. They fulfill the role of the final actors displaying the message. As you can see from the diagram, they are not equal, as CDisplay uses CString

3.2 Setting up environment

The good news is that for our bootloader development task we do not require anything besides standard Microsoft Visual Studio 2005/2008. Other tools will do as well, and you can use them if it is more convenient for you, but with a few adjustments, one of the mentioned tools make our lives easier during compilations. It will be also easier to follow the tutorial if you use one of them.

To start with, we need to create a project using the Makefile Project template. Most of the work we do will be performed here.

Click File > New > Project > General and select Makefile Project. Click OK.

Creating a custom bootloader

3.3 BIOS interruptions and screen cleaners

Before displaying any messages, first of all, the screen must be cleared. BIOS has special interruptions for this task.

BIOS provides various interruptions that allow interacting with computer hardware (input devices, disk storages, audio adapters, and so on). The structure of an interruption is as follows:

int [number_of_interrupt]; 

Here the number_of_interrupts is the interruption number.

Before you call an interruption, you must first define its parameters. The ah processor register contains the function number for an interruption, while the rest of the registers store other parameters of the current operation. Now we will consider how the int 10h interruption works in Assembler. For this purpose, we will need the 00 function to change the video mode, which will result in a clear screen:

mov al, 02h ; here we set the 80x25 graphical mode (text)
mov ah, 00h ; this is the code of the function that allows us to change the video mode
int 10h   ; here we call the interruption 

We are interested only in interruptions and functions that we will need for our application, these are:

int 10h, function 00h – this function changes the video mode and thus clears the screen;
int 10h, function 01h – we use this function to set the type of the cursor;
int 10h, function 13h – this function concludes the whole routine by displaying a string of text on the screen;

3.4 «Mixed code» technique

One of the advantages of the C++ compiler is that it has inbuilt Assembler, which allows you to use a low-level language when you write something in a high-level language. Assembler instructions written in high-level code are called asm insertions, you will recognize them by the introductory word _asm followed by a block of Assembler instructions enclosed in braces. Here is an example of low-level language code insertion:

__asm ;  this is a keyword that introduces an asm insertion
  { ;  the beginning of a block of code
  … ; some asm code
} ;  the end of the block of code

Now we combine the C++ code with the Assembler code that clears the screen to illustrate this technique.

void ClearScreen()
{
 __asm
{
 mov al, 02h ; here we set the 80x25 graphical mode (text)
mov ah, 00h ; this is the code of the function that allows us to change the video mode
int 10h   ; here we call the interruption
}
}

3.5 CString implementation

The CString class works with strings. The value of the string it contains will be used by the CDisplay class. There is the Strlen() method, which gets a pointer to a string as its parameter, counts the number of characters the obtained string contains, and returns the resulting number:

// CString.h 
#ifndef __CSTRING__
#define __CSTRING__
#include "Types.h"
class CString 
{
public:
    static byte Strlen(
        const char far* inStrSource 
        );
};
#endif // __CSTRING__
// CString.cpp
#include "CString.h"
byte CString::Strlen(
        const char far* inStrSource 
        )
{
        byte lenghtOfString = 0;
        
        while(*inStrSource++ != '\0')
        {
            ++lenghtOfString;
        }
        return lenghtOfString;
}

3.6 CDisplay implementation

As its name states, this class is developed to interact with the screen. It consists of the following methods:

  • The ShowCursor () method: This method controls the cursor manifestation on the display. It has two values: show (enables the cursor manifestation) and hide (disables the cursor manifestation).
  • The TextOut () method: This method simply produces the text output, i.e. displays a string on the screen.
  • The ClearScreen () method: This method clears the screen by the means of changing the video mode.
 
// CDisplay.h
#ifndef __CDISPLAY__
#define __CDISPLAY__
//
// colors for TextOut func
//
#define BLACK			0x0
#define BLUE			0x1
#define GREEN			0x2
#define CYAN			0x3
#define RED				0x4
#define MAGENTA			0x5
#define BROWN			0x6
#define GREY			0x7
#define DARK_GREY			0x8
#define LIGHT_BLUE		0x9
#define LIGHT_GREEN		0xA
#define LIGHT_CYAN		0xB
#define LIGHT_RED		      0xC
#define LIGHT_MAGENTA   	0xD
#define LIGHT_BROWN		0xE
#define WHITE			0xF
#include "Types.h"
#include "CString.h"
class CDisplay
{
public:
    static void ClearScreen();
    static void TextOut(
        const char far* inStrSource,
        byte            inX = 0,
        byte            inY = 0,
        byte            inBackgroundColor   = BLACK,
        byte            inTextColor         = WHITE,
        bool            inUpdateCursor      = false
        );
    static void ShowCursor(
        bool inMode
        );
};
#endif // __CDISPLAY__
// CDisplay.cpp
#include "CDisplay.h"
void CDisplay::TextOut( 
        const char far* inStrSource, 
        byte            inX, 
        byte            inY,  
        byte            inBackgroundColor, 
        byte            inTextColor,
        bool            inUpdateCursor
        )
{
    byte textAttribute = ((inTextColor) | (inBackgroundColor << 4));
    byte lengthOfString = CString::Strlen(inStrSource);
    __asm
    {		
        push	bp
        mov		al, inUpdateCursor
        xor		bh, bh	
        mov		bl, textAttribute
        xor		cx, cx
        mov		cl, lengthOfString
        mov		dh, inY
        mov		dl, inX  
        mov     es, word ptr[inStrSource + 2]
        mov     bp, word ptr[inStrSource]
        mov		ah,	13h
        int		10h
        pop		bp
    }
}
void CDisplay::ClearScreen()
{
    __asm
    {
        mov     al, 02h
        mov     ah, 00h
        int     10h
    } 
}
void CDisplay::ShowCursor(
        bool inMode
        )
                                 
{
    byte flag = inMode ? 0 : 0x32;
    __asm
    {
        mov     ch, flag
        mov     cl, 0Ah
        mov     ah, 01h
        int     10h
    }
}

3.7 Types.h implementation

The Types.h header file is a definition container for data types and macros.

 // Types.h
#ifndef __TYPES__
#define __TYPES__     
typedef unsigned char   byte;
typedef unsigned short  word;
typedef unsigned long   dword;
typedef char            bool;
#define true            0x1
#define false           0x0
#endif // __TYPES__

3.8 BootMain.cpp implementation

The BootMain() function serves as a starting point of the program and is its main function. This is where main operations take place.

// BootMain.cpp
#include "CDisplay.h"
#define HELLO_STR               "\"Hello, world…\", from low-level..."
extern "C" void BootMain()
{
    CDisplay::ClearScreen();
    CDisplay::ShowCursor(false);
    CDisplay::TextOut(
        HELLO_STR,
        0,
        0,
        BLACK,
        WHITE,
        false
        );
    return;
}

3.9 StartPoint.asm implementation

;------------------------------------------------------------
.286							   ; CPU type
;------------------------------------------------------------
.model TINY						   ; memory of model
;---------------------- EXTERNS -----------------------------
extrn				_BootMain:near	   ; prototype of C func
;------------------------------------------------------------
;------------------------------------------------------------   
.code   
org				07c00h		   ; for BootSector
main:
				jmp short start	   ; go to main
				nop
						
;----------------------- CODE SEGMENT -----------------------
start:	
        cli
        mov ax,cs               ; Setup segment registers
        mov ds,ax               ; Make DS correct
        mov es,ax               ; Make ES correct
        mov ss,ax               ; Make SS correct        
        mov bp,7c00h
        mov sp,7c00h            ; Setup a stack
        sti
                                ; start the program 
        call           _BootMain
        ret
        
        END main                ; End of program

4. Assembling

4.1 COM file creation

So after the development of boot loader code is done, it is time to convert it to a file, which will be able to work on a 16-bit OS – this is a *.com file. Any compiler for Assembler or C/C++ can be started from the command line. After that we pass the required parameters to compilers; as a result, we receive object files. Then a linker comes in. We use it to merge the object files we received into a single executable file. The resulting file is a *.com executable file. This approach works stably, however it is not that easy.

To make our lives easier, we can make this process automatic. This step will not take much efforts as it simply requires us to create a *.bat file with all necessary commands and parameters. The whole process of application assembling looks as follows:

How to create Bootloader: Assembling process

Build.bat

The compilers and the linker must be placed to the folder where the project is saved. In this folder, we need to place a *.bat file with the following content (the name of a folder with the compilers and the linker – V152 – can be replaced with any other name, but the rest of the content must remain unchanged):

.\VC152\CL.EXE /AT /G2 /Gs /Gx /c /Zl *.cpp
.\VC152\ML.EXE /AT /c *.asm
.\VC152\LINK.EXE /T /NOD StartPoint.obj bootmain.obj cdisplay.obj cstring.obj
del *.obj

4.2 Automation of building process

First of all, we will show how you can turn Microsoft Visual Studio 2005/2008 into your development environment. A great advantage of this environment is that it will be able to support any compiler after we configure it. Now open Project > Properties > Configuration Properties\General > Configuration Type.

In the Configuration Properties section, you will see three pages: General, Debugging, and NMake. Click the NMake page and enter the path to the build.bat file in the Build Command Line and in the Rebuild Command Line boxes of the General section as shown on the screenshot below:

Custom Bootloader Assembling

If you have done everything right, then you can start the compilation as usual by pressing the F7 or Ctrl+F7 hotkey. During the process, all accompanying information is displayed in the Output window. Not only we can automate the process of assembly this way, but we can also navigate in the code errors, if any.

5. Demonstration and testing

In this section, we will discuss the examination of the bootloader in work, its testing, and debugging.

5.1 Testing boot loader

Depending on what is more convenient for you, you can test a boot loader on a physical machine or a specially configured for this task virtual machine – VMware. The advantage of testing on a physical machine is that not only you can be sure that it works, but also check that its performance is good enough; the advantage of testing on a virtual machine is that it is safer and easier to fix, however it only ensures that a boot loader works. Nevertheless VMware is a great tool for testing and debugging. Regardless of what you choose, the description of both methods will be given here.

The first thing we need is a tool that will help us write a bootloader to a virtual or physical drive. There is no shortage of such tools on the Internet. Depending on your needs and resources, you can pick up from a number of free and commercial tools, either console or interface-based ones. My choice for Windows is Disk Explorer for NTFS 3.66 (you can also find a version for the FAT file system) and for MS-DOS I’ve chosen Norton Disk Editor 2002.

I will give only the description of the method using Disk Explorer for NTFS 3.66 as it is the easiest and the best for our task.

5.2 Testing on virtual machine

Step 1: Creating a virtual machine

To perform testing on a virtual machine, we need VMware 5.0 or higher. The minimum size of the disk space of a virtual machine for bootloader testing is 1GB. As we use a tool for NTFS file system, we need to format the allocated space to NTFS accordingly, and then we map the drive to VMware to make it a virtual drive.

To map a drive, go to File > Map or Disconnect Virtual Drives and click the Map button. In the Map Virtual Disk window, define the path to the disk in the File name box and select the drive partition label in the Drive box.

Testing bootloader on virtual machine

Make sure to clear the Open file in read-only mode checkbox. This option prevents data corruption by prohibiting data writing to a disk. The rest of the options may be left unchanged.

This allows us to work with a virtual disk as with a regular Windows logical disk. So finally we can record our bootloader at the 0 physical offset using Disk Explorer for NTFS 3.66.

Step 2: Using Disk Explorer for NTFS

Go to File > Drive and select our virtual drive. In the Select Drive window, expand the Logical Drives group and select the drive with the label you have previously defined.

Write Custom Boot loader to a drive

Select View > As Hex. In the Hex View pane, the 16-bit representation of the disk is displayed. The content is divided by offsets and sectors. As the disk is empty at the moment, there are only 0s.

Write Custom Boot loader to a drive

So let’s write the bootloader to the first sector of the drive. Move the marker to the 00 position as illustrated on the screenshot above. Select Edit > Paste from file, specify the path to the file whose content must be written to the selected position, and click Open. This will copy and paste the bootloader. The content of the first sector changes accordingly (see the screenshot below; unless you made some modifications to the code, the content will be the same).

Now we need to instruct BIOS to identify the first sector as the boot sector and load it to the memory. For this purpose, we need to add the 55AAh signature at the 1FE offset from the beginning of the sector. To do this, press the F2 hotkey to enable the editing mode and write the required characters of the signature at the defined offset. After the editing is finished, press Esc.

Boot loader tutorial: bootloader to a disk

To confirm the writing of data, select Tools > Options. In the Options window, select the Virtual write mode (this mode allows making modifications that will be stored only in memory) and click Write.

This brings us to the end, where all routine actions are finished. Now we get the complete picture of what we have been implementing from the start. Before you do anything else, you need to disconnect our virtual disk in VMware; to do this, go to File > Map or Disconnect Virtual Disks and click Disconnect.

So now we run the virtual machine and behold this moment of triumph when our efforts of writing low-level code result in “Hello world…”, from low-level!” on the screen.

custom bootloader working

5.3 Testing on physical machine

The process of testing a bootloader on a physical machine is almost identical to that of testing it on a virtual machine. The difference here is that while you can fix any error with a virtual machine by just creating a new one, on a real hardware, you need much more time and effort to fix errors if anything goes wrong. If you fear the corruption of existing data, you can also use a flash drive instead of a hard drive. But before you do that, restart your computer, start BIOS, and make sure that your BIOS supports flash drives. If flash drives are supported, than everything is fine, otherwise the only safe solution for you is to perform testing on a virtual machine (as we mentioned earlier, this approach allows ensuring the work of a bootloader just as the physical machine approach if you have no need to check the performance indicator of the bootloader).

To write a bootloader to a flash drive using Disk Explorer for NTFS, you need to perform the same steps as for a virtual machine and select a whole physical drive instead of one of its logical partitions so that writing is performed at a correct offset.

6. Debug

Make sure to have debugging tools for your bootloader in case something goes wrong (and it’s a common case). But be warned that this process is time-consuming, tiresome, and very complicated, so be ready to spare the same amount of time debugging as you spent developing the bootloader. You will need to dive deep into the machine code of Assembler. This is the only case where good knowledge of Assembler is obligatory. In any case, the following tools will come in handy for this task:

  • Turbo Debugger: Great debugging solution for 16-bit mode by Borland
  • CoveView: Nice 16-bit debugger by Microsoft
  • D86: Good 16-bit debugger by Eric Isaacson, a programmer experienced in Assembler development for Intel
  • Bocsh: Virtual machine program emulator with inbuilt debugger for machine commands.

7. Short summary

We have considered the following things in this article: what is a bootloader, how BIOS operates, and how system components work with each other during booting. The bootloader tutorial provided good practice and supplementary information to help you develop a simple yet working bootloader. We have also introduced the “mixed code” technique and shown how you can automate the assembly process in Microsoft Visual Studio 2005/2008.

This is but a small fraction of the whole topic of low-level programming, but this article gives you a good start.

8. Additional references

Assembly Language for Intel-based Computers by Kip R. Irvine: This book allows you to acquire good understanding of how a computer works inside and how to develop in Assembler. It also contains instructions on how to install, configure, and work with the MASM 6.15 compiler.

BIOS Interrupt Call: A Wikipedia article with guidance on the list of BIOS interruptions.

Kernel and Windows, Mac, Linux Driver Development Services: This article gives more information on low-level development projects and skills at Apriorit.

Continue reading our Dev Blog with this article - Windows USB debugging

Get source code - GitHub project

Learn more about Apriort's low-level development projects and skills!

Subscribe to updates