Develop your own operating system

DON'T post new tutorials here! Please use the "Pending Submissions" board so the staff can review them first.
User avatar
Gogeta70
^_^
^_^
Posts: 3275
Joined: 25 Jun 2005, 16:00
18

Develop your own operating system

Post by Gogeta70 »

Gogeta70's Operating System Kernel Development Guide

Foreword

Welcome to the beginning of your journey into OS Development! OS Dev is an extremely difficult, frustrating, and puzzling task. There is little in the way of instant gratification, and nothing is just handed to you. Likewise, if you can make your way through the labyrinth that is OS Dev, you will find the rewards are well worth the effort!

In OS Development, you start with nothing at your disposal. Want to use printf() to write to screen? You'll have to write your own printf function. Need to debug a function? Good luck, there are no debuggers here - unless you write your own kernel debugger. For me, what drew me to OS Dev is the challenge of it. I've run into several walls along the way, problems i thought i couldn't solve. When that happens - read, research, and retry! - but don't give up.

Following OS Dev will ultimately give you an understanding of the inner workings of other operating systems and the underlying hardware that they interact with. You will understand virtual memory, memory paging, how swapping from ram to disk works, the differences between ring3 and ring0, how and why system crashes happen (BSOD, anybody?), what your processor's protected mode is and how it works, keyboard drivers, graphics drivers, IRQ lines, and the list goes on and on...

Excited? Scared? Good, then let's get started.


Introduction

Like i said in the Foreword, in OS Dev, you don't get much to work with. There is no standard library of functions to use. The only functions and libraries you have are the ones you write. That being said, you do have some resources at your disposal.

Most of this guide uses information i learned from the OSDev Wiki - http://wiki.osdev.org/Main_Page" onclick="window.open(this.href);return false;. If you run into a problem in your code, the best thing to do is to read as much as you can about what you're trying to do. Knowledge is your greatest asset in OS Dev, and this is where you can find it. Additionally, they have a community forum where you can ask questions, but do your research before asking!

Additionally, there *IS* a debugger available for debugging your OS, but it is very basic and limited in its abilities. http://bochs.sourceforge.net/" onclick="window.open(this.href);return false;

We will be writing our OS in C in this guide, and we will require a few tools to get started:

VirtualBox - for running and testing your OS
Bochs (optional, but recommended) - Can also run and test your OS, used for debugging
Debian Linux - We will be developing our OS in debian, install it in a VM
A GCC Cross-compiler - We will use this compiler for compiling our kernel.
NASM - For compiling assembly source files
A Code Editor - A Code editor of your choice. Look around in synaptic for one.


Setting Up The Environment


Virtualbox

The first thing you need to install is virtualbox. I shouldn't have to walk you through this.

https://www.virtualbox.org/wiki/Downloads" onclick="window.open(this.href);return false;


Debian Linux

Next, we'll set up debian for development. Even if you are running a version of linux on your computer, i still recommend doing your development inside a debian virtual machine. We'll be building a cross-compiler for our OS, which may interfere with the regular compiler on your system and prevent you from building any packages.

I recommend downloading the debian net install disc image:

(32 bit) http://cdimage.debian.org/debian-cd/7.5 ... etinst.iso" onclick="window.open(this.href);return false;
(64 bit) http://cdimage.debian.org/debian-cd/7.5 ... etinst.iso" onclick="window.open(this.href);return false;

I shouldn't need to walk you through installing debian, so i won't.

When it's all set up, in your virtual machine settings for debian, expose a folder from your HOST OS to debian - this is where we'll be saving our OS's .iso file for testing in virtualbox/bochs.

Install the following tools in Debian unless stated otherwise.

Install a Code Editor

Before we build our cross-compiler, we need to download and install all of our other tools. If you have a preference for a code editor, you're welcome to use it. Make sure it can highlight assembly code as well as C code.

Personally, i use Geany, so if you want to try it out:

Run this as root: apt-get install geany

Install NASM

Install NASM by doing the following as root:

apt-get install nasm

Pretty straight forward, moving on...


Install Bochs

If you want to, install bochs. You're going to install this in your HOST OS, not your Debian machine!

On linux, just do: apt-get install bochs

Windows: http://bochs.sourceforge.net/" onclick="window.open(this.href);return false;

I'm not sure about linux, but on windows there are 2 binaries for bochs after you install it. One is for regular usage, and the other has debugging support enabled. Guess which one we're interested in?


Build your Cross-Compiler

Now for the fun part. In your debian virtual machine, you're going to compile your own version of gcc for your OS.

Since this is not a guide on building gcc cross-compilers and it's already been very well documented, i'm going to point you to the guide for doing this. Once you've got your compiler set up, continue to the next section.

http://wiki.osdev.org/GCC_Cross-Compiler" onclick="window.open(this.href);return false;

You'll want to build a cross-compiler for i686-elf.


Starting to Code Your Kernel

At this point, you'll want to make a directory for your project files if you haven't already. Typically i create an "osdev" folder in my home directory, and create "src", and "iso" folders inside of that.

Aside from your source files, you'll need a linker script and you'll probably want a compile script, unless you want to type in the long chain of commands every time.

You can use my linker and compile scripts:

Save this file as 'linker.ld' in your src directory:

Code: Select all

/* The bootloader will look at this image and start execution at the symbol
   designated as the entry point. */
ENTRY(_start)

/* Tell where the various sections of the object files will be put in the final
   kernel image. */
