In this blog post, we will guide you through the process of creating a simple operating system from scratch, using some basic tools and languages. By the end of this post, you will have a working system that can boot, load a kernel, print "Hello, World!" to the screen, and run a shell.
What is an Operating System?
An operating system is a software program that acts as an intermediary between the computer hardware and the user of the computer. The main purpose of an operating system is to provide an environment for effective execution of an application program. An operating system manages the resources and services such as devices, processors, memory, files, and networks.
An operating system consists of several components, such as:- Kernel: The kernel is the core of the operating system that controls the basic functions of the system, such as process management, memory management, device management, and system calls.
- Bootloader: The bootloader is a small program that runs when the computer is turned on and loads the kernel into memory and executes it.
- Shell: The shell is a program that provides a user interface to interact with the operating system, such as typing commands and running programs.
What Do You Need to Create an Operating System?
To create an operating system from scratch, you will need some tools and libraries, such as:- QEMU: QEMU is a software that emulates a computer and allows you to run your operating system without installing it on a physical machine.
- GCC: GCC is a compiler that converts your C code into executable code that can run on your operating system.
- NASM: NASM is an assembler that converts your assembly code into executable code that can run on your operating system.
- GRUB: GRUB is a bootloader that loads your operating system from a disk image and transfers control to it.
sudo apt-get update sudo apt-get install qemu gcc nasm grub
choco install qemu gcc nasm grub
How to Create an Operating System from Scratch?
Now that you have the tools and libraries ready, you can start creating your operating system from scratch. The steps are:- Setting up the development environment
- Writing the bootloader
- Writing the kernel
- Writing the shell
Setting up the Development Environment
The first step is to set up the development environment where you will write and test your code. You will need a code editor, a terminal, and a disk image.
- Code editor: You can use any code editor of your choice, such as Visual Studio Code, Atom, or Sublime Text. You will need to write code in C and assembly languages, so make sure your code editor supports syntax highlighting and indentation for these languages.
- Terminal: You will need a terminal to compile and run your code, as well as to interact with your operating system. You can use any terminal of your choice, such as Bash, PowerShell, or CMD.
- Disk image: You will need a disk image to store your operating system files and boot it using QEMU. You can create a disk image using the following command:
bash qemu-img create -f raw os.img 64M
This command creates a disk image namedos.img
with a size of 64 MB and a format of raw.
Writing the Bootloader
The second step is to write the bootloader that will load your kernel into memory and execute it. The bootloader is written in assembly language and stored in the first sector of the disk image, which is called the boot sector. The boot sector has a size of 512 bytes and ends with a magic number of 0xAA55
, which tells the BIOS that this is a bootable disk.
boot.asm
and paste the following code:
; Define some constants BITS 16 ; Use 16-bit mode org 0x7c00 ; Set the origin to 0x7c00, the address where the BIOS loads the boot sector SECTOR_SIZE equ 512 ; Set the sector size to 512 bytes KERNEL_OFFSET equ 0x1000 ; Set the kernel offset to 0x1000, the address where the bootloader loads the kernel ; Define a macro to print a string %macro print 2 mov ah, 0x0e ; Set the BIOS function to print a character mov bh, 0x00 ; Set the page number to 0 mov bl, %2 ; Set the foreground color to %2 mov si, %1 ; Set the source index to %1, the address of the string .print_char: lodsb ; Load the next character from si to al and increment si cmp al, 0 ; Compare al with 0, the null terminator je .done ; If al is 0, jump to .done int 0x10 ; Call the BIOS interrupt 0x10 to print the character in al jmp .print_char ; Jump to .print_char to print the next character .done: %endmacro ; Define a macro to load a sector %macro load_sector 3 push dx ; Save dx to the stack mov ah, 0x02 ; Set the BIOS function to read a sector mov al, 1 ; Set the number of sectors to read to 1 mov ch, 0 ; Set the cylinder number to 0 mov dh, 0 ; Set the head number to 0 mov cl, %1 ; Set the sector number to %1 mov bx, %2 ; Set the destination address to %2 mov dl, %3 ; Set the drive number to %3 int 0x13 ; Call the ; Load the kernel from the second sector to the kernel offset load_sector 2, KERNEL_OFFSET, 0x80 ; Jump to the kernel offset to execute the kernel jmp KERNEL_OFFSET ; Fill the remaining bytes of the boot sector with zeros times SECTOR_SIZE - ($ - $$) db 0 ; Append the magic number to the end of the boot sector dw 0xAA55
This code loads the kernel from the second sector of the disk image to the memory address 0x1000
, which is the kernel offset. Then, it jumps to that address to execute the kernel. The times
directive fills the remaining bytes of the boot sector with zeros, so that the total size of the boot sector is 512 bytes. The dw
directive appends the magic number 0xAA55
to the end of the boot sector, which tells the BIOS that this is a bootable disk.
bash nasm -f bin boot.asm -o boot.bin
This command uses NASM to compile the boot.asm
file into a binary file named boot.bin
.
bash dd if=boot.bin of=os.img conv=notrunc
This command uses DD to copy the boot.bin
file to the os.img
file without truncating it.
Writing the Kernel
The third step is to write the kernel that will print "Hello, World!" to the screen and halt the CPU. The kernel is written in C language and stored in the second sector of the disk image. The kernel uses some functions from the standard library, such as printf
and exit
, which are provided by the GCC compiler.
kernel.c
and paste the following code:
c #include <stdio.h> #include <stdlib.h> // Define a function to print a string to the screen void print(char *str) { // Loop through each character of the string while (*str) { // Write the character to the video memory at address 0xb8000 *(unsigned short *)0xb8000 = (*str++ | 0x0f00); // Increment the video memory address by 2 bytes 0xb8000 += 2; } } // Define the main function of the kernel int main() { // Print "Hello, World!" to the screen print("Hello, World!"); // Exit the kernel exit(0); // Return 0 return 0; }
This code defines a function named print
that takes a string as an argument and prints it to the screen. The function writes each character of the string to the video memory at address 0xb8000
, which is the start of the text mode display. The function also adds an attribute byte of 0x0f
to each character, which sets the foreground color to white and the background color to black. The main function of the kernel calls the print
function with the argument "Hello, World!", and then exits the kernel using the exit
function from the standard library.
bash gcc -ffreestanding -c kernel.c -o kernel.o
This command uses GCC to compile the kernel.c
file into an object file named kernel.o
. The -ffreestanding
option tells the compiler that the program does not depend on the standard library or the operating system.
bash ld -o kernel.bin -Ttext 0x1000 kernel.o --oformat binary
This command uses LD to link the kernel.o
file into a binary file named kernel.bin
. The -Ttext 0x1000
option tells the linker that the code section of the kernel should start at address 0x1000
, which is the kernel offset. The --oformat binary
option tells the linker that the output format should be binary.
bash dd if=kernel.bin of=os.img seek=1 conv=notrunc
This command uses DD to copy the kernel.bin
file to the os.img
file, starting from the second sector. The seek=1
option tells DD to skip the first sector, which is occupied by the bootloader. The conv=notrunc
option tells DD not to truncate the disk image.
Writing the Shell
The fourth and final step is to write the shell that will read user input and execute commands. The shell is written in C language and stored in the third sector of the disk image. The shell uses some functions from the standard library, such as gets
, puts
, and strcmp
, which are provided by the GCC compiler.
shell.c
and paste the following code:
c #include <stdio.h> #include <stdlib.h> #include <string.h> // Define a function to read a line from the keyboard char *read_line() { // Allocate a buffer of 256 bytes char *buffer = malloc(256); // Read a line from the standard input and store it in the buffer gets(buffer); // Return the buffer return buffer; } // Define a function to execute a command void execute_command(char *command) { // Check if the command is "help" if (strcmp(command, "help") == 0) { // Print the help message puts("Welcome to my shell!"); puts("The available commands are:"); puts("help - show this help message"); puts("hello - print hello to the screen"); puts("exit - exit the shell"); } // Check if the command is "hello" else if (strcmp(command, "hello") == 0) { // Print hello to the screen puts("Hello!"); } // Check if the command is "exit" else if (strcmp(command, "exit") == 0) { // Exit the shell exit(0); } // Otherwise else { // Print an error message puts("Invalid command!"); } } // Define the main function of the shell int main() { // Declare a variable to store the command char *command; // Loop forever while (1) { // Print the prompt printf("> "); // Read a line from the keyboard and store it in the command variable command = read_line(); // Execute the command execute_command(command); // Free the command variable free(command); } // Return 0 return 0; }
This code defines a function named read_line
that reads a line from the keyboard and returns it as a string. The function allocates a buffer of 256 bytes using the malloc
function from the standard library, and then reads a line from the standard input using the gets
function from the standard library. The function returns the buffer as the result.
The code also defines a function named execute_command
that takes a command as an argument and executes it. The function checks if the command is one of the predefined commands, such as "help", "hello", or "exit", and performs the corresponding action. If the command is "help", the function prints a help message that shows the available commands. If the command is "hello", the function prints "Hello!" to the screen. If the command is "exit", the function exits the shell using the exit
function from the standard library. If the command is not one of the predefined commands, the function prints an error message that says "Invalid command!".
- Prints the prompt "> " to the screen using the
printf
function from the standard library. - Reads a line from the keyboard and stores it in the command variable using the
read_line
function. - Executes the command using the
execute_command
function. - Frees the command variable using the
free
function from the standard library.
bash gcc -ffreestanding -c shell.c -o shell.o
This command uses GCC to compile the shell.c
file into an object file named shell.o
. The -ffreestanding
option tells the compiler that the program does not depend on the standard library or the operating system.
bash ld -o shell.bin -Ttext 0x2000 shell.o --oformat binary
This command uses LD to link the shell.o
file into a binary file named shell.bin
. The -Ttext 0x2000
option tells the linker that the code section of the shell should start at address 0x2000
, which is the shell offset. The --oformat binary
option tells the linker that the output format should be binary.
bash dd if=shell.bin of=os.img seek=2 conv=notruncThis command uses DD to copy the
shell.bin
file to the os.img
file, starting from the third sector. The seek=2
option tells...
How to Test Your Operating System?
The last step is to test your operating system and see it in action. You can use QEMU to emulate a computer and boot your operating system from the disk image. To do that, use the following command:bash qemu-system-i386 -hda os.imgThis command uses QEMU to emulate a 32-bit x86 system and load the
os.img
file as the hard disk drive. You should see a window that shows the output of your operating system, such as:
![QEMU window]
You can interact with your operating system using the keyboard and the mouse. You can type commands in the shell and see the results. You can also exit QEMU by pressing Ctrl+C
in the terminal.
Congratulations! You have successfully created a simple operating system from scratch. You have learned how to write a bootloader, a kernel, and a shell, and how to compile, link, and test them. You have also learned some basic concepts and skills of operating system development.
Conclusion
In this blog post, we have guided you through the process of creating a simple operating system from scratch, using some basic tools and languages. We hope you have enjoyed this project and learned something new and useful. If you have any questions, comments, or feedback, please feel free to share them with us.
Thank you for reading and happy coding! 😊