(6) Kernel Start

박서경박서경
10 min read

🧠 1. What is a Kernel?

The kernel is the core of an operating system. It acts as a mediator between hardware and user applications.

Key Responsibilities:

FeatureDescription
Memory ManagementTracks memory usage, allocates and frees memory.
Process ManagementHandles execution and scheduling of multiple programs.
File SystemManages disk I/O and file operations.
Driver ManagementFacilitates communication with hardware devices.
System CallsAllows user programs to request services from the OS.

⚙️ 2. What Should the Kernel Look Like After the Bootloader?

When booting using UEFI and an ELF binary, like Skylar’s setup, the kernel must meet these essential requirements:

✅ Core Requirements:

  • Fixed link address (e.g., 0x100000)

  • Pure 64-bit binary (use -ffreestanding, -m64)

  • Must define an entry point function (e.g., kernel_main)

  • Must run independently after ExitBootServices()

  • Should be a well-formed ELF with .text, .data, .bss sections properly separated

📄 3. Writing and Building the Kernel

As a first step, we'll create a basic kernel and test loading it using our bootloader.

kernel.c

#include <stdint.h>  // 표준 정수 타입 포함

typedef struct {
    void* FrameBufferBase;
    unsigned int HorizontalResolution;
    unsigned int VerticalResolution;
    unsigned int PixelsPerScanLine;
} FrameBufferInfo;

void kernel_main(FrameBufferInfo* fbInfo) {
    uint32_t* fb = (uint32_t*)fbInfo->FrameBufferBase;
    uint32_t color = 0x00FF00FF;  // ARGB: Magenta


    while (1) {
    for (unsigned int y = 0; y < fbInfo->VerticalResolution; y++) {
        for (unsigned int x = 0; x < fbInfo->HorizontalResolution; x++)    
            {
            fb[y * fbInfo->PixelsPerScanLine + x] = color;
            }
        }
    }
}

✅ The overall meaning of the structure

typedef struct {
    void* FrameBufferBase;
    unsigned int HorizontalResolution;
    unsigned int VerticalResolution;
    unsigned int PixelsPerScanLine;
} FrameBufferInfo;

This struct holds the four key parameters required for direct pixel rendering to the screen.


1. void* FrameBufferBase;

The starting address of the framebuffer in memory.

From this address, graphical data is written sequentially — one pixel per 4 bytes — in a linear memory layout.

Typically, this address points to a region of RAM reserved by the GPU for video output.

For example, it might look like 0x00000000C0000000.

예시 사용:

uint32_t* fb = (uint32_t*)fbInfo->FrameBufferBase;
fb[0] = 0x00FF00FF;  // 첫 픽셀에 자홍색 칠하기 (ARGB)

2.unsigned int HorizontalResolution;

The number of horizontal pixels on the screen (X-axis resolution).

Example: 1920 → The horizontal resolution in Full HD.

Commonly used as the x-coordinate in rendering loops.


3.unsigned int VerticalResolution;

The number of vertical pixels on the screen (Y-axis resolution).

Example: 1080 → The vertical resolution in Full HD.

Commonly used as the y-coordinate in rendering loops.


4.unsigned int PixelsPerScanLine;

The number of pixels actually allocated per scanline in memory.

⚠️ Note: This value can be greater than HorizontalResolution!

Why?
To align memory to 4-byte boundaries or improve GPU performance, padding may be added to each scanline.

This field is essential for correctly calculating pixel addresses.

예시:


fb[y * PixelsPerScanLine + x] = color;

🎯 Why is this structure necessary?

In low-level graphics environments — such as UEFI applications or early kernel initialization
the CPU must manipulate the screen without a GPU driver.

This structure, provided by UEFI, allows the system to:

  • Determine the screen resolution

  • Access the framebuffer memory address

  • Calculate pixel positions in memory

  • Draw pixels in desired colors

All of this enables direct pixel rendering to the screen at a very early stage of system boot.

kernel.ld

ENTRY(kernel_main)

SECTIONS {
    . = 0x100000;

    .text : {
        *(.text*)
    }

    .data : {
        *(.data*)
    }

    .bss : {
        *(.bss*)
    }
}