SECTIONS
{
	/* Begin putting sections at 1 MiB, a conventional place for kernels to be
	   loaded at by the bootloader. */
	. = 1M;
	
	._text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot) 
		*(._text)
	}
	
	/*. += 0xC0000000;*/
	/* First put the multiboot header, as it is required to be put very early
	   early in the image or the bootloader won't recognize the file format.
	   Next we'll put the .text section. */
	.text ALIGN(4K) : AT(ADDR(.text)/* - 0xC0000000*/)
	{
		*(.text)
	}

	/* Read-only data. */
	.rodata ALIGN(4K) : AT(ADDR(.rodata)/* - 0xC0000000*/)
	{
		*(.rodata)
	}

	/* Read-write data (initialized) */
	.data ALIGN(4K) : AT(ADDR(.data)/* - 0xC0000000*/)
	{
		*(.data)
	}

	/* Read-write data (uninitialized) and stack */
	.bss ALIGN(4K) : AT(ADDR(.bss)/* - 0xC0000000*/)
	{
		*(COMMON)
		*(.bss)
		*(.bootstrap_stack)
	}
	
	_KERNEL_END_ADDR = .;

	/* The compiler may produce other sections, by default it will put them in
	   a segment with the same name. Simply add stuff here as needed. */
}

Save this as 'compile.sh' in your src directory:

Code: Select all

nasm kboot.asm -f elf -o kboot.o

../cross/bin/i586-elf-gcc -T linker.ld -o kernel.bin -ffreestanding -O2 -nostdlib kboot.o -lgcc

Edit the paths in the compile script to suit your system. When you create the file, make sure to run `chmod +x compile.sh` to make it executable.

Next, create a new file in your src directory, called 'kboot.asm'

Paste the following code into it:

Code: Select all


; Declare constants used for creating a multiboot header.
MBALIGN     equ  1<<0                   ; align loaded modules on page boundaries
MEMINFO     equ  1<<1                   ; provide memory map
FLAGS       equ  MBALIGN | MEMINFO      ; this is the Multiboot 'flag' field
MAGIC       equ  0x1BADB002             ; 'magic number' lets bootloader find the header
CHECKSUM    equ -(MAGIC + FLAGS)        ; checksum of above, to prove we are multiboot
 
; Declare a header as in the Multiboot Standard. We put this into a special
; section so we can force the header to be in the start of the final program.
; You don't need to understand all these details as it is just magic values that
; is documented in the multiboot standard. The bootloader will search for this
; magic sequence and recognize us as a multiboot kernel.
section .multiboot
align 4
	dd MAGIC
	dd FLAGS
	dd CHECKSUM
 
; Currently the stack pointer register (esp) points at anything and using it may
; cause massive harm. Instead, we'll provide our own stack. We will allocate
; room for a small temporary stack by creating a symbol at the bottom of it,
; then allocating 16384 bytes for it, and finally creating a symbol at the top.
section .bootstrap_stack
align 4
stack_bottom:
times 16384 db 0
stack_top:
 
; The linker script specifies _start as the entry point to the kernel and the
; bootloader will jump to this position once the kernel has been loaded. It
; doesn't make sense to return from this function as the bootloader is gone.
section ._text
global _start
_start:
	; Welcome to kernel mode!
	
	cli
	
.hang:
	hlt
	jmp .hang

Now run your compile script: `./compile.sh`

