ApriorIT

Bootloader Programming Tutorial: How To Develop Your Own Boot Loader

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

3. Implementation

4. Assembling

5. Demonstration and testing

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.

Related services Kernel and Driver Development

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:

BIOS reads the first sector of the hard disk drive. 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:

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

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

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.

Related services Operating System Management

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).

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.

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:

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:

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.

Read also:

Windows File System Filter Driver Development Tutorial

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.

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.