SECTIONS { . = 0x100000;

This directive means the entire kernel binary will start at physical memory address 0x100000 (1MB).

Traditionally, on x86 systems, 1MB is considered a safe starting point for the kernel after transitioning out of real mode, bootloader execution, or UEFI setup.

⚠️ However, this address is only valid if the bootloader explicitly allocates it using AllocatePages() or a similar memory allocation function.

✅ 1단계: Compile Kernel (kernel.c → kernel.o)

x86_64-elf-gcc -ffreestanding -m64 -c kernel.c -o kernel.o
  • -ffreestanding

    Indicates that the code is being compiled in a freestanding environment,
    such as an operating system kernel or bootloader, without relying on the standard C library or startup routines provided by a typical OS.

    This tells the compiler not to assume the existence of functions like main(), printf(), or exit().


    -m64

    Instructs the compiler to generate code for a 64-bit target architecture.

    Specifically, this enables x86-64 code generation instead of 32-bit (x86) or other mode


x86_64-elf-ld -T kernel.ld -o kernel.elf kernel.o --oformat=elf64-x86-64
  • -T kernel.ld

    Specifies a custom linker script to be used during the linking process.
    In this case, kernel.ld defines how the sections of the output binary are laid out in memory (e.g., the start address, memory segments, etc.).

    This gives full control over the memory layout — essential for OS kernels or bare-metal code.


    --oformat=elf64-x86-64

    Forces the output format to be 64-bit ELF (Executable and Linkable Format) for the x86-64 architecture.

    This ensures compatibility with 64-bit UEFI and bootloaders that expect a specific binary format.

📄 BootLoader.c

//BootLoader.c

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DevicePathLib.h>
#include <Protocol/SimpleFileSystem.h>
#include <Protocol/LoadedImage.h>
#include <Guid/FileInfo.h>

// Define FrameBufferInfo struct to pass to kernel
typedef struct {
    void* FrameBufferBase;
    unsigned int HorizontalResolution;
    unsigned int VerticalResolution;
    unsigned int PixelsPerScanLine;
} FrameBufferInfo;

typedef void (*KernelEntry)(FrameBufferInfo*);

EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) {
    EFI_STATUS Status;
    EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
    EFI_FILE_PROTOCOL *RootDir, *KernelFile;
    EFI_FILE_INFO *FileInfo;
    UINTN FileInfoSize = 0;
    VOID *KernelBuffer = NULL;
    KernelEntry EntryPoint;
    FrameBufferInfo fbInfo;

    Print(L"[UEFI] Skylar's BootLoader Starting...\n");

    // Get loaded image protocol
    Status = gBS->HandleProtocol(ImageHandle, &gEfiLoadedImageProtocolGuid, (VOID**)&LoadedImage);
    if (EFI_ERROR(Status)) return Status;

    // Get file system protocol
    Status = gBS->HandleProtocol(LoadedImage->DeviceHandle, &gEfiSimpleFileSystemProtocolGuid, (VOID**)&FileSystem);
    if (EFI_ERROR(Status)) return Status;

    // Open root directory
    Status = FileSystem->OpenVolume(FileSystem, &RootDir);
    if (EFI_ERROR(Status)) return Status;

    // Open kernel.elf
    Status = RootDir->Open(RootDir, &KernelFile, L"kernel.elf", EFI_FILE_MODE_READ, 0);
    if (EFI_ERROR(Status)) {
        Print(L"[ERROR] Cannot open kernel.elf\n");
        return Status;
    }

    // Get file size
    Status = KernelFile->GetInfo(KernelFile, &gEfiFileInfoGuid, &FileInfoSize, NULL);
    if (Status == EFI_BUFFER_TOO_SMALL) {
        Status = gBS->AllocatePool(EfiLoaderData, FileInfoSize, (VOID**)&FileInfo);
        if (EFI_ERROR(Status)) return Status;

        Status = KernelFile->GetInfo(KernelFile, &gEfiFileInfoGuid, &FileInfoSize, FileInfo);
        if (EFI_ERROR(Status)) return Status;
    }

    // Allocate buffer for kernel
    Status = gBS->AllocatePages(AllocateAnyPages, EfiLoaderData,
        EFI_SIZE_TO_PAGES(FileInfo->FileSize), (EFI_PHYSICAL_ADDRESS*)&KernelBuffer);
    if (EFI_ERROR(Status)) return Status;

    // Read kernel into buffer
    UINTN KernelSize = FileInfo->FileSize;
    Status = KernelFile->Read(KernelFile, &KernelSize, KernelBuffer);
    if (EFI_ERROR(Status)) return Status;

    Print(L"[INFO] Kernel loaded at address: %p\n", KernelBuffer);

    // Setup framebuffer info (simplified: not really querying GOP here)
    fbInfo.FrameBufferBase = (VOID*)0x00000000; // replace with actual address if using GOP
    fbInfo.HorizontalResolution = 800; // fake values
    fbInfo.VerticalResolution = 600;
    fbInfo.PixelsPerScanLine = 800;

    // Entry point is at beginning for this simple binary
    EntryPoint = (KernelEntry)KernelBuffer;

    // Exit boot services
    UINTN MapSize = 0, MapKey, DescriptorSize;
    UINT32 DescriptorVersion;
    EFI_MEMORY_DESCRIPTOR *MemMap = NULL;

    gBS->GetMemoryMap(&MapSize, MemMap, &MapKey, &DescriptorSize, &DescriptorVersion);
    MapSize += DescriptorSize * 10;
    gBS->AllocatePool(EfiLoaderData, MapSize, (VOID**)&MemMap);
    gBS->GetMemoryMap(&MapSize, MemMap, &MapKey, &DescriptorSize, &DescriptorVersion);

    gBS->ExitBootServices(ImageHandle, MapKey);

    // Jump to kernel
    EntryPoint(&fbInfo);
    return EFI_SUCCESS;
}

