13 – Developing an Operating System – Tutorial – Episode 7 – Second Stage Loader, Load Kernel at 0x10000 (1M) and Cleanup – OSDEV

In previous tutorial, we wrote ATA PIO driver.. now it is time to utilize the driver.

Before, starting with it.. I have made several changes in the structure, also compile script is no more bat file but rather a powershell script now, output is now stored inside a proper debug folder instead of in root folder. If this tutorial gets complicated, you can see the video I have posted on YouTube.

Following structural changes have been made:

  1. Create boot directory
  2. Move boot.asm, loader.asm and kernel.c file into a boot directory.
  3. Rename kernel.c to loader.c
  4. Create ld directory
  5. Move linker.ld file into a ld directory.
  6. Rename linker.ld file to loader.ld.
  7. Rename kmain method to loadkernel in loader.c file.
  8. Rename kmain to loadkernel in loader.asm file as well.
  9. Create a directory called kernel.
  10. Create two files kernel.h and kernel.c inside kernel directory.
  11. Also inside utils directory, create mem.h and mem.c file.

Now let’s first write hello world type of code in kernel.h and kernel.c as following:

Following is the kernel.h file

void start();

Following is the kernel.c file

#include "kernel.h"
#include "../drivers/screen.h"

void start()
{
    printf("I am the actual kernel :D");
} 

Next, we need malloc utility in our OS, which will allow us to request memory.

mem.h file will have following content:

#include <stdint.h>

/* At this stage there is no 'free' implemented. */
uint32_t malloc(uint32_t size, int align, uint32_t *phys_addr); 

and mem.c file will have following content:

#include <stdint.h>
#include "mem.h"

uint32_t free_mem_addr = 0x10000;
/* Implementation is just a pointer to some free memory which
 * keeps growing */
uint32_t malloc(uint32_t size, int align, uint32_t *phys_addr) {
    /* Pages are aligned to 4K, or 0x1000 */
    if (align == 1 && (free_mem_addr & 0xFFFFF000)) {
        free_mem_addr &= 0xFFFFF000;
        free_mem_addr += 0x1000;
    }
    /* Save also the physical address */
    if (phys_addr) *phys_addr = free_mem_addr;

    uint32_t ret = free_mem_addr;
    free_mem_addr += size; /* Remember to increment the pointer */
    return ret;
} 

Also, now our loader.c file will be changed to read from storage, allocate memory, load kernel at specified memory address and execute the kernel.

Following willbe our loader.c file:

#include "../drivers/screen.h"
#include "../utils/utils.h"
#include "../drivers/ata/ata.h"
#include "../utils/mem.h"

void loadkernel(void) {
    clear(); /* clear the screen. */
    
    printf("Welcome to Learn OS.\r\n");
    printf("Memory address of loadkernel in: 0x%x\r\n\r\n", loadkernel); /* print memory address of loadkernel method */

    uint32_t phy_addr;
    uint32_t page = malloc(1000, 1, &phy_addr); /* allocate memory */
    printf("page: %x\r\n", page);
    printf("phy addr: %x\r\n", phy_addr);

    printf("loading kernel into memory at 1M (0x%x)...\r\n", page);

    read_sectors_ATA_PIO(page, 0x12, 38); /* read 38 sectors (size of our kernel) from 12th sector (That's where kernel will be stored.). */

    uint32_t* kernelFile = page;
    printf("\r\n\r\n");

    if(kernelFile[0] == 0x464C457F) /*read first 4 bits of file to validate that it is a ELF file */
    {
        printf("Kernel is ELF File:\r\n");
    }
    int bits = (kernelFile[1] >> 0) & 0xFF; /* 5th bit is for 32-bit or 64-bit */
    if(bits == 1)
    {
        printf("\t32-bit\r\n");
    }
    int endian = (kernelFile[1] >> 8) & 0xFF; /* 6th bit is for little endian or big endian */
    if(endian == 1)
    {
        printf("\tlittle endian\r\n");
    }
    printf("\tentry position : 0x%x \r\n", page + kernelFile[6]); /* get the entry point of a kernel */
    printf("\texecuting kernel from: 0x%x....\r\n\r\n", page + kernelFile[6]); /* add entry point of a kernel to base address of requested memory */

    void (*func)(void) = page + kernelFile[6]; /* create a pointer to a function */
    func(); /* execute it / pass control to kernel */
}

Next, we create kernel.ld file inside ld directory:

OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
    . = 0x01000; /*0x001000*/
    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.text)
    }

    /*Read-only data*/
    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
    }

    /*Read-write data (initialized)*/
    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }

    /*Read-write data (uninitialized) and stack*/
    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(.bss)
    }
}

We delete our compile.bat file and create a new file called compile.ps1. Our compiler process will now do more like creating a raw hard disk image, write kernel at the beginning of the raw disk created, join raw disk with the boot loader and 2nd stage loader. Code for the same is as follows:

param($paramName)
Write-Output $paramName
Function Create-Directory($directoryPath) {
    if(Test-Path -Path $directoryPath -PathType Container) {
        Write-Output "$directoryPath directory exists..."
    }
    else {
        Write-Output "Creating $directoryPath Directory..."
        New-Item -Path $directoryPath -ItemType Directory
    }
}

Function CleanBinaries() {
    Write-Output "Clearing all binaries...."
    Remove-Item debug/bin/*.bin
    Remove-Item debug/obj/*.o
    Remove-Item debug/elf/*.elf
    Remove-Item debug/objdump/*.objdump
    Remove-Item *.log
}

Function Build() {
    Write-Output "Creating Output Directories..."
    Create-Directory('debug\bin')
    Create-Directory('debug\obj')
    Create-Directory('debug\objdump')
    Create-Directory('debug\elf')
    
    Write-Output "compile boot.asm"
    fasm boot/boot.asm debug/bin/boot.bin
    
    Write-Output "Compile second stage loader"
    wsl gcc -g -m32 -ffreestanding -c boot/*.c drivers/*.c utils/*.c drivers/ata/*.c -nostartfiles -nostdlib
    Move-Item *.o 'debug/obj'
    
    Write-Output "link all c object files"
    wsl ld -m elf_i386 -nostdlib -T ld/loader.ld debug/obj/*.o -o debug/elf/loader.elf
    
    Write-Output "compile loader.asm"
    fasm boot/loader.asm debug/obj/loader.o
    
    Write-Output "Link loader asm and loader c"
    wsl ld -m elf_i386 -nostdlib -T ld/loader.ld debug/obj/loader.o debug/elf/loader.elf -o debug/elf/loader_full.elf
    
    Write-Output "objdump loader file"
    objdump -phxd debug/obj/loader.o > debug/objdump/loader.objdump
    
    Write-Output "objdump loader C file"
    objdump -phxd debug/elf/loader_full.elf > debug/objdump/loader_full.objdump
    
    Write-Output "Producing elf file"
    wsl objcopy debug/elf/loader_full.elf -O binary debug/bin/loader.bin
    
    Write-Output "Padding loader bin file"
    $loader_file_size=$(wsl wc -c debug/bin/loader.bin).Split(" ")[0]
    $loader_sectors=[math]::ceiling($loader_file_size/512)
    $loader_final_bytes=$loader_sectors * 512
    $loader_pad_bytes=$loader_final_bytes-$loader_file_size
    wsl dd if=/dev/zero of=debug/bin/loader.bin bs=1 count=$loader_pad_bytes seek=$loader_file_size
    
    Write-Output "Clearing .o and .elf"
    Remove-Item debug/obj/*.o
    Remove-Item debug/elf/*.elf
    
    Write-Output "Compiling Kernel...."
    wsl gcc -g -m32 -ffreestanding utils/*.c drivers/*.c kernel/*.c -nostartfiles -nostdlib -o kernel.o -T ld/kernel.ld
    Move-Item *.o 'debug/obj'
    #wsl ld -m elf_i386 -nostdlib -T linker.ld *.o -o prog.elf
    objdump debug/obj/kernel.o -phxd > debug/objdump/kernel.objdump
    
    Write-Output "Creating Empty Raw 10 MB HDD Image..."
    wsl dd if=/dev/zero of=debug/bin/hdd.bin bs=1024 count=10240
    
    Write-Output "Writing Kernel to HDD Image...."
    $second_kernel_size=$(wsl wc -c debug/obj/kernel.o).Split(" ")[0]
    wsl dd if=debug/obj/kernel.o of=debug/bin/hdd.bin count=$second_kernel_size conv=notrunc
}

Function Make()
{
    Write-Output "Creating image...."
    Get-Content debug/bin/boot.bin, debug/bin/loader.bin, debug/bin/hdd.bin -Raw | Set-Content debug/bin/os_image.bin -NoNewline
}

Function RunOS()
{
    Write-Output "Launching QEMU"
    qemu-system-x86_64 debug/bin/os_image.bin
    # #bochs -f bochsconfig.conf
    # #bochsdbg -f bochsconfig.conf
}

if($paramName -eq "all" -Or $Null -eq $paramName)
{
    CleanBinaries
    Build
    Make
    RunOS
}

if($paramName -eq "clean")
{
    CleanBinaries
}

if($paramName -eq "build")
{
    Build
}

if($paramName -eq "make")
{
    Make
}

if($paramName -eq "run")
{
    RunOS
}

About the Author

6 thoughts on “13 – Developing an Operating System – Tutorial – Episode 7 – Second Stage Loader, Load Kernel at 0x10000 (1M) and Cleanup – OSDEV

Leave a Reply

Your email address will not be published. Required fields are marked *