OS & Kernel

OS & Kernel

Ohh, I see you have taken up the realm of Operating Systems and Kernels. Though it shares its land with other areas of computer science, this domain holds a certain mystique — a place where software dances closely with hardware. It's the foundational layer that makes everything else on your computer possible, managing resources and providing services to applications.

Let's begin at the very beginning: how a system awakens from its digital slumber. When you press the power button on a machine made of billions of silicon transistors, it doesn’t just magically spring to life with a desktop environment. Instead, a carefully orchestrated sequence of events, often referred to as the bootstrapping process or boot sequence, takes place, transferring control from the hardware's initial state to the fully loaded operating system.

The Boot Sequence: From Silicon to System

The journey starts the instant power flows into the motherboard:

  1. Initial Hardware State (CPU Reset): The very first step. When power is supplied, the CPU is reset. This reset forces the CPU into a predefined state. Crucially, it loads a specific, hardcoded memory address into its program counter register (like IP or RIP). This address isn't in RAM; it points to a fixed location in the system's firmware, typically the BIOS or UEFI ROM chip on the motherboard. The CPU immediately begins executing the instruction found at this firmware address. This is the very start of the bootstrap.
  2. BIOS/UEFI Execution: The code at the firmware address is the start of the BIOS (Basic Input/Output System) or UEFI (Unified Extensible Firmware Interface) firmware. This firmware is stored in non-volatile memory (like a flash chip) on the motherboard.

    Its first major task is the POST (Power-On Self-Test). The POST checks essential hardware components like the CPU, RAM, and graphics card to ensure they are working correctly. If POST finds a critical error (e.g., no RAM detected), it will typically halt the boot process and signal the error (often with beeps or error codes).

    Next, the firmware initializes basic hardware needed to boot, such as the disk controllers. It then consults its configuration to determine the boot order – which devices (like a hard drive, SSD, USB drive, or network) it should check for a bootable operating system.

    Finally, the firmware locates the designated boot device and searches for the initial stage of the bootloader, typically in a specific location like the Master Boot Record (MBR) on older systems or a UEFI System Partition (ESP) on modern ones. Once found, the firmware loads this small piece of code into memory and transfers control to it. The bootstrap process transitions from firmware to bootloader.

  3. Bootloader Stage 1/2: The code loaded by the firmware is the first stage of the bootloader (e.g., a tiny piece of GRUB, LILO, or the Windows Boot Manager). Because the initial space available (like the MBR's 512 bytes) is tiny, this first stage usually just contains enough code to load a larger, more capable second stage of the bootloader.

    The second stage (which runs in a more capable environment with more memory available) is responsible for understanding the file system on the boot partition, finding the operating system kernel image (e.g., vmlinuz on Linux, ntoskrnl.exe on Windows), and loading it into the system's RAM. It might also load an initial RAM disk (initrd or initramfs) which contains essential drivers and utilities needed early in the boot process.

    Before transferring control, the bootloader sets up the CPU's state appropriately (e.g., switching from real mode or protected mode to long mode for 64-bit systems) and passes necessary information (like memory layout and hardware details) to the kernel. Then, it jumps to the kernel's entry point.

  4. Kernel Initialization: Control is now passed to the operating system kernel, the core of the OS. The kernel is initially compressed and might need to decompress itself into memory. It then performs a series of critical initialization tasks:
    • Setting up its own data structures.
    • Initializing memory management (setting up page tables, virtual memory).
    • Detecting and initializing hardware devices and loading necessary device drivers (either built-in or from the initial RAM disk).
    • Initializing process management structures.
    • Setting up interrupts and system calls.

    At this stage, the kernel is running in a privileged mode (kernel space) with full control over the hardware, but the user environment isn't ready yet.

  5. Init System / Service Manager Startup: Once the kernel has initialized itself and the essential hardware, its final act in the boot sequence is to launch the very first user-space process. This process always has Process ID (PID) 1 and is typically an Init System or Service Manager (like systemd on most Linux distributions, launchd on macOS, or the Service Control Manager on Windows).

    The Init System is responsible for reading configuration files and starting all other necessary user-space processes, services, and daemons required for the system to be fully operational. This includes things like mounting file systems, starting networking services, setting up login prompts, and eventually launching the graphical environment (if applicable).

  6. Operating System (User Space) Ready: With the Init System having launched all necessary services, the operating system is now fully booted and ready for user interaction. This is the environment where user shells (like bash), graphical interfaces (like GNOME or the Windows Desktop), and all applications run. These programs operate in user space and rely on the kernel to provide services via system calls.

For this post, I’ll primarily use Linux as a reference, as it uses a monolithic kernel architecture and widely adopts standardized concepts like init systems (specifically systemd in many modern distributions) and Dynamic Kernel Module Support (DKMS), which allows drivers and kernel modules to be built and loaded easily. Windows, by contrast, uses a hybrid kernel model, and macOS uses a microkernel-like architecture called XNU. Understanding Linux provides a solid foundation applicable to many other OS concepts.

Alright, but what are these components really?

Component Description
BIOS/UEFI The initial firmware that runs after power-on, performs hardware tests (POST), initializes basic devices, and loads the bootloader from storage.
Bootloader A small program loaded by the firmware that finds, loads, and prepares the OS kernel in memory before transferring control to it (e.g., GRUB, LILO, systemd-boot, Windows Boot Manager).
Kernel The central core of the OS, running in a privileged mode. It manages all system resources: CPU, memory, hardware devices, processes, and provides core OS services through system calls.
Operating System (User Space) The collection of system programs, utilities, libraries, shells, and graphical interfaces that run on top of the kernel in an unprivileged mode. This is the environment the user directly interacts with and where applications run.
Init System / Service Manager The very first user-space process (PID 1) started by the kernel. It reads configuration and launches all other necessary system services and daemons to bring the system to a usable state (e.g., systemd, launchd, Service Control Manager).

But wait — why should we care?

Understanding the OS and kernel is the key to mastering your machine and truly understanding how software interacts with hardware. It's the difference between being a user of a system and an architect or engineer of systems. It demystifies the complex layers of software and hardware. You'll gain deep insights into:

Whether you’re building high-performance applications, writing device drivers, working with embedded systems, troubleshooting complex system issues, contributing to open-source OS projects, or even considering writing your own minimal OS, this low-level knowledge becomes your sword and shield. Welcome to the realm of rings — understanding each layer brings you closer to the fundamental operations of the machine.

Required Knowledge

To effectively dive into this domain, a grasp of the following is highly beneficial:

Let's delve into these prerequisites, starting with x86 Assembly.

🛡️ x86 Assembly – Your Gateway to the Machine

Assembly language is the closest human-readable representation of the machine code that your CPU executes. While it might seem cryptic at first glance, it's remarkably logical, directly reflecting the atomic operations the processor can perform. Each instruction typically corresponds to a single, very basic task for the CPU.

Example of a simple instruction:

mov ax, 100

In this line, mov is the mnemonic for the operation (move data), ax is the destination operand (a 16-bit register), and 100 is the source operand (an immediate value). This instruction tells the CPU to place the value 100 into the ax register.

Many believe software is magical — a realm without limits. But in truth, all complex software boils down to executing a sequence of these basic operations billions of times per second. We rely on well-defined instruction set architectures (ISAs) like x86. Modern CPUs operate at frequencies like 4–5 GHz, meaning billions of cycles per second — and each cycle can execute one or more instructions. All this incredible computational power is packed into a small chip. Truly remarkable.

🔧 Registers in x86

Registers are small, high-speed storage locations directly within the CPU. They are used to hold data and addresses that the CPU is actively working with. Understanding registers is crucial because assembly instructions operate heavily on them. They are the CPU's workspace.

In the context of 32-bit x86 (often referred to as IA-32), you'll primarily encounter 8 general-purpose registers, each 32 bits wide (hence the 'E' prefix for 'Extended'):

One powerful feature of x86 is the ability to access smaller parts of these 32-bit registers:

Note that 64-bit x86 (x86-64 or AMD64) extends these registers (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP are 64 bits wide) and introduces 8 additional general-purpose registers (R8 through R15), making assembly programming more flexible and providing more workspace directly on the CPU.

📦 Declaring Data in Memory

Assembly allows you to reserve space in memory for data. Static data is often declared in a dedicated data section (often named .data or .DATA depending on the assembler syntax) using directives that specify the size of the data element you wish to store:

					section .data ; Example using NASM/GAS syntax - defines a data segment
					var1 db 64 ; Define Byte initialized with value 64 (decimal)
					var2 db 0x40 ; Define Byte initialized with value 64 (hexadecimal)
					var3 db 'A' ; Define Byte initialized with the ASCII value of 'A' (65)
					uninit_byte db ? ; Define Byte, uninitialized (its content is undefined)
					count dw 0 ; Define Word initialized to 0 (2 bytes)
					X dd ? ; Define Double Word, uninitialized (4 bytes)
					Y dd 30000 ; Define Double Word initialized to 30000 (decimal)
					Z dd 1,2,3,5 ; Define Double Words, an array of 4 integers
					buffer db 10 dup(?) ; Reserve 10 Bytes, uninitialized. dup(?) is specific syntax for reserving
					space.
					message db 'hello', 0 ; Define Bytes, a null-terminated string (0 is the null byte terminator)

The label before the directive (e.g., var1, message, buffer) is a symbolic name that represents the starting memory address where the data is stored. These labels make code more readable than using raw memory addresses.

📍 Addressing Modes

The x86 architecture provides flexible ways to access data in memory using various addressing modes. These modes define how the effective memory address, where the data resides, is calculated. In 32-bit mode, the addressable memory space is 4 GB (from address 0 to 232-1). Understanding addressing modes is key to manipulating data in memory.

Common addressing modes include:

					mov eax, [ebx] ; Register Indirect: Load value from memory pointed by EBX
					mov [var], ebx ; Direct Addressing (with label): Store EBX into memory at address 'var'
					mov eax, [esi-4] ; Based/Indexed with displacement: Load from memory at address ESI - 4
					mov [esi+eax], cl ; Based-Indexed: Store CL at address ESI + EAX
					mov edx, [esi+4*ebx] ; Scaled-Indexed-Based: Load from memory at address ESI + (4 * EBX)

Understanding how memory is organized and how to access it using registers and addressing modes is fundamental to low-level programming and essential for understanding how the OS manages memory for processes, sets up stack frames for functions, and interacts with hardware devices mapped into memory.

📚 Resources for x86 Assembly

To deepen your understanding of assembly, explore these resources:

Recommended Books on Assembly

These books offer structured learning paths for assembly language:

Mastering assembly gives you the ability to see the world from the CPU's perspective, understanding exactly how instructions manipulate data in registers and memory. This low-level view is absolutely essential when working on operating systems.

System Programming Language: The Indispensable C

To build an operating system, kernel modules, or interact closely with hardware, you need a language that offers direct control over memory and low-level system interfaces without the complexity of writing everything in assembly. This is where system programming languages come in. For decades, C has been the dominant language for OS development, and for good reasons. I strongly recommend getting familiar with C. I will use C examples throughout this journey because:

You may hear compelling arguments for using Rust in place of C for new system-level development, especially given its strong memory safety guarantees without needing a garbage collector. Rust *is* an excellent, modern language for system programming and is increasingly being adopted in kernel development (e.g., the Rust for Linux project). It is designed to prevent common memory-related bugs (like data races in safe code) that are frequent sources of vulnerabilities and crashes in C programs, especially in 'safe' Rust code. While memory safety issues can technically still occur in Rust code that explicitly uses unsafe blocks, the language's design significantly reduces the occurrence of these critical error types compared to typical C programming.

Despite Rust's significant advantages in memory safety and modern language features, C's unparalleled maturity, enormous existing codebase in critical system software, established tooling, and widespread use in embedded systems and legacy OS components make it the essential starting point for anyone wanting to truly understand how operating systems are built and function at a fundamental level today. Understanding both C and assembly empowers you to reason about program execution at the lowest levels, which is crucial when working on or within an operating system.

User Space vs. Kernel Space: The Fundamental Divide

A fundamental concept in modern operating systems, crucial for security and stability, is the division of virtual memory and CPU privilege levels into two distinct modes: user space and kernel space.

This strict separation ensures that a faulty or malicious user application cannot directly access or corrupt critical kernel data structures or hardware, thereby preventing it from crashing the entire system or compromising its security. If a user process attempts to perform a privileged operation or access restricted memory, the CPU detects this violation and triggers a trap or fault, transferring control back to the kernel to handle the error (typically by terminating the offending process).

System Calls: The Controlled Interaction

Since user-space applications cannot directly access hardware or perform privileged operations themselves (like creating a new process, reading/writing files on disk, or sending data over a network), they must request these services from the kernel. This is done through a well-defined interface called a system call.

A system call is essentially a request from a user-space process to the kernel to perform a specific task on its behalf. It involves a controlled transition from the less privileged user space to the more privileged kernel space. When a user program makes a system call (often indirectly through standard library functions like printf, which eventually call write), the program prepares the necessary arguments (like file descriptors, buffers, sizes) and then executes a special instruction (like syscall on x86-64 Linux, sysenter, or the legacy int 0x80 on 32-bit x86) that triggers a software interrupt or trap. This transfers execution control to a predefined entry point within the kernel.

The kernel's system call handler then takes over. It saves the user process's state, validates the arguments passed from user space to prevent security vulnerabilities, executes the requested operation in the trusted kernel environment (e.g., accessing the file system to read a file), and once the operation is complete, it prepares the return value and status. Finally, the kernel restores the user process's state and returns control back to the user space, allowing the application to continue execution. This mechanism provides a safe and standardized way for applications to interact with the OS's core functionalities. Examples of common system calls include open(), read(), write(), close(), fork() (to create a new process), execve() (to run a new program), exit(), brk() or mmap() (for memory allocation - often managed by user-space libraries like `glibc`'s `malloc`), etc. Understanding system calls is fundamental to understanding the boundary between applications and the operating system kernel.

Processes and Memory Management: The Kernel's Core Tasks

Among the kernel's most critical responsibilities are managing processes and system memory.

Delving into these topics involves understanding complex algorithms for CPU scheduling (like preemptive multitasking), memory allocation (like buddy systems or slab allocators within the kernel), and the intricate workings of page table lookups and context switching.

So, fellow wanderer, the journey has only begun. Learning assembly and C provides you with the foundational tools to understand how software truly interacts with the underlying hardware and the operating system. Learn to wield your tools — the assembler, the C compiler, the kernel sources. Understand the runes (syscalls), tame the beasts (interrupts, which are hardware signals that demand the CPU's attention, like a key press or a disk operation completing), and soon, you shall speak in rings — navigating the sacred privilege levels and domains of modern computation. This exploration into the OS and kernel is challenging but incredibly rewarding, providing a deep understanding of what happens inside your computer, from the very first instruction executed at power-on.