ARM64 OS Handbook
🔍

Chapter 1: What is an Operating System?

What You Will Learn in This Chapter
  • What an operating system is and why computers need one
  • The difference between kernel space and user space
  • How system calls let programs request services from the kernel
  • The difference between monolithic and microkernel architectures
  • How the boot process works at a high level
  • How to set up a development environment and run a minimal kernel

1.1 What is an Operating System?

An operating system, or OS, is the most important program that runs on a computer. It manages all the hardware and software, and it provides a way for applications to interact with the hardware without needing to know the details of how that hardware works.

Without an operating system, a computer is just a collection of electronic parts. If you wanted to write a program without an OS, you would need to write code that directly controls the CPU, reads keystrokes from the keyboard byte by byte, sends pixel data to the screen one pixel at a time, manages your own memory to avoid overwriting other programs, and handles every network packet yourself. This would be incredibly difficult. Every program would need to be a complete operating system itself.

This is why operating systems exist. They provide three essential things:

Abstraction. The OS hides the complexity of hardware behind simple interfaces. When you call fopen("file.txt", "r"), your program does not need to know anything about the disk hardware. The OS handles all that complexity and just returns a file handle.

Resource management. A computer has limited resources: CPU time, memory, disk space, network bandwidth. Without an OS, one program could take all the CPU time and never let anything else run. The OS decides who gets what, when, and for how long.

Protection. Programs should not be able to interfere with each other. A bug in one program should not crash the entire computer. The OS creates boundaries between programs so they cannot access each other's memory without permission.

An Analogy: The Office Building

Imagine an office building with many companies working inside. Each company is like a program running on a computer.

Office BuildingOperating System
The building structure (walls, electricity, plumbing)Hardware (CPU, memory, disk)
Building managementOperating system
Companies working in the buildingApplications (programs)
Each company has its own locked officeMemory protection (programs isolated from each other)
Employees call building management for repairsSystem calls (programs request OS services)
Building management schedules meeting room useScheduler (decides which program gets CPU time)
Security guards check badges at the doorProtection (prevents unauthorized access)

Just as building management handles electricity, water, security, and cleaning so that companies can focus on their actual work, the operating system handles hardware details so that applications can focus on what they are designed to do.

1.2 What Does an Operating System Do?

Every operating system performs several key tasks. These are not optional; every general-purpose OS must handle them:

TaskWhat It Does
Process managementCreates, runs, and stops programs. Decides which program gets the CPU at any moment.
Memory managementTracks which memory is in use. Allocates memory to programs and reclaims it when they finish.
File system managementOrganizes data on disks. Provides the file and directory abstraction.
Device managementCommunicates with hardware devices through drivers.
Security and protectionEnforces boundaries between programs. Controls who can access what.
NetworkingManages network connections and data transfer between computers.

These tasks are not independent. When you open a file, the file system finds the data on disk, the memory manager loads it into memory, the process manager schedules your program so it can process the data, and protection ensures no other program can read it without permission. The OS coordinates all of this automatically.

1.3 Kernel Space vs User Space

One of the most important concepts in operating systems is the division of memory into two regions: kernel space and user space.

Kernel space is where the kernel lives. The kernel is the core of the operating system. It runs with the highest privileges and has access to all hardware and all memory. Code running in kernel space can do anything.

User space is where applications run. Code in user space has limited privileges. It cannot access hardware directly, cannot access kernel memory, and cannot perform privileged operations. If a user-space program crashes, the kernel is not affected.

            graph TD
                subgraph User Space
                    A[Application 1]
                    B[Application 2]
                end
                subgraph Kernel Space
                    C[Kernel]
                    D[Device Drivers]
                end
                subgraph Hardware
                    E[CPU]
                    F[Memory]
                    G[Disk]
                end
                A -->|System Calls| C
                B -->|System Calls| C
                C --> E
                C --> F
                C --> G
            

Figure 1.1: User space and kernel space. Applications communicate with the kernel through system calls.

The separation exists for protection. If an application runs with full privileges and crashes, it could corrupt kernel data, damage hardware, or steal data from other programs. By limiting what user-space programs can do, the OS ensures that a buggy program can only hurt itself.