Header File

타입용도예시
#include <Library/...>Functions and helpers used for implementationUefiLib.h, MemoryAllocationLib.h
#include <Protocol/...>Hardware interface definitions (interfaces, structures)SimpleFileSystem.h, GraphicsOutput.h
#include <Guid/...>GUID definitions (keys used to identify protocols or data)FileInfo.h, Acpi.h

Let me walk you through the main components of the code, one by one..

✅ 1. LoadedImage Protocol

Status = gBS->HandleProtocol(ImageHandle, &gEfiLoadedImageProtocolGuid, (VOID**)&LoadedImage);

🧠 Role:

The purpose of this stage is to determine which device this bootloader (.efi) was loaded from.
→ In other words, it identifies “Where am I running from?”


✅ 2. File system protocol

Status = gBS->HandleProtocol(LoadedImage->DeviceHandle, &gEfiSimpleFileSystemProtocolGuid, (VOID**)&FileSystem);

🧠 Role:

From the device identified earlier, retrieve a file system interface (e.g., FAT32).
→ This is necessary to open and load kernel.elf.


✅ 3. Open Root Directory

Status = FileSystem->OpenVolume(FileSystem, &RootDir);

📂 Role:

Open the root directory (i.e., the FAT32 root) of the device.
→ It is assumed that kernel.elf resides in this directory.


✅ 4. kernel.elf 파일 열기

Status = RootDir->Open(RootDir, &KernelFile, L"kernel.elf", EFI_FILE_MODE_READ, 0);

📄 Role:

Open the file named kernel.elf from the root directory in read-only mode.
→ This is the preparation step before loading it into memory.

📏 5. Retrieve the file size of kernel.elf.

Status = KernelFile->GetInfo(..., NULL);
if (Status == EFI_BUFFER_TOO_SMALL) {
    AllocatePool(...)       // FileInfo 구조체 공간 할당
    KernelFile->GetInfo(...) // 진짜 파일 크기 정보 읽기
}

📦Knowing the size is essential to allocate the right amount of memory before loading the file into RAM.


🧠 6. Allocate memory for the kernel and read kernel.elf into memory.

AllocatePages(...);    // 커널용 메모리 공간 확보
KernelFile->Read(...); // 커널 내용을 그 메모리에 읽어들임

🧠 목적: This prepares the kernel binary for execution by placing it in a memory region large enough to hold the full file.


🖥️ 7. FrameBuffer

fbInfo.FrameBufferBase = (VOID*)0x00000000;
fbInfo.HorizontalResolution = 800;
fbInfo.VerticalResolution = 600;
fbInfo.PixelsPerScanLine = 800;

🎯 Purpose:
Prepare screen information to pass to the kernel.
→ (Note: Currently, fake placeholder values are used instead of retrieving real data via GOP.)


🚪 8. Prepare to call ExitBootServices()

GetMemoryMap(...)
AllocatePool(...)     // MemMap 공간 확보
GetMemoryMap(...)     // 실제 메모리 맵 다시 가져옴
ExitBootServices(...) // UEFI 기능 종료

🚪 The point where the UEFI firmware hands over full control to the OS kernel.

→ Must ensure that all required memory is allocated and that no more UEFI services are needed afterward.


🚀 9. Enter Kernel

EntryPoint(&fbInfo);

🚀Jump to the entry point of kernel.elf, passing the framebuffer information as an argument.

→ This marks the actual transfer of control from the bootloader to the kernel.

📄 BootLoader.inf

[Defines]
  INF_VERSION    = 0x00010005
  BASE_NAME      = BootLoader
  FILE_GUID      = 3995fb85-fdfc-4c3a-9754-bcceedb7ef11
  MODULE_TYPE    = UEFI_APPLICATION
  ENTRY_POINT    = UefiMain

[Sources]
  BootLoader.c

[Packages]
  MdePkg/MdePkg.dec


[LibraryClasses]
  UefiLib
  UefiApplicationEntryPoint 
  UefiBootServicesTableLib
  MemoryAllocationLib
  BaseMemoryLib
  DevicePathLib

[Protocols]
  gEfiSimpleFileSystemProtocolGuid
  gEfiLoadedImageProtocolGuid

[Guids]
  gEfiFileInfoGuid

📄 QEMU

qemu-img create -f raw disk.img 200M

mkfs.fat -n 'OWN OS' -s 2 -f 2 -R 32 -F 32 disk.img

mkdir -p mnt
sudo mount -o loop disk.img mnt

sudo mkdir -p mnt/EFI/BOOT
sudo cp BootLoader.efi mnt/EFI/BOOT/BOOTX64.EFI

sudo cp kernel.elf mnt/                                

sudo umount mnt

qemu-system-x86_64 \
  -drive format=raw,file=disk.img \
  -bios /usr/share/ovmf/OVMF.fd

Hmm… when I run it, the magenta color only appears at the top of the screen.

Of course

fbInfo.FrameBufferBase = (VOID*)0x00000000;
fbInfo.HorizontalResolution = 800;
fbInfo.VerticalResolution = 600;
fbInfo.PixelsPerScanLine = 800;

hat’s because we inserted placeholder (fake) values instead of retrieving actual data via GOP (Graphics Output Protocol).

What is GOP (Graphics Output Protocol)?

GOP is a standard UEFI interface used to handle graphics output — particularly the framebuffer — during the pre-boot phase.
It provides services such as screen resolution configuration, pixel output, and framebuffer location information, all before the OS is loaded.


🔧 Why is GOP necessary?

In a UEFI boot environment, if the kernel or bootloader wants to draw directly to the screen, it must know the actual location of VRAM (video memory).
However, this location can vary across systems, QEMU settings, and firmware implementations.

GOP serves as a safe and standardized interface to retrieve this information reliably.


What is VRAM?

VRAM (Video RAM) is a region of memory that stores the color data of every pixel to be displayed on the screen.


🎯 Put simply:

The screen you see is made up of thousands (or millions) of pixels.
Each pixel’s color value must be stored somewhere so the GPU can read it and render the image on the monitor.

That “somewhere” is VRAM — the memory that holds all the pixel data for the screen.

✅Using GOP

📄 BootLoader.inf

[Protocols]
 gEfiGraphicsOutputProtocolGuid

Add protocol

📄 BootLoader.c


    Status = gBS->LocateProtocol(&gopGuid, NULL, (VOID**)&Gop);
    if (EFI_ERROR(Status)) {
        Print(L"Failed to get GOP\n");
        return Status;
    }

    // Fill fbInfo with real data
    fbInfo.FrameBufferBase = (VOID*)Gop->Mode->FrameBufferBase;
    fbInfo.HorizontalResolution = Gop->Mode->Info->HorizontalResolution;
    fbInfo.VerticalResolution = Gop->Mode->Info->VerticalResolution;
    fbInfo.PixelsPerScanLine = Gop->Mode->Info->PixelsPerScanLine;

change the part of GOP.

Success

0
Subscribe to my newsletter

Read articles from 박서경 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

박서경
박서경