Introduction
- Environment setup
- C++ support code and the console
- Descriptor tables and interrupts
- The Real-Time Clock, Programmable Interrupt Timer and KeyBoard Controller
I’m going to have to assume from the outset that you’ve read and followed the previous article. If you haven’t, then what follows will be rather useless to you. You can read the previous article here.
The first thing that we need is some support code. G++ will be expecting some of these, but we will need to add some others to avoid the slightly more subtle bugs. In order for this, the code which we will add will:
- Call the constructors
- Provide
new
and delete
implementation stubs - Provide a method to be called when GCC can’t call a virtual method
Apart from those three, that’s it. Some Operating Systems call the destructors at the end of the kernel’s code, but I don’t see the point of this, given that most multi-tasking Operating Systems will infinite loop at the end of Main
to allow a timer IRQ to pick them up (more on that a lot later.)
First off, we need to call the constructors. Our linker script tells us where the pointers to these constructors start and finish. To call them, we just iterate through them. It’s possible that the linker might have put them in the wrong order, but this is highly unlikely. Given the scope of this article, you can trust GCC.
To call the constructors, we use this code:
extern "C" void LoadConstructors()
{
extern int (*firstConstructor)();
extern int (*lastConstructor)();
int (**constructor)();
for(constructor = &firstConstructor; constructor <
&lastConstructor; constructor++)
(*constructor)();
}
Now, it’s important to realise that we need to increment the pointer to the function pointer, not the function pointer itself. This is because otherwise we could overshoot, and start running our local variables (remember that assembly code is just a sequence of numbers, just like our local variables.)
Something you will have to understand is name mangling. This is G++’s way of allowing function overloading, classes, etc. The gist of it is that G++ encodes the parameter types (and some other stuff) into the method name. While this is usually very convenient for us, it has distinct disadvantages when we want to call those methods from assembly language. To override this mechanism, we have to use the extern "C"
keywords. These keywords simplify the calling conventions from assembly. I’m going to assume that you know how to use this. If not, the source code should help, but you'll want to investigate it yourself as well.
The next two support pieces are just stubs. When G++ encounters an object creation via new
, it just allocates the memory needed, casts, and calls the relevant constructor. We don’t need to do this yet, so just add these stubs to your common code file (I assume that you’re maintaining a tidy source tree – if you don’t, you’ll only complicate matters for yourself later):
extern "C" void __cxa_pure_virtual()
{
}
void *operator new(long unsigned int size)
{
return (void *)0;
}
void *operator new[](long unsigned int size)
{
return (void *)0;
}
void operator delete(void *p)
{
}
void operator delete[](void *p)
{
}
These should be fairly obvious – new
requires a call to your memory allocation routines, and delete
just frees the allocated memory. You can’t get much simpler than that.
The only method which needs an explanation is __cxa_pure_virtual
. This is the only mandatory magic method name which G++ demands of us. This method is called if a virtual call can’t be made for some reason. You could do some checking of the call stack to find out which method caused the problem, but you don’t have to. You can just leave it blank.
With that, the C++ support code is complete. Now, you just need something which will take over where GrUB leaves off. For this, we need to use assembly code, which is as close to raw binary as we need to go when programming our kernel.
Assembly code
Our assembly code must do two things. First, it has to set up a stack. Although this isn’t strictly mandatory, it’s always good to know exactly where everything is. We do this by just MOV
ing the same pointer to ESP
and EBP
. We can set up our own section of memory by reserving some bytes in the BSS section.
It also has to interface with GrUB. GrUB knows that this is a valid Multiboot kernel by the values located at the start of the executable file. First comes a magic value, next come the flags, and last comes the checksum. The checksum is whatever value is needed to make the sum of these three fields equal to zero. A pointer to the Multiboot information structure is located in EBX
, so we need to pass this to our C++ code.
When it’s done these things, it just has to call Main
and loop. This stops the processor from executing whatever values happen to reside in memory at the end of the program. These values can vary randomly, as the electrical ‘residues’ in memory fade over time. In an emulator like Bochs or Virtual PC, we’re lucky – the memory is zeroed out, so we know about this very quickly. However, if you run it on real hardware, you may not be so lucky, and this can take a long time to show. It’s best to avoid it in the first place.
With that, let’s see some code. This particular code won’t change throughout the articles, but you’re unlikely to get much more code handed to you on a plate. Hopefully, you aren’t copying and pasting the code verbatim – it isn’t ‘your’ kernel if you’ve just copied-and-pasted everything and tried to build your changes on top.
STACKSIZE equ 0x8000
MBOOT_PAGE_ALIGN equ 1 << 0
MBOOT_MEM_INFO equ 1 << 1
MBOOT_GRAPHICS equ 1 << 2
MBOOT_HEADER_MAGIC equ 0x1BADB002
MBOOT_HEADER_FLAGS equ MBOOT_PAGE_ALIGN | MBOOT_MEM_INFO | MBOOT_GRAPHICS
MBOOT_CHECKSUM equ -(MBOOT_HEADER_MAGIC + MBOOT_HEADER_FLAGS)
[BITS 32] ; Tell NASM to output 32-bit code [GLOBAL Multiboot] ; This lets LD reorder the file to put
[GLOBAL start] ; The assembly entry point
[EXTERN code] [EXTERN bss] [EXTERN end] ; This marks the end of the kernel [EXTERN Main] ; This is the C++ entry point. We have to call this manually [EXTERN LoadConstructors] ; Called before Main(), this calls the global constructors
This isn’t actually code per se; it just makes life easier for us, by showing us what we can expect by passing the flags and which bits of those flags are set. It also puts the stack size in a constant, so that we can change this value and have a different stack size without searching and replacing. You’ll need this if Bochs consistently complains about executing bogus memory.
Next, we lay out our kernel.
Multiboot:
dd MBOOT_HEADER_MAGIC
dd MBOOT_HEADER_FLAGS
dd MBOOT_CHECKSUM
dd Multiboot
dd code
dd bss
dd end
dd start
After the flags, there’s a checksum, which is a sanity check to make sure that every field in the Multiboot header added together equals zero. The last five lines aren’t included in that, as they are just additional information needed to make our kernel boot. They are the location of the Multiboot header in the kernel (usually at the 1 MiB mark), the start of our compiled code, the start of the uninitialized variable section, (BSS) the end of the kernel, and the kernel entry point. This will be where execution starts.
Now that GrUB has got everything it needs, it can boot our kernel. This is what will get executed – it’s the counterpart of the stub which runs before the traditional entry point of a program and sets everything up. This entry point will only be run once, and cannot depend on any memory protection at all. We don’t even know where our code is executing:
Start:
mov esp, stackEnd ; Set up a stack which is 0x8000 bytes long
mov ebp, 0
cli
call LoadConstructors
push ebx
push esp
call Main
jmp $
section .bss
stackStart:
resb STACKSIZE
stackEnd:
This is very simple code. To resolve the problem which we will face in a while, we move the stack somewhere safe and clean – and what’s going to be cleaner than a section within our kernel which the ELF specification says has to be filled with zeroes?
It’s important that a small subtlety is pointed out. We move the location of the end of the stack to ESP
. This is because the stack grows backwards, and if we gave it the start of the stack, we’d overwrite kernel data. We set EBP
to 0 so that when we get a stack trace working we can land somewhere to prevent any irritating infinite loops.
After the stack’s working properly, we disable interrupts. This isn’t necessary, but it’s always best to be sure.
Then, we call our LoadConstructors
function. This is the first piece of C++ code which will actually be executed. Normally, C++ method names are mangled so that method overloading can happen; we don’t want this to happen to the LoadConstructors
function, so we just give it the extern "C"
preface so that this doesn’t happen (unless you enjoy making your kernel compiler-specific.)
When everything’s done in that function, we push the parameters for Main
. Note the lack of name mangling for this function as well. Now, we’ll want the location of the stack in a while, so pass that along with the Multiboot structure, which can provide a great deal of information about the system. The catch is that we’ll be passing them as parameters, which need to be passed backwards. So, the method signature will be something like this:
void Main(unsigned int esp, multibootInfo *mbootPtr)
{
}
When we get here, we’re in our kernel. We’ve landed. Now we just need to show this to the world. Let’s start off by printing to the screen.
Printing to the screen
The computer boots into text mode. We have a region of memory which is mapped to the screen. The screen size is 80 characters across, by 25 lines down. Remember, this isn’t normal memory. Reads and writes may work like normal, but underneath, it’s mapped to the graphics card by a collection of electrical circuits.
We can’t just write our sentences to the video memory; we have to provide additional information. This additional information is the background and foreground colour, and is located in the top eight bytes of the sixteen byte integer. Effectively, we have 16 values for the text or background colour. These are:
0 | Black | 0000 |
1 | Blue | 0001 |
2 | Green | 0010 |
3 | Cyan | 0011 |
4 | Red | 0100 |
5 | Magenta | 0101 |
6 | Brown | 0110 |
7 | Light gray | 0111 |
8 | Dark gray | 1000 |
9 | Light blue | 1001 |
10 | Light green | 1010 |
11 | Light cyan | 1011 |
12 | Light red | 1100 |
13 | Light magenta | 1101 |
14 | Light brown | 1110 |
15 | White | 1111 |
As you can tell by the binary representation, the value which holds the values is 4 bits wide. Because we want a text and a background colour, the total width of the colours is 8 bits, or one byte. This effectively means that if we convert the memory mapped address to a pointer to a byte (or unsigned character), we could alternate, so address[0] would be the first character to write, and address[1] would hold the attributes of that character.
Something which you may have noticed is that while the screen is effectively two-dimensional, the block of memory we’ve got is only one-dimensional. This is resolved by ‘flattening’ the screen, so that one line directly follows another in memory and the VGA controller figures it out from there. This makes life simpler for us, as we just need to figure out where to put the next character.
The formula should be self-evident: if we have 25 columns, each 80 characters long, then we can calculate the offset in memory as 80Y+X. However, if we were writing data as an unsigned character, we would need to double that to make 2(80Y + X). This formula isn’t exactly self-evident, so we’ll be writing unsigned shorts instead, to maintain the first formula.
Now we have the offset, we just need to know where to write the data. The address is standardised to be one of two integers: 0xB8000 or 0xB0000. 0xB8000 is used for colour displays, 0xB0000 for monochrome. By reading from the BIOS Data Area, you can find this out, but every emulator you come across will have a colour screen by default, so there isn't any pressing need to do this.
It’s important that we know the format of the data we’re writing. We’ll be writing it as two unsigned characters: character value followed by attributes. This is the exact format:
Bits | Field size (bits) | Description |
0 : 7 | 8 | Character code |
8 : 11 | 4 | Text colour |
12 : 15 | 4 | Background colour |
The character code won’t be a problem. What is slightly trickier is the creation of the attribute byte. Now, we could theoretically use a structure, but this is bloated for what we need to do; depending on how your OS turns out, you might only be in text mode for a few seconds.
So, to write a string to the screen, we just need to do something like this:
void writeCharacter(char c, unsigned char backColour,
unsigned char textColour, int x, int y)
{
unsigned short *videoMemory = (unsigned char *)(0xB8000 + 80 * y + x);
unsigned char attribute = (backColour << 4) | (textColour & 0xF);
*videoMemory = c | ((unsigned short)attribute << 8);
}
As you can see, it’s surprisingly simple. First, we get a pointer to the required section of video memory, using the X and Y co-ordinates as a guide (there’s no need to multiply by 2 because we’re using an unsigned short, not an unsigned character). Then, we create the attribute byte. This is basic bit-shifting. First, we shift the background colour left by 4 bits, which creates the left-hand part of the data format. Then, we mask the text colour with 0xF, which creates the next one from the left. When we’ve done that, we simple have to OR the two portions together to form the attribute byte.
By now, we simply have to shift the attribute byte left by 8 bits and OR it with the character. That creates the 16-bit integer in the necessary format, and we simply write it to memory. There you are; you’ve written your first character to the screen!
Now, I’ll leave it up to you to create your own Console
class. Just remember these rules for implementation:
- A new line ("\n") needs to increment Y and set X to zero.
- When the X co-ordinate is above 80, you need to react in the same way as you do to a new line character.
- You’ll need to keep track of the X and Y co-ordinates.
- To clear the screen from all the stuff GrUB leaves there, you’ll need to write a space (0x20) character, with a back colour of black (0) and any text colour.
- You’ll want to write more than just a string. Hexadecimal and decimal printing will help a lot.
- To write a string, just iterate through until you reach a null character, printing each character.
- To save time, you’ll probably want a
printf
implementation.
When you get to this, you might not want (or need) the full complement of format strings, just a minimum of %x, %d, %s, and %c.
If you want to see some immediate results, then you could try printing the values which you can find in the Multiboot pointer passed as a parameter to your Main
function. As you’ll see, there’s a lot of interesting information there.
Utility functions
Now that you have a console driver, you need to have utility functions. You won’t use them (except to move the text cursor – there’s code freely available which can do that for you; consider it a research task) until the next tutorial, but you need them anyway. You also need some type aliases, to make life easier for you. You can call them what you want in your kernel, but I’ve called my types the C# names, because that’s what I’m most familiar with. I’ll be using these names in my tutorial. For reference, there’s a table below which describes them:
unsigned char | 8 bit integer; known as a uchar |
unsigned short | 16 bit integer; known as a ushort |
unsigned integer | 32-bit integer; known as a uint |
unsigned long long | 64-bit integer; known as a ulong |
Now, the utility functions. These allow port input and output, and they’re surprisingly simple. They use the privileged instructions INx
and OUTx
. You’ll need these functions on several occasions, and once they’re done, you could (theoretically) move directly into hard drive access. If you want to do this, you’re welcome to; it won’t be covered for quite some time, as it’s much better when you can detect the drives and work with as few assumptions as possible.
Because of the importance of these functions, I’ll be giving you the code, instead of pointing out links to datasheets or specifications:
void outportByte(unsigned short port, unsigned char value)
{
asm volatile ("outb %1, %0" : : "dN" (port), "a" (value));
}
void outportWord(unsigned short port, unsigned short value)
{
asm volatile ("outw %1, %0" : : "dN" (port), "a" (value));
}
void outportLong(unsigned short port, unsigned long value)
{
asm volatile ("outl %1, %0" : : "dN" (port), "a" (value));
}
unsigned char inportByte(unsigned short port)
{
unsigned char result;
asm volatile("inb %1, %0" : "=a" (result) : "dN" (port));
return result;
}
unsigned short inportWord(unsigned short port)
{
unsigned short result;
asm volatile("inw %1, %0" : "=a" (result) : "dN" (port));
return result;
}
unsigned long inportLong(unsigned short port)
{
ulong result;
asm volatile("inl %1, %0" : "=a" (result) : "dN" (port));
return result;
}
Don’t forget the method signatures in the correct header file.
Fini
That’s about the breadth of it. Creating your own Operating System isn’t that difficult to start off, but it’s going to get much more interesting in later parts.
Next up: Descriptor tables and interrupts.