1.4 System Calls: The Bridge Between User and Kernel

When a user-space program needs something that requires higher privileges (reading a file, sending network data, allocating memory), it cannot just do it directly. Instead, it makes a system call.

A system call is a request from user space to kernel space. The program says, "Kernel, please perform this operation on my behalf." The kernel checks whether the request is valid, performs it, and returns the result.

Common system calls include:

System CallWhat It Does
read()Read data from a file or device
write()Write data to a file or device
open()Open a file for reading or writing
close()Close a file descriptor
fork()Create a new process
exec()Replace the current process with a new program
mmap()Map files or devices into memory

When you call fopen() in C, the C library calls the open() system call. The CPU switches from user mode to kernel mode, the kernel opens the file and returns a handle, and the CPU switches back to user mode. Your program never knows this happened.

1.5 Monolithic vs Microkernel Architectures

Operating systems can be structured in different ways. The two main architectures are monolithic kernels and microkernels.

Monolithic Kernel

In a monolithic kernel, all operating system services run in kernel space. The file system, device drivers, networking stack, and memory manager all live together in the kernel binary. They can call each other's functions directly.

Examples: Linux, BSD Unix, MS-DOS.

AdvantagesDisadvantages
Fast: no message passing between componentsLarge: a bug in any component can crash the entire system
Simple design: all code can call each other directlyHard to maintain as the codebase grows
Well-understood: many examples exist (Linux)Driver bugs can bring down the whole kernel

Microkernel

In a microkernel, the kernel is kept as small as possible. Only the most essential services run in kernel space: scheduling, basic memory management, and inter-process communication (IPC). Everything else (file systems, drivers, networking) runs as user-space programs.

Examples: Minix, QNX, seL4.

AdvantagesDisadvantages
Small kernel with fewer bugsSlower: components communicate through message passing
A driver crash does not crash the systemComplex design with more moving parts
More secure: less code runs with high privilegeHarder to implement correctly

1.6 How a Computer Boots

When you press the power button, a sequence of events begins that eventually loads and starts the operating system. Understanding this sequence is important because our kernel must handle every step after the bootloader.

            graph LR
                A[Power On] --> B[Firmware]
                B --> C[Bootloader]
                C --> D[Kernel Entry]
                D --> E[Kernel Initialization]
                E --> F[First Process]
                F --> G[Shell / GUI]
            

Figure 1.2: The boot sequence from power-on to a running system.

  1. Power On. When power is applied, the CPU resets and begins executing code at a fixed address. This address points to the firmware.
  2. Firmware. The firmware (UEFI on modern systems) initializes basic hardware, runs self-tests, and then looks for a bootloader on disk.
  3. Bootloader. The bootloader loads the kernel from disk into memory, sets up basic system state, and jumps to the kernel entry point.
  4. Kernel Entry. The kernel begins executing. It must set up exception handlers, initialize memory management, configure the MMU, and prepare the system to run user-space code.
  5. Kernel Initialization. The kernel initializes all its subsystems: the scheduler, memory allocator, file system, device drivers, and networking stack.
  6. First Process. The kernel creates the first user-space process (traditionally called init) and starts it.
  7. Shell / GUI. The first process starts the user interface, and the system is ready.

1.7 Our Implementation

Now that we understand what an operating system is and how it works, let us see how these concepts apply to the kernel we are going to build.

What We Are Building

We are building a monolithic kernel for the ARMv8-A architecture in 64-bit mode (AArch64). The kernel will run at Exception Level 1 (EL1), and user-space applications will run at Exception Level 0 (EL0).

Development Platform

We develop using QEMU, which emulates an ARM64 computer. This lets us test our kernel without needing real hardware. Later, we will port the kernel to run on Raspberry Pi 4 and Raspberry Pi 5.

Setting Up the Environment

Before we can write kernel code, we need a cross-compiler. A cross-compiler runs on your computer (which might be x86) but generates code for ARM64. We also need QEMU to run the kernel and GDB to debug it.

Install these tools:

