Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / ASM

Beginning Operating System Development, Part Two

Rate me:
Please Sign up or sign in to vote.
4.96/5 (39 votes)
1 Oct 2009CPOL13 min read 87.1K   1.2K   118   23
C++ support code and the console.

Introduction

  1. Environment setup
  2. C++ support code and the console
  3. Descriptor tables and interrupts
  4. 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:

C++
extern "C" void LoadConstructors()
{
    //This function pointer refers to the first global constructor
    extern int (*firstConstructor)();
    //Same as the above, but refers to the end of the constructor list
    extern int (*lastConstructor)();
    //This represents a pointer to an individual constructor
    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):

C++
extern "C" void __cxa_pure_virtual()
{
    //This doesn't need to have an implementation. 
    //If a virtual call cannot be made, nothing needs to be done
}

//These methods will require an implementation when you implement a memory manager
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 MOVing 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.

ASM
; Define some constants, just to make life easier for us
STACKSIZE           equ 0x8000

MBOOT_PAGE_ALIGN    equ 1 << 0   ; Load kernel and modules on a page boundary
MBOOT_MEM_INFO      equ 1 << 1   ; Provide the kernel with memory info
MBOOT_GRAPHICS      equ 1 << 2

MBOOT_HEADER_MAGIC  equ 0x1BADB002     ; Multiboot Magic value
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                               ; the Multiboot header at the start of our file
[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.

ASM
Multiboot:
  dd  MBOOT_HEADER_MAGIC     ; GRUB will search for this value
                             ; on each 4-byte boundary in the ELF file
  dd  MBOOT_HEADER_FLAGS
  dd  MBOOT_CHECKSUM         ; Ensures that the above values are correct
   
  dd  Multiboot              ; Location of this section
  dd  code                   ; Start of kernel '.text' (code) section
  dd  bss                    ; End of kernel '.data' section
  dd  end                    ; End of kernel
  dd  start                    ; Kernel entry point (initial EIP)

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:

ASM
Start:

    mov esp, stackEnd      ; Set up a stack which is 0x8000 bytes long
    mov ebp, 0             ; Give us a landing point for stack traces
    cli                    ; Disable interrupts.
    call LoadConstructors  ; Load global constructors
                           ; Parameters for the C++ entry point need to be pushed backwards
    push ebx               ; Load multiboot header location
    push esp               ; This is the stack address
    call Main              ; Invoke the Main() function
    jmp $                  ; Enter an infinite loop, to stop the processor
                           ; executing whatever rubbish is in the memory after the kernel

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:

C++
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:

0Black0000
1Blue0001
2Green0010
3Cyan0011
4Red0100
5Magenta0101
6Brown0110
7Light gray0111
8Dark gray1000
9Light blue1001
10Light green1010
11Light cyan1011
12Light red1100
13Light magenta1101
14Light brown1110
15White1111

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:

BitsField size (bits)Description
0 : 78Character code
8 : 114Text colour
12 : 154Background 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:

C++
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 char8 bit integer; known as a uchar
unsigned short16 bit integer; known as a ushort
unsigned integer32-bit integer; known as a uint
unsigned long long64-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:

C++
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Other
United Kingdom United Kingdom
Ninja programmer

Comments and Discussions

 
QuestionNeato Pin
TheRaven18-Sep-11 16:02
TheRaven18-Sep-11 16:02 
Generalproblem Pin
Member 42314671-May-10 5:15
Member 42314671-May-10 5:15 
GeneralRe: problem Pin
0x3c01-May-10 5:50
0x3c01-May-10 5:50 
GeneralRe: problem Pin
Member 42314671-May-10 6:37
Member 42314671-May-10 6:37 
Questionproblem with assembly file Pin
alaaaa15-Jan-10 2:46
alaaaa15-Jan-10 2:46 
AnswerRe: problem with assembly file Pin
0x3c015-Jan-10 2:50
0x3c015-Jan-10 2:50 
GeneralRe: problem with assembly file Pin
alaaaa15-Jan-10 4:53
alaaaa15-Jan-10 4:53 
GeneralPart Three Pin
0x3c020-Oct-09 23:04
0x3c020-Oct-09 23:04 
For those who are interested, I've posted the next part of the series. You can find the link in my article listing, or by following this direct link:

Beginning Operating System Development, Part Three[^]


GeneralI'm impressed Pin
jszczur8-Oct-09 22:50
jszczur8-Oct-09 22:50 
GeneralDid i already say Pin
Jerry Evans6-Oct-09 22:11
Jerry Evans6-Oct-09 22:11 
GeneralRe: Did i already say Pin
0x3c06-Oct-09 23:30
0x3c06-Oct-09 23:30 
QuestionWhy again that ugly C? Pin
Thornik1-Oct-09 21:29
Thornik1-Oct-09 21:29 
AnswerRe: Why again that ugly C? PinPopular
0x3c02-Oct-09 1:21
0x3c02-Oct-09 1:21 
GeneralWaiting for more Pin
Kyp28-Aug-09 0:12
Kyp28-Aug-09 0:12 
GeneralRe: Waiting for more Pin
0x3c04-Sep-09 4:50
0x3c04-Sep-09 4:50 
GeneralCool! Pin
Jim Crafton27-Aug-09 3:23
Jim Crafton27-Aug-09 3:23 
GeneralRe: Cool! [modified] Pin
0x3c03-Sep-09 8:42
0x3c03-Sep-09 8:42 
GeneralRe: Cool! Pin
AspDotNetDev5-Sep-09 22:55
protectorAspDotNetDev5-Sep-09 22:55 
GeneralRe: Cool! Pin
0x3c05-Sep-09 23:12
0x3c05-Sep-09 23:12 
GeneralRe: Cool! Pin
AspDotNetDev5-Sep-09 23:54
protectorAspDotNetDev5-Sep-09 23:54 
GeneralRe: Cool! Pin
brianhood5-Jan-10 12:02
brianhood5-Jan-10 12:02 
GeneralSo what will come out at the end of the series Pin
Rama Krishna Vavilala19-Aug-09 13:44
Rama Krishna Vavilala19-Aug-09 13:44 
GeneralRe: So what will come out at the end of the series Pin
0x3c019-Aug-09 22:37
0x3c019-Aug-09 22:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.