Check for errors (there shouldn't be any), and check for a 'kernel.bin' in your src directory.
Congratulations! You have now compiled your first kernel! This is called a bare-bones kernel - all it does is disable interrupts and halt the processor.

Let's try running our kernel. First, we'll need to put it into an iso disc image. This isn't too difficult to do. There's already a guide for doing this, so i'm going to point you to that guide. Once you get it all set up, we'll add some lines to your 'compile.sh' to automatically create the iso after the kernel is compiled.

http://wiki.osdev.org/Bootable_El-Torit ... RUB_Legacy" onclick="window.open(this.href);return false;

Not too difficult, right?

Now open your 'compile.sh' for editing and add the following lines to the bottom:

Code: Select all

cp ./kernel.bin ../iso/boot/
cd ../
genisoimage -R -b boot/grub/stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table -o bootable.iso iso
rm ./windows/bootable.iso
cp ./bootable.iso ./windows/
Make sure to edit the paths to match the ones on your system.

Remember during the Debian setup step above, i told you expose a folder from your host OS to debian? In my script, './windows/' is referring to that folder. From there, i could go into virtualbox in windows and run the iso for testing.

Go ahead and save your compile script and run it again. If everything worked out okay, you should get output like this:

Code: Select all

I: -input-charset not specified, using utf-8 (detected in locale settings)
Size of boot image is 4 sectors -> No emulation
Total translation table size: 2048
Total rockridge attributes bytes: 922
Total directory bytes: 4096
Path table size(bytes): 34
Max brk space used 22000
251 extents written (0 MB)
Anything else than that is an error that you'll need to fix. Double check that your ISO file was created, and go ahead and try running it in virtual box:

Image

Awesome! It worked! It doesn't do anything yet, but we'll change that soon. First, let's talk about what's happening here.


You may have noticed that we've taken a few shortcuts on some things. For example, we did not write our own bootloader. My reason for this is that a bootloader is fairly complicated to write, it's all done in assembly, it's ridiculously difficult to debug when there's a problem, and most bootloaders do the same thing. Most people want to jump right into the OS Dev part, so we're using grub to load our kernel for us.

Aside from loading our kernel, grub does us a few favors as well. When the computer first turns on, the processor is in what's called "Real Mode". This is a 16-bit mode that the processor runs in while running your bootloader. The only real advantage to being in this mode is that you have access to BIOS interrupts, which act as functions. Using BIOS interrupts, you can do things like change the screen resolution, read and write to floppy drives and hard drives, write to the screen, etc.

Shortly before GRUB hands control over to your kernel, it enables 32-bit Protected mode. It also scans your RAM and maps used and free memory ranges for you, which is passed in the EBX register when it calls your kernel's entry point.

So, when GRUB finally calls your kernel's entry point, this is where you stand:

- You are in 32-bit Protected mode and have full access to the 4GB range of memory.
- Virtual Memory (paging) is disabled. You have direct access to physical memory.
- You have a pointer in EBX that points to a memory map array, which is provided by GRUB
- You are running in Ring-0
- Any errors in your code that would throw an interrupt will cause the cpu to triple-fault, causing a reboot loop.


So what now? Well, the first thing you really want to do is be able to draw to screen. This is useful for debugging future code, but it's also nice to make your kernel do something that you can see.

First things first, though... we must install a GDT.


The Global Descriptor Table - GDT

The GDT is used by the processor to determine permissions and access rights to certain areas of physical memory. This is where you can set memory ranges to be used in ring-0 only, or ring-3. It's also used to set which sections of memory are used for executing code and which sections are used for data. Code and data sections can overlap in the GDT.

For our kernel, we're simply going to map the entire 4GB memory range to both code and data, all for ring-0 only. We can easily change this later.

Each entry in the GDT is an 8-byte value. The first entry of the GDT is not used, so most people just put a null entry for the first entry of the GDT.

The 8 bytes of a GDT entry Specify the following:

-Base address
-Limit
-Flags: Granularity, Size
-Access byte: Present bit, Privilege bits, Executable bit, Direction/conforming bit, Readable/Writable bit, Accessed bit

Base address is used to specify the beginning address the GDT entry pertains to.
Limit is added or subtracted from base (depending on your direction bit) to create the memory range affected by this GDT entry.
Granularity specifies whether limit is a count of bytes or a count of pages (a page is 4kb or 4096 bytes).

What we want are 2 GDT entries, covering the entire 4gb memory space, specifying both code and data. This will make all memory readable, writeable, and executable. This can be changed later, so don't fret over it.

To do this, we need to create a entry with a base of 0, a limit of 0xFFFFF, and a granularity of 1, to represent page granularity. A limit of 0xFFFFF multiplied by a 4kb granularity gives us the full 4gb memory range.

Add the following code to your 'kboot.asm' after the `cli` command and before the `.hang` label:

Code: Select all

gdt_install:
		lgdt [m_gdt]
		mov ax, 0x10 ; Load 0x10 into AX - 0x10 represents the data segment in the GDT
		mov ds, ax ; Fill in the segment registers with AX
		mov es, ax
		mov fs, ax
		mov gs, ax
		mov ss, ax
		jmp 0x08:gdt_return ; jumping like this puts 0x08 into CS -  the Code Segment


	gdt_start:
		
		; null descriptor
		
		dd 0
		dd 0
		
		;code descriptor
		
		dw 0xFFFF ; limit low
		dw 0x0000 ; base low
		db 0x00	  ; base middle
		db 0x9A	  ; access
		db 0xCF	  ; granularity ; 4 bits of this is flags, the other 4 are for the limit.
		db 0x00   ; base high
		
		;data descriptor
		
		dw 0xFFFF ; limit low
		dw 0x0000 ; base low
		db 0x00	  ; base middle
		db 0x92	  ; access
		db 0xCF	  ; granularity
		db 0x00   ; base high
		
	gdt_end:

	m_gdt:
		dw gdt_end - gdt_start - 1;
		dd gdt_start
	
	gdt_return:
There is a lot more to GDT's than what i've described here. For more information, check out: http://wiki.osdev.org/Global_descriptor_table" onclick="window.open(this.href);return false;

If you compile now, you won't see any visual change. However, now that we know we can execute code and write data anywhere, we can now write a function for drawing to screen.

At this point, we should go ahead and write a 'kernel_main' function. So, let's write some C code finally!


Writing a kernel main function in C

Go ahead and create a new file, 'kernel.c'. Paste the following code into the new file:

Code: Select all

#if !defined(__cplusplus)
#include <stdbool.h> /* C doesn't have booleans by default. */
#endif
#include <stddef.h>
#include <stdint.h>

/* Check if the compiler thinks if we are targeting the wrong operating system. */
#if defined(__linux__)
#error "You are not using a cross-compiler, you will most certainly run into trouble"
#endif


#if defined(__cplusplus)
extern "C" /* Use C linkage for kernel_main. */
#endif
void k_main(void)
{
	
	unsigned char *vidmem = (unsigned char*) 0xB8000;
	
	unsigned char *tmp = vidmem;
	for(int i = 0; i < 80 * 25; i++) // clear the screen - fill with black
	{
		*tmp = 0;
		tmp++;
		*tmp = 0x07;
		tmp++;
	}
	
	char hello[] = "Hello, world!";
	
	tmp = vidmem;
	
	for(int i = 0; hello[i] != 0; i++)
	{
		*tmp = (unsigned char) hello[i];
		tmp++;
		*tmp = 0x07;
		tmp++;
	}
	
	// technically, we should not return from this function. In this case, it's okay since it'll just halt the processor.
}
The files included at the top are generated by the compiler, so you don't have to worry about having those files. In the code above, i wrote a little bit of code to write "Hello, world!" to screen.

In VGA Text Mode, (which is what we're in) video memory resides at address 0xB8000. For each character that is displayed on screen, 2 bytes are used. The first byte is the ascii code of the character, and the 2nd byte is the color code. The 4 low bits are used for the foreground color (the color of the ascii character), and 3 high bits (bits 4-6) are used for the background color. In this Text Mode, you have an 80 character wide screen and 25 rows.

In the code above, the first loop clears the screen to black. The second loop takes the string and writes it to video memory.

Before you can compile and run this code, you need to do a few more things.

Add the following code to your kboot.asm file, after 'gdt_return'

Code: Select all

	extern k_main
	call k_main
This code just tells nasm that the 'k_main' function is external to the code, letting the linker fill in the function address for 'k_main'. Then, we call simply call our 'k_main' function which we wrote above.

One more thing to do: Add 'kernel.c' to our compile script. Put the following in 'compile.sh':

Code: Select all

../cross/bin/i586-elf-gcc -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra -O0
Also modify the 'ld' line to link in kernel.o:

Code: Select all

../cross/bin/i586-elf-gcc -T linker.ld -o kernel.bin -ffreestanding -O2 -nostdlib kboot.o kernel.o -lgcc
Modify the path to suit your system.

Go ahead and compile and run.

Image

Hey, now we're getting somewhere! Now let's write some basic functions for setting text color, background color, and writing to the screen.

Create a new folder in your 'src' directory and call it 'display'. Inside the new folder, create 2 new files: 'console.h' and 'console.c'.

Update your 'compile.sh' script:

Code: Select all

../cross/bin/i586-elf-gcc -c ./display/console.c -o console.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra -O0
Also make sure to tell the linker to include console.o.


Now open up your 'console.h' and paste the following:

Code: Select all

#ifndef CONSOLE_H
#define CONSOLE_H

#if !defined(__cplusplus)
#include <stdbool.h> /* C doesn't have booleans by default. */
#endif
#include <stddef.h>
#include <stdint.h>

#define VID_MEM_ADDR	0xB8000

#define CLR_BLACK			0
#define CLR_BLUE			1
#define CLR_GREEN			2
#define CLR_CYAN			3
#define CLR_RED				4
#define CLR_MAGENTA			5
#define CLR_BROWN			6
#define CLR_LIGHTGRAY		7
#define CLR_DARKGRAY		8
#define CLR_LIGHTBLUE		9
#define CLR_LIGHTGREEN		0x0A
#define CLR_LIGHTCYAN		0x0B
#define CLR_LIGHTRED		0x0C
#define CLR_LIGHTMAGENTA	0x0D
#define CLR_YELLOW			0x0E
#define CLR_WHITE			0x0F

/* *
 * 
 * 		These two functions set the color code value for foreground and background color.
 * 		The color code is 1 byte (8 bits). The low 4 bits (bits 0-3) represent the foreground color,
 * 		and bits 4-6 are for the background color. Bit 7 is not used for the color code and should
 * 		be set to 0. Due to the 3 bit limit of the background color, the background color can only
 * 		be colors 0-7.
 * 
 * */

bool setBgColor(unsigned char); // sets the background color
bool setFgColor(unsigned char); // sets the foreground color

/* *
 * 
 * 		The writeLine() function writes a string to the next available line. Moves down to the next line when
 * 		encountering '\n' characters. writeLine() also adds an extra '\n' to the end of the provided string.
 * 		writeLine() will also call scrollTerm() if it reaches the 25th line.
 * 
 * */

void writeLine(char *str);

/* *
 * 
 * 		putString() writes a string to the specified line. Ignores all newline characters and does not modify
 * 		the current line position. Will not scroll the screen. Any strings longer than 80 characters are truncated to 80 characters.
 * 
 * */

void putString(char *str, short line);

/* *
 * 
 * 		clearScreen() Simply clears the screen with the background color set by the setBgColor() function.
 * 		Resets line position to the top of the screen.
 * 
 * */

void clearScreen(void);

/* *
 * 
 * 		scrollTerm() scrolls the screen by 'n' lines. Fills in the bottom 'n' lines with blank space.
 * 
 * */

void scrollTerm(unsigned char n);

#endif
Given what you've learned so far in this guide, you should be able to write each function that is declared in the above header. So go ahead and give it a try. If you need help, the entire source code for this guide is provided at the end of this guide. You can find my console code there.

Now let's test our code and trying writing to the screen, setting colors, and scrolling:

Put this in your 'k_main' function:

Code: Select all

	setBgColor(CLR_BLACK);
	setFgColor(CLR_GREEN);
	
	clearScreen();
	
	char *strings[5] = {0};
	
	char str1[] = "This is a line.";
	char str2[] = "This is another line.";
	char str3[] = "This is yet another line.";
	char str4[] = "This is an even better line.";
	char str5[] = "But this, this is the best line of all. :)";
	
	strings[0] = str1;
	strings[1] = str2;
	strings[2] = str3;
	strings[3] = str4;
	strings[4] = str5;
	
	for(int i = 0; i < 1000; i++)
	{
		writeLine(strings[i % 5]);
		
		for(unsigned long x = 0; x < 0x2FFFFFF; x++); // this is just to slow the scrolling down since we don't have a sleep function yet.
	}
Here's what i got:

Image

Did yours work? If not, try debugging your code. Learning how to do so in this development environment will be very helpful when writing more complex code later on.

The next thing we should do is set up our interrupt descriptor table.


The Interrupt Descriptor Table - IDT

The interrupt descriptor table is very similar in setup and structure to the GDT we set up before. It's function, however, is completely different.

The x86 (and amd64) architecture is an interrupt driven system. This allows the processor to run other pieces of code while waiting for some other operation to complete, like a disk read. Without this, the processor would be wasting valuable clock cycles constantly polling hardware devices for their status.

Interrupts occur when the processor's current state of execution is interrupted and redirected to another piece of code - usually an interrupt handler.

For example, a common interrupt is int3. The in3 interrupt is used by debuggers to set a breakpoint in a piece of code. When the int3 interrupt is raised, the processor saves some state information to the stack and starts executing the specified interrupt handler. This is called a software interrupt.

A hardware interrupt acts very similarly to a software interrupt. A common hardware interrupt is the one generated by the keyboard every time a key is pressed. In this case, the keyboard notifies a chip called the Programmable Interrupt Controller, or PIC.

The PIC has 16 IRQ lines, or Interrupt Request lines. Some of these lines are hardwired to a specific piece of hardware. For example, IRQ0 is for the system timer interrupt (the Programmable Interval Timer chip) and IRQ1 is for the keyboard. When any of these IRQ lines are raised, the PIC checks its mask register to see if the CPU wants interrupts from this IRQ. If the IRQ is enabled, the PIC then sets the interrupt line going to the processor to high. Depending on if interrupts are enabled in the processor's flags register, the processor will either ignore the interrupt or execute the interrupt handler for that interrupt.

There are times when we want the processor to ignore all interrupts. For example, while we're setting up our new IDT, we don't want the processor to be interrupted. That could lead to a triple fault! There are two assembly instructions for this: cli, and sti. The 'cli' instruction clears the interrupt flag in the processor's flags register, while 'sti' sets the flag. After the processor executes each instruction, it checks the interrupt line, and if the interrupt flag is set, it handles the interrupt.

We have to be careful about how we set up our IDT. One mistake can cause a problem that will be very difficult to track down. Aside from setting up our IDT, we'll need to program the PIC as well. Let's do it.

Create a new folder called "interrupt" in your src directory. Inside that folder create 3 files: "idt.h", "idt.c", and "idt.asm". Yes, we'll have to write some assembly code.

Paste the following code in your "idt.h" file:

Code: Select all

#ifndef IDT_H
#define IDT_H

#if !defined(__cplusplus)
#include <stdbool.h> /* C doesn't have booleans by default. */
#endif
#include <stddef.h>
#include <stdint.h>

struct IDT_Entry {
	
	unsigned short offsetlow;
	unsigned short codeSelector;
	unsigned char zero;
	unsigned char type_attr;
	unsigned short offsethigh;
	
} __attribute__((packed)); // we want this packed down to exactly 8 bytes.

struct IDT_Desc {
	
	unsigned short size;
	unsigned long addr;
	
} __attribute__((packed)); // we want this packed down to exactly 6 bytes

struct IDT_Desc idt_desc;
struct IDT_Entry idt_entries[256];

void idt_init(void);

// these external functions are handled in idt.asm
extern void load_idt(void);
extern void init_pic(void);

extern void isr_divide_by_zero(void);
extern void isr_debugger(void);
extern void isr_nmi(void);
extern void isr_breakpoint(void);
extern void isr_overflow(void);
extern void isr_bounds(void);
extern void isr_invalid_opcode(void);
extern void isr_coprocessor_not_avail(void);
extern void isr_double_fault(unsigned long);
extern void isr_coprocessor_segment_overrun(void);
extern void isr_invalid_tss(unsigned long);
extern void isr_segment_not_present(unsigned long);
extern void isr_stack_fault(unsigned long);
extern void isr_general_protection_fault(unsigned long);
extern void isr_page_fault(unsigned long);
extern void isr_reserved(void);
extern void isr_math_fault(void);
extern void isr_alignment_check(void);
extern void isr_machine_check(void);
extern void isr_simd_floating_point_exception(void);

void int_divide_by_zero(void);
void int_debugger(void);
void int_nmi(void);
void int_breakpoint(void);
void int_overflow(void);
void int_bounds(void);
void int_invalid_opcode(void);
void int_coprocessor_not_avail(void);
void int_double_fault(unsigned long);
void int_coprocessor_segment_overrun(void);
void int_invalid_tss(unsigned long);
void int_segment_not_present(unsigned long);
void int_stack_fault(unsigned long);
void int_general_protection_fault(unsigned long);
void int_page_fault(unsigned long);
void int_reserved(void);
void int_math_fault(void);
void int_alignment_check(void);
void int_machine_check(void);
void int_simd_floating_point_exception(void);

#endif
The header should be fairly easy to understand. The most important parts are the structure definitions. The IDT_Desc structure is used to tell the processor where the idt is and how big it is. The IDT_Entry structure is used for each entry in the IDT. The max number of entries that can be handled in an IDT is 256, so we'll make sure to provide an entry for each one.

Paste the following code into your "idt.c":

Code: Select all

#include "idt.h"
#include "../display/console.h"

void idt_init(void)
{
	
	// We'll need to tell the processor where the IDT is in memory, and how big it is in bytes.
	// If we set the data in the following structure, then we can just point the processor to
	// our structure.
	
	idt_desc.addr = (unsigned long) idt_entries;
	idt_desc.size = (unsigned short) sizeof(struct IDT_Entry) * 256;
	
	
	// This is an array of interrupt service routine addresses.
	// The first 32 interrupts are reserved by intel for processor exceptions.
	// Intel doesn't use all the reserved interrupts, but we need to keep them
	// free in case that changes in the future.
	
	unsigned long isr_addresses[20] = {0};
	
	isr_addresses[0] = (unsigned long) isr_divide_by_zero;
	isr_addresses[1] = (unsigned long) isr_debugger;
	isr_addresses[2] = (unsigned long) isr_nmi;
	isr_addresses[3] = (unsigned long) isr_breakpoint;
	isr_addresses[4] = (unsigned long) isr_overflow;
	isr_addresses[5] = (unsigned long) isr_bounds;
	isr_addresses[6] = (unsigned long) isr_invalid_opcode;
	isr_addresses[7] = (unsigned long) isr_coprocessor_not_avail;
	isr_addresses[8] = (unsigned long) isr_double_fault;
	isr_addresses[9] = (unsigned long) isr_coprocessor_segment_overrun;
	isr_addresses[10] = (unsigned long) isr_invalid_tss;
	isr_addresses[11] = (unsigned long) isr_segment_not_present;
	isr_addresses[12] = (unsigned long) isr_stack_fault;
	isr_addresses[13] = (unsigned long) isr_general_protection_fault;
	isr_addresses[14] = (unsigned long) isr_page_fault;
	isr_addresses[15] = (unsigned long) isr_reserved;
	isr_addresses[16] = (unsigned long) isr_math_fault;
	isr_addresses[17] = (unsigned long) isr_alignment_check;
	isr_addresses[18] = (unsigned long) isr_machine_check;
	isr_addresses[19] = (unsigned long) isr_simd_floating_point_exception;
	
	// Now we must loop through and fill in each IDT entry
	
	for(int i = 0; i < 256; i++)
	{
		if(i < 20) // fill in the function pointers set in the array above
		{
			idt_entries[i].offsetlow = (isr_addresses[i] & 0xFFFF);
			idt_entries[i].codeSelector = 0x08;
			idt_entries[i].offsethigh = ((isr_addresses[i] >> 16) & 0xFFFF);
			idt_entries[i].zero = 0;
			idt_entries[i].type_attr = ((1 << 7) | 0x0E); // sets present bit, 0x0E = interrupt gate
			
			continue;
		}
			
			// these remaining routines just use the empty 'reserved' irq routine, since they're unused right now
			idt_entries[i].offsetlow = (isr_addresses[15] & 0xFFFF);
			idt_entries[i].codeSelector = 0x08;
			idt_entries[i].offsethigh = ((isr_addresses[15] >> 16) & 0xFFFF);
			idt_entries[i].zero = 0;
			idt_entries[i].type_attr = ((1 << 7) | 0x0E); // sets present bit, 0x0E = interrupt gate
	}
	
	load_idt();
	init_pic();
	__asm("sti"); // enable interrupts
}

void int_divide_by_zero(void)
{
	
}

void int_debugger(void)
{
	
}
void int_nmi(void)
{
	
}
void int_breakpoint(void)
{
	setBgColor(CLR_BLACK);
	setFgColor(CLR_RED);

	writeLine("INT3 DEBUGGER BREAKPOINT INTERRUPT");

	setBgColor(CLR_BLACK);
	setFgColor(CLR_GREEN);
}
void int_overflow(void)
{
	
}

void int_bounds(void)
{
	
}
void int_invalid_opcode(void)
{
	
}
void int_coprocessor_not_avail(void)
{
	
}

void int_double_fault(unsigned long e __attribute__((unused)))
{
	
}
void int_coprocessor_segment_overrun(void)
{
	
}

void int_invalid_tss(unsigned long e __attribute__((unused)))
{
	
}

void int_segment_not_present(unsigned long e __attribute__((unused)))
{
	
}

void int_stack_fault(unsigned long e __attribute__((unused)))
{
	
}

void int_general_protection_fault(unsigned long e __attribute__((unused)))
{
	
}

void int_page_fault(unsigned long e __attribute__((unused)))
{
	
}

void int_reserved(void)
{
	
}

void int_math_fault(void)
{
	
}

void int_alignment_check(void)
{
	
}

void int_machine_check(void)
{
	
}

void int_simd_floating_point_exception(void)
{
	
}

The comments in the code pretty much describe it all. You can see a bunch of empty functions there. Those are all the interrupts reserved by Intel for processor exceptions. When you get a BSOD, typically it's when one of these processor exceptions occur. A common one is a page fault. Some of these are recoverable errors, and others are not errors at all, like the breakpoint exception.

Next, copy the following into "idt.asm":

Code: Select all

extern idt_desc

; load the IDT
global load_idt
load_idt:
	lidt [idt_desc]
	ret

; these functions are declared in idt.h and handled in idt.c
extern int_divide_by_zero
extern int_debugger
extern int_nmi
extern int_breakpoint
extern int_overflow
extern int_bounds
extern int_invalid_opcode
extern int_coprocessor_not_avail
extern int_double_fault
extern int_coprocessor_segment_overrun
extern int_invalid_tss
extern int_segment_not_present
extern int_stack_fault
extern int_general_protection_fault
extern int_page_fault
extern int_reserved
extern int_math_fault
extern int_alignment_check
extern int_machine_check
extern int_simd_floating_point_exception
extern int_system_timer

; these functions are 'trampoline' functions. The simply call their C-function counterpart.
global isr_divide_by_zero
global isr_debugger
global isr_nmi
global isr_breakpoint
global isr_overflow
global isr_bounds
global isr_invalid_opcode
global isr_coprocessor_not_avail
global isr_double_fault
global isr_coprocessor_segment_overrun
global isr_invalid_tss
global isr_segment_not_present
global isr_stack_fault
global isr_general_protection_fault
global isr_page_fault
global isr_reserved
global isr_math_fault
global isr_alignment_check
global isr_machine_check
global isr_simd_floating_point_exception
global isr_system_timer

isr_divide_by_zero:
	pushad
	call int_divide_by_zero
	popad
	iret
	
isr_debugger:
	pushad
	call int_debugger
	popad
	iret
	
isr_nmi:
	pushad
	call int_nmi
	popad
	iret
	
isr_breakpoint:
	pushad
	call int_breakpoint
	popad
	iret
	
isr_overflow:
	pushad
	call int_overflow
	popad
	iret
	
isr_bounds:
	pushad
	call int_bounds
	popad
	iret
	
isr_invalid_opcode:
	pushad
	call int_invalid_opcode
	popad
	iret
	
isr_coprocessor_not_avail:
	pushad
	call int_coprocessor_not_avail
	popad
	iret
	
isr_double_fault:
	pushad
	call int_double_fault
	popad
	iret
	
isr_coprocessor_segment_overrun:
	pushad
	call int_coprocessor_segment_overrun
	popad
	iret
	
isr_invalid_tss:
	pushad
	call int_invalid_tss
	popad
	iret
	
isr_segment_not_present:
	pushad
	call int_segment_not_present
	popad
	iret
	
isr_stack_fault:
	pushad
	call int_stack_fault
	popad
	iret
	
isr_general_protection_fault:
	pushad
	call int_general_protection_fault
	popad
	iret
	
isr_page_fault:
	pushad
	call int_page_fault
	popad
	iret
	
isr_reserved:
	pushad
	call int_reserved
	popad
	iret
	
isr_math_fault:
	pushad
	call int_math_fault
	popad
	iret
	
isr_alignment_check:
	pushad
	call int_alignment_check
	popad
	iret
	
isr_machine_check:
	pushad
	call int_machine_check
	popad
	iret
	
isr_simd_floating_point_exception:
	pushad
	call int_simd_floating_point_exception
	popad
	iret
There's a lot of functions here doing the same thing, isn't there? These are basically trampoline functions. When we fill in our IDT, we put the address of these functions in the IDT, instead of the address of the C functions. Why? Well if you look at all these functions, they all do the following: push registers on to stack, call their respective C function, pop the registers of the stack, then do an IRET. So these functions all preserve the processor registers. Additionally, you must call iret when returning from an interrupt. The iret instruction tells the processor it's returning from an interrupt, so the processor will restore the state information it saved before handling the interrupt.

At the top of the assembly file, we see a strange instruction, "lidt". That basically means "Load IDT".
You can see that we're passing the IDT_Desc structure we filled in in our C file. Everything is coming together nicely so far, wouldn't you say?

The last thing we need to do is program the PIC. This is pretty easy.

Create a new file in your interrupt folder and call it 'pic.asm'.
Paste the following code into the new file:

Code: Select all


global init_pic
init_pic:
	
	mov al, 0x11 ; First initialization byte for the pic
	
	out 0x20, al ; send first init byte to pic1
	out 0xA0, al ; send first init byte to pic2
	
	; now let's map the pic to irq numbers 32 - 48
	
	mov al, 0x20
	out 0x21, al
	
	mov al, 0x28
	out 0xA1, al
	
	; now let's tell the pic chips what lines they're linked together on
	
	mov al, 0x04
	out 0x21, al
	
	mov al, 0x02
	out 0xA1, al
	
	; tell the pic that we're in 80x86 mode
	
	mov al, 1
	
	out 0x21, al
	out 0xA1, al
	
	; now let's just null the data registers and we're done
	
	xor al, al
	
	out 0x21, al
	out 0xA1, al
	
	ret

This code won't make much sense to you until you read up on PICs. Basically, the pic takes 4 Initialization Code Words (ICW), which is what the above code does. The 'out' instruction is a way to send data to a device, given a hardware port number (0x20, 0xA0, 0x21, 0xA1), and a value.

There is a lot of information available about the PIC, so i'm not going to cover it in any more detail here. For more information on the PIC and about interrupts, check out the following links:

http://wiki.osdev.org/Interrupts" onclick="window.open(this.href);return false;
http://wiki.osdev.org/Interrupt_Service_Routines" onclick="window.open(this.href);return false;
http://wiki.osdev.org/IDT" onclick="window.open(this.href);return false;
http://wiki.osdev.org/PIC" onclick="window.open(this.href);return false;
http://www.brokenthorn.com/Resources/OSDevPic.html" onclick="window.open(this.href);return false;

Want to test your interrupt handlers? Try putting an int3 breakpoint in your k_main:

Code: Select all

__asm("int3");
It should print some text to the screen saying you've hit a breakpoint ;)

The Physical Memory Manager

Physical memory refers to the physical RAM in your computer. If i put some data at address 0x1000 in ram, then that's exactly where it is. There is no translation to a different address, like with virtual memory.

Virtual memory, on the other hand is the opposite. Address 0x1000 in virtual memory could point to any location in physical memory. It can also be a different location in physical memory each time.

The concept behind physical memory management is pretty simple. Basically, you keep track of which *pages* of physical memory are allocated and which pages are free. You have to do this without eating up half of the ram in the process though. that means no large data structures to represent a single page of physical memory. There are a few different ways to do this, each with it's own pros and cons, but today i'm going to show you one of the simpler ways of doing it: a bit-map.

A bit-map basically uses a single bit to represent the status of a page of memory. If the bit is set, then the memory is in use and cannot be allocated for something else. If the bit is clear, then the memory is free to use.

In this case, we can use an array of DWORD values for our bitmap. Since we're developing a 32-bit OS and the max amount of ram (without PAE) for a 32-bit OS is 4gb, we need enough bits to represent 4gb of ram. You can do the math yourself, but that comes out to 1,048,576 bits, or 32,768 DWORDs. That's only 128kb!

We'll need to write a few functions:

-A function to initialize the physical memory manager: pmm_init()
-A function to allocate x pages of physical memory: pmm_allocate()
-A function to free a single page of physical memory: pmm_free()
-Optionally, a function that prints the free memory ranges and the number of free bytes, kilobytes, and megabytes (a nice function for testing and troubleshooting)

Did you notice something strange? We have a function to allocate multiple pages at once, but our pmm_free function only frees a single page. The reason for this is because of how virtual memory works, but we'll cover that in the next section.

Speaking of virtual memory... why use it? It seems like we're getting along just fine with physical memory. In fact, we could skip using virtual memory completely if we wanted to. Older computer systems did exactly that. There are problems with it, though.

Virtual memory brings with it the ability to do access control on specific pages of memory (read, write, execute), it also allows each program to have its own address space. That means each program can be loaded at the same virtual address every time. Additionally, it allows us to use the hard drive as extra space by swapping pages out of memory.

The role of the physical memory manager is simply to hand out unallocated physical memory to the virtual memory manager and to de-allocate physical memory when the virtual memory manager is done with it. So aside from testing and debugging, we won't be directly allocating physical memory for anything in our kernel. In fact, once we enable virtual memory paging, we won't be able to even if we want to.

With that said, let's get to the code.

Inside your 'src' directory, create a new folder called 'memory'. Inside that folder, create 3 new files: 'pm_manager.h', 'pm_manager.c', and 'multiboot.h'.

Copy the code from here and put it in your multiboot.h file.

Paste the following into pm_manager.h:

Code: Select all

#ifndef PM_MANAGER_H
#define PM_MANAGER_H

#if !defined(__cplusplus)
#include <stdbool.h> /* C doesn't have booleans by default. */
#endif
#include <stddef.h>
#include <stdint.h>

#include "multiboot.h"

#define ADDR_TO_PAGE(x) (x / 0x1000) // takes a memory address and converts it to a 4kb page number

void pmm_init(multiboot_info_t* mbinfo);
void pmm_print_ranges(void);

void* pmm_allocate(unsigned long pages);
void pmm_free(unsigned long page);

#endif
The code that goes into 'pm_manager.c' is nearly 400 lines, so grab it from the following link and copy it into your file.

http://code.suck-o.com/42560" onclick="window.open(this.href);return false;

Additionally, in order to compile the code i provided, you'll need a printf function. If you want, you can use mine:

Download File

Create a new directory in your 'src' folder and call it 'include'. Copy the files from the archive into the new folder.


Once you get everything set up and working, you can try playing with the physical memory manager:
Image

That's it for this section. Before moving onto virtual memory management, make sure you have a strong grasp of the concept behind the physical memory manager and the code for it. We will be expanding upon this concept in the next section.

Further reading:

http://wiki.osdev.org/Detecting_Memory_ ... p_Via_GRUB" onclick="window.open(this.href);return false;
http://wiki.osdev.org/Page_Frame_Allocation" onclick="window.open(this.href);return false;

Guide continues on page 2 of this topic
Attachments
standard_library.zip
I wrote a few functions that i needed from the C Standard Library. You can find them in this file.
(4.84 KiB) Downloaded 210 times
¯\_(ツ)_/¯ It works on my machine...

User avatar
l0ngb1t
Fame ! Where are the chicks?!
Fame ! Where are the chicks?!
Posts: 598
Joined: 15 Apr 2009, 16:00
15
Contact:

Re: Develop your own operating system

Post by l0ngb1t »

how many communities have such post, writing your own OS, I guess they are rare...
this just made suck-o a more special and serious community...
hats off.
There is an UNEQUAL amount of good and bad in most things, the trick is to work out the ratio and act accordingly. "The Jester"

scatter
Fame ! Where are the chicks?!
Fame ! Where are the chicks?!
Posts: 366
Joined: 01 Jan 2014, 05:22
10

Re: Develop your own operating system

Post by scatter »

interesting thx for the post *thumb*

scatter
Fame ! Where are the chicks?!
Fame ! Where are the chicks?!
Posts: 366
Joined: 01 Jan 2014, 05:22
10

Re: Develop your own operating system

Post by scatter »

oh btw , I will be waiting for the next parts of the series :D at least will try to wait :p

User avatar
ayu
Staff
Staff
Posts: 8109
Joined: 27 Aug 2005, 16:00
18
Contact:

Re: Develop your own operating system

Post by ayu »

Awesome stuff Gog! :D
"The best place to hide a tree, is in a forest"

User avatar
Gogeta70
^_^
^_^
Posts: 3275
Joined: 25 Jun 2005, 16:00
18

Re: Develop your own operating system

Post by Gogeta70 »

I'm glad you all like the guide so far ^_^

I'm trying to make it as easy to follow as possible, but considering the topic, not everybody will be able to understand everything without a lot of experience in programming, computer hardware, integrated chips, etc. Fact is, so far i've only covered the pretty easy stuff. The next thing i'm going to cover is interrupts, which is a little more difficult than what we've been doing.

If you guys are able to follow and understand the section on virtual memory management, i'll know i'm doing a good job of writing this guide. :P

Well anyway, i've gotten all the code relevant to interrupts done, so expect that section to be updated soon!
¯\_(ツ)_/¯ It works on my machine...

User avatar
maboroshi
Dr. Mab
Dr. Mab
Posts: 1624
Joined: 28 Aug 2005, 16:00
18

Re: Develop your own operating system

Post by maboroshi »

Pretty fucking amazing!

Nice work Gogeta! :D

User avatar
ph0bYx
Staff Member
Staff Member
Posts: 2039
Joined: 22 Sep 2008, 16:00
15
Contact:

Re: Develop your own operating system

Post by ph0bYx »

Now that's one comprehensive guide, it will sure take time to go through it all. I do appreciate the effort! *thumb*

scatter
Fame ! Where are the chicks?!
Fame ! Where are the chicks?!
Posts: 366
Joined: 01 Jan 2014, 05:22
10

Re: Develop your own operating system

Post by scatter »

I am the person with the less experience in here but despite of that , this tutorial gives the practical part of the theory of what am learning which means it complete it :D

User avatar
Gogeta70
^_^
^_^
Posts: 3275
Joined: 25 Jun 2005, 16:00
18

Re: Develop your own operating system

Post by Gogeta70 »

Seems like i'm doing ok on the guide i guess :P

I've updated the guide, the section on interrupts is finished. I covered a lot of stuff in it, so let me know if something is hard to understand or not explained clearly enough. The rest of the guide is going to start getting more difficult, since we're getting to the real inner workings of an operating system now.

Take it easy fellas ^_^
¯\_(ツ)_/¯ It works on my machine...

User avatar
bad_brain
Site Owner
Site Owner
Posts: 11636
Joined: 06 Apr 2005, 16:00
19
Location: In your eye floaters.
Contact:

Re: Develop your own operating system

Post by bad_brain »

phew, finally had some time to read it...really great man, when you're done you should create an e-book guide, like they did (in a bad way sadly) with LFS.. :D
Image

User avatar
Gogeta70
^_^
^_^
Posts: 3275
Joined: 25 Jun 2005, 16:00
18

Re: Develop your own operating system

Post by Gogeta70 »

I'm not sure what LFS is, but i don't think putting this guide in an E-Book format would be too difficult. :P

I've updated the guide. The section on physical memory management is complete. The code for this one was a bit more complex than the previous sections, so it's commented pretty heavily. If anybody has any questions or comments, feel free to post 'em. ^_^

Take it easy fellas.
¯\_(ツ)_/¯ It works on my machine...

User avatar
ph0bYx
Staff Member
Staff Member
Posts: 2039
Joined: 22 Sep 2008, 16:00
15
Contact:

Re: Develop your own operating system

Post by ph0bYx »

I believe bad_brain is talking about Linux From Scratch.

User avatar
bad_brain
Site Owner
Site Owner
Posts: 11636
Joined: 06 Apr 2005, 16:00
19
Location: In your eye floaters.
Contact:

Re: Develop your own operating system

Post by bad_brain »

ph0bYx wrote:I believe bad_brain is talking about Linux From Scratch.
*thumb*
Image

User avatar
CommonStray
Forum Assassin
Forum Assassin
Posts: 1215
Joined: 20 Aug 2005, 16:00
18

Re: Develop your own operating system

Post by CommonStray »

Really nice guide, ive actually considered getting more into this but I think my interests lie a higher up in the linux layer than kernel dev. I'm more of a desktop manager person, lol.

Post Reply