# Linux (Ubuntu/Debian)
sudo apt install build-essential qemu-system-arm gdb-multiarch
sudo apt install gcc-aarch64-linux-gnu

# macOS (Homebrew)
brew install qemu arm-none-eabi-gcc gdb

Our First Kernel

Let us write the smallest possible kernel that prints a message and halts. This will be our starting point. As we progress through the book, we will add more features until we have a complete operating system.

The kernel consists of three files:

kernel.ld - The linker script that tells the linker where to place code in memory:

ENTRY(_start)

SECTIONS
{
    . = 0x40000000;

    .text : { *(.text._start) *(.text*) }
    .rodata : { *(.rodata*) }
    .data : { *(.data*) }
    .bss : { *(.bss*) }
}

start.S - The assembly entry point that sets up the stack and jumps to C code:

.section .text._start
.global _start

_start:
    ldr x0, =_stack_end
    mov sp, x0
    bl kernel_main
    wfi
    b halt

kernel.c - The C code that prints a message to the UART serial port:

#define UART_BASE 0x09000000

void kernel_main(void) {
    volatile char *uart = (volatile char *)UART_BASE;
    const char *msg = "Hello from our kernel!\r\n";
    while (*msg) *uart = *msg++;
    while (1) __asm__("wfi");
}

Build and run it:

aarch64-none-elf-gcc -c -O2 -ffreestanding -Wall -Wextra kernel.c -o kernel.o
aarch64-none-elf-as start.S -o start.o
aarch64-none-elf-ld -T kernel.ld start.o kernel.o -o kernel.elf
qemu-system-aarch64 -M virt -cpu cortex-a72 -nographic -kernel kernel.elf

If everything works, you will see Hello from our kernel! printed in the terminal. Press Ctrl-A X to exit QEMU.

Key Points About This Code
  • The kernel loads at address 0x40000000 (the default load address for QEMU virt)
  • The UART is at physical address 0x09000000; writing a byte to this address sends it to the serial port
  • The volatile keyword prevents the compiler from optimizing away the write to the UART address
  • The wfi instruction puts the CPU into a low-power wait state until an interrupt occurs
  • The -ffreestanding flag tells the compiler that we are building a kernel, not a user-space program, so it should not assume a standard library is available

1.8 Exercises

Try these exercises to make sure you understand the material before continuing.

Exercise 1: Explain the Office Analogy

In your own words, explain how the office building analogy maps to an operating system. Write three to four sentences as if explaining to someone who has never heard of an OS.

Exercise 2: Kernel vs User Space

For each operation, state whether it runs in kernel space or user space:

  1. Typing text in a word processor
  2. Reading data from a hard drive
  3. Responding to a keyboard interrupt
  4. Creating a new process
  5. Running a web browser

Exercise 3: Monolithic vs Microkernel

Create a table with at least five differences between monolithic and microkernel architectures. Then write one paragraph explaining which you would choose for a medical device and why.

Exercise 4: Trace a System Call

When a program calls read(fd, buffer, size), describe the sequence of events from the application to the kernel and back. Include the C library, the kernel's system call handler, and the device driver.

Exercise 5: Extend the Hello Kernel

Modify the kernel.c file to print your name and a number counter from 1 to 10. You will need to implement a function that converts an integer to a string, since we cannot use printf.

Exercise 6: Fibonacci (Challenge)

Extend the kernel to print the first 20 Fibonacci numbers using the UART. Each number should be on a new line. This requires implementing integer-to-string conversion.

1.9 Summary

In this chapter, we learned that an operating system is a manager for computer hardware and software. It provides abstraction (hiding hardware complexity), resource management (sharing CPU, memory, and devices fairly), and protection (isolating programs from each other).

We explored the division between kernel space (where the privileged kernel runs) and user space (where applications run). System calls are the bridge between them: applications request services, and the kernel performs them.

We compared monolithic kernels, where all services run in kernel space, with microkernels, where only the essentials run in kernel space. Our kernel will use a monolithic design.

Finally, we set up our development environment and ran our first kernel. In the next chapter, we will take a deeper look at how computers are structured at the hardware level.