From c568c9dedd62c1640c2bbd22318f7ff70f1d5498 Mon Sep 17 00:00:00 2001 From: Jakub Lisowski Date: Fri, 30 Jan 2026 01:17:04 +0100 Subject: [PATCH] Fixed listings --- src/thesis-en.tex | 84 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/thesis-en.tex b/src/thesis-en.tex index 32dadde..07e259d 100644 --- a/src/thesis-en.tex +++ b/src/thesis-en.tex @@ -2298,6 +2298,9 @@ \subsection{Thread} This approach simplifies the architecture significantly and enhances performance. It is worth noting that while certain architectures provide hardware-assisted context switching mechanisms (such as the legacy Hardware Task Switching on x86), these are rarely utilized in modern general-purpose kernels due to performance overhead and portability concerns. Instead, the standard approach is to unify context storage using the stack. Since the stack is heavily utilized, it is typically cached, eliminating the need to allocate separate memory regions for saving context. When saving context, registers are pushed onto the stack. When initializing a thread, the stack is artificially populated with boot-up values. \noindent A concise initial structure can be defined as follows: + +\clearpage + \begin{cppcode}[caption={Thread Structure First Iteration}, label={lst:os-tutorial-thread-v0}] struct Thread { Process* owner; @@ -2330,6 +2333,9 @@ \subsection{Thread} A critical design consideration regarding context switching is the location where the state is saved. Saving the execution state directly onto the user stack is inherently insecure. A malicious program could manipulate the stack pointer to overwrite critical kernel structures, or the kernel could unintentionally leak sensitive data (such as cryptographic keys) onto the user stack during a switch. Furthermore, to allow kernel code to be preempted or to block (i.e., perform a context switch while inside the kernel), the kernel state must be preserved. This necessitates a separate, protected kernel stack for each thread. Relying on a single kernel stack per core is insufficient for a preemptible kernel. \noindent The final minimal structure is presented below: + +\clearpage + \begin{cppcode}[caption={Thread Structure Third Iteration}, label={lst:os-tutorial-thread-v2}] enum class ThreadState : u64 { kReady = 0, @@ -2422,8 +2428,9 @@ \subsection{Scheduler} To illustrate these concepts, we present the simplest form of a preemptive scheduler: a periodic, single-core Round Robin scheduler. This algorithm maintains a single queue of executable tasks. New tasks are appended to the tail of the queue, while the scheduler selects the task at the head for execution. At fixed intervals (e.g., every 10ms), the running task is interrupted, placed back at the tail of the queue, and the next task is dequeued. The following pseudocode demonstrates this logic: -\begin{cppcode}[caption={Round Robin Pseudocode}, label={lst:rr_pseudo_code}] +\clearpage +\begin{cppcode}[caption={Round Robin Pseudocode}, label={lst:rr_pseudo_code}] #define SCHED_PERIOD_TICK 10 struct Scheduler { @@ -2701,6 +2708,9 @@ \subsection{Intrusive Data Structures} A common solution to this problem is the use of \textbf{intrusive data structures}. In this paradigm, the pointers (such as \texttt{next} and \texttt{prev}) are embedded directly within the data object itself, rather than in an external node. This pattern is widely recognized in the C++ ecosystem and serves as the foundation for industry-standard libraries such as \textbf{Boost.Intrusive}\cite{boost_intrusive}. By adopting this approach, the container manipulates pointers that already exist within the object structure. A typical implementation of this pattern using C++ templates is shown in Listing \ref{lst:intrusiveNodesExample}. + +\clearpage + \begin{cppcode}[caption={Intrusive Data Structures Example}, label={lst:intrusiveNodesExample}] template struct ListNode { @@ -3332,6 +3342,8 @@ \subsubsection{Entry Point} The Multiboot2 specification defines the machine state upon entry: \texttt{EAX} contains the magic number \texttt{0x36d76289}, and \texttt{EBX} points to the Multiboot information structure. The entry point establishes a stack and delegates to C++ code: +\clearpage + \begin{nasmcode}[caption={Loader32 Entry Point (entry.nasm)}, label={lst:loader32_entry}] bits 32 ; GRUB leaves us in 32-bit Protected Mode @@ -3404,6 +3416,8 @@ \subsubsection{Memory Management Initialization} The identity mapping ensures that code continues to execute correctly after paging is enabled, and the instruction pointer remains valid because virtual addresses 0--10GB map directly to the same physical addresses. +\clearpage + \subsubsection{Long Mode Enablement} The transition to Long Mode follows the sequence defined in the Intel Software Developer's Manual \cite{Intel-ControlRegisters}, as shown in Listing~\ref{lst:enable_longmode}: @@ -3439,6 +3453,8 @@ \subsubsection{Long Mode Enablement} ret \end{nasmcode} +\clearpage + \subsubsection{Loading Loader64 as an ELF Module} A critical responsibility of Loader32 is loading the next stage. Loader64 is provided to GRUB as a separate Multiboot2 module. Loader32 locates this module, parses it as an ELF64 executable, allocates memory for its segments, and performs dynamic relocation (Listing~\ref{lst:load_loader64}): @@ -3470,6 +3486,8 @@ \subsubsection{Loading Loader64 as an ELF Module} } \end{cppcode} +\clearpage + \subsubsection{64-bit GDT and Mode Transition} Before jumping to 64-bit code, a 64-bit Global Descriptor Table must be loaded. The GDT defines the code and data segments for Long Mode (Listing~\ref{lst:gdt64}): @@ -3499,6 +3517,8 @@ \subsubsection{64-bit GDT and Mode Transition} dq GDT64 \end{nasmcode} +\clearpage + The final transition performs a far jump to flush the CPU pipeline and begin executing 64-bit code (Listing~\ref{lst:transition64}): \begin{nasmcode}[caption={Transition to 64-bit Mode (transition\_64.nasm)}, label={lst:transition64}] @@ -3534,6 +3554,8 @@ \subsection{Loader64: Position-Independent Higher-Half Setup} Loader64 executes in 64-bit Long Mode but faces a unique challenge: it is loaded at an unknown physical address by GRUB, yet must set up virtual memory and load the kernel at a fixed higher-half address. +\clearpage + \subsubsection{Position-Independent Compilation} Loader64 is compiled as a Position-Independent Executable (PIE). The CMake configuration is shown in Listing~\ref{lst:loader64_cmake}: @@ -3574,6 +3596,8 @@ \subsubsection{Position-Independent Compilation} call MainLoader64 \end{nasmcode} +\clearpage + \subsubsection{State Restoration from Loader32} Loader32 passes its PMM and VMM state to Loader64 via a \texttt{TransitionData} structure. Loader64 deserializes this state to continue memory management, as shown in Listing~\ref{lst:transition_data}: @@ -3644,6 +3668,8 @@ \subsubsection{Direct Memory Mapping} This mapping allows the kernel to convert any physical address \texttt{P} to the virtual address \texttt{0xFFFF800000000000 + P}. +\clearpage + \subsubsection{Kernel Arguments Preparation} Before transferring control to the kernel, Loader64 collects essential system information into a \texttt{KernelArguments} structure (Listing~\ref{lst:kernel_args}): @@ -3688,9 +3714,10 @@ \subsubsection{Kernel Arguments Preparation} u64 multiboot_header_start_addr; u64 multiboot_header_end_addr; }; -} \end{cppcode} +\clearpage + The final transition to the kernel is a simple jump (Listing~\ref{lst:enter_kernel}): \begin{nasmcode}[caption={Kernel Entry (transition.nasm)}, label={lst:enter_kernel}] @@ -3743,6 +3770,8 @@ \subsection{Memory Layout} \label{fig:memory_layout} \end{figure} +\clearpage + \subsection{Architecture Initialization} After the kernel receives control, the first architecture-specific function executed is \texttt{arch::ArchInit}. This function enables additional CPU features that were not safe to enable during the bootloader phase, as shown in Listing~\ref{lst:arch_init}: @@ -3897,6 +3926,8 @@ \subsubsection{Mechanism} The allocation algorithm implemented in \texttt{BitmapPmm::Alloc} is a primitive linear search. To satisfy a request for $N$ pages, it scans the bitmap from the last known allocation index until it finds a run of $N$ contiguous zero bits. +\clearpage + \begin{cppcode}[caption={Bootstrap Linear Allocation Logic}, label={lst:bitmap_alloc}] expected BitmapPmm::FindContiguousBlock( size_t start_pfn, size_t end_pfn, u64 num_pages) @@ -4077,6 +4108,8 @@ \subsubsection{Aligned Allocation} To support this, AlkOS implements \texttt{KMallocAligned}. Since the underlying allocators cannot arbitrarily shift the start address of a block, this is achieved through over-allocation and pointer adjustment. +\clearpage + \begin{cppcode}[caption={Aligned Allocation Strategy}, label={lst:aligned_alloc}] struct AllocationHeader { VPtr original_ptr; @@ -4142,6 +4175,8 @@ \subsubsection{The Direct Physical Map (HHDM)} This allows the kernel to access any physical object instantly, without modifying page tables. This logic is encapsulated in the helper templates found in \texttt{mem/types.hpp}: +\clearpage + \begin{cppcode}[caption={Physical to Virtual Address Translation helpers}, label={lst:phys_to_virt}] template FORCE_INLINE_F VPtr PhysToVirt(PPtr physAddr) @@ -4270,6 +4305,8 @@ \subsubsection{Allocation Strategy} The VMM delegates this geometric search to the \texttt{AddressSpace::FindGap} method. This method iterates through the sorted list of existing VMAs. For every pair of adjacent VMAs, it calculates the free space between the end of the first and the start of the second. If the gap is sufficient, it returns the start address aligned to the page size. +\clearpage + \begin{cppcode}[caption={Gap Search Logic}, label={lst:find_gap}] expected AS::FindGap(size_t size, VPtr range_start, VPtr range_end) { @@ -4417,6 +4454,8 @@ \subsubsection{Interrupt Service Routines} A significant challenge with standard compiler-generated functions is that they may modify the stack frame (prologue/epilogue) in ways that interfere with the hardware-defined layout. To maintain full control and prevent stack corruption, we implement assembly wrappers. These wrappers perform the necessary architecture-specific actions before invoking the architecture-agnostic interrupt handling code defined in C++. +\clearpage + \begin{nasmcode}[caption={Assembly ISR wrapper}, label={lst:isr_asm}] %macro context_switch_if_needed 0 cmp rax, 0 @@ -4524,6 +4563,8 @@ \subsection{Context Switch} We assume that all threads begin execution in kernel space before eventually jumping to user space. Since all interrupt handling occurs within the kernel, the interrupt frame always resides on the kernel stack. Therefore, we can initialize the kernel stack of the new thread with a fabricated frame before the context switch. The initialization procedure is shown in Listing~\ref{lst:threadStack}: +\clearpage + \begin{cppcode}[caption={Thread stack initialization}, label={lst:threadStack}] void InitializeThreadStack(void **stack, const Sched::Task &task) { @@ -4563,6 +4604,8 @@ \subsection{Jumping to User Space} Transitioning to user space is performed similarly to a thread context switch. We construct an artificial interrupt frame, but in this case, the code segment and stack segment within the frame are set to the user space selectors, and the stack pointer is set to the user space stack. The C++ implementation is shown in Listing~\ref{lst:userSpaceJump}, and the corresponding NASM code is shown in Listing~\ref{lst:userSpaceJumpNasm}: +\clearpage + \begin{cppcode}[caption={User Space Jump C++ code}, label={lst:userSpaceJump}] extern "C" void cdecl_JumpToUserSpaceEntry(void *addr, IsrStackFrame *frame) { @@ -4588,6 +4631,8 @@ \subsection{Jumping to User Space} } \end{cppcode} +\clearpage + \begin{nasmcode}[caption={User Space Jump NASM code}, label={lst:userSpaceJumpNasm}] ; c_decl ; void JumpToUserSpace(void (*func)(), void* arg) @@ -4671,6 +4716,8 @@ \subsubsection{TSC} \subsubsection{HPET} The High Precision Event Timer (HPET) also offers high precision and is generally more stable than the TSC on older hardware. However, it is an external device mapped via Memory-Mapped I/O (MMIO). As a result, accessing the HPET is significantly slower than reading the TSC, potentially taking up to 1000 cycles per read. Due to this performance overhead, the HPET is designated as the secondary choice. It is primarily utilized for calibrating other clocks rather than for frequent timekeeping operations. +\clearpage + \subsection{Event Clocks} Event Clock Drivers are defined by the structure shown in Listing~\ref{lst:eventClockDriver}: @@ -4734,7 +4781,7 @@ \chapter{High-Level Subsystems} \section{File System} \label{section:fs} -The file system is a fundamental part of any operating system, providing mechanisms to store, access, and manage data on storage devices. We have designed a unified abstraction layer known as the Virtual File System (VFS). The VFS abstracts the underlying implementation details of specific file systems, providing the kernel and user space with an API for operations such as reading, writing, creating, moving, and deleting files and directories. +The file system is a fundamental part of any operating sContextSwitchystem, providing mechanisms to store, access, and manage data on storage devices. We have designed a unified abstraction layer known as the Virtual File System (VFS). The VFS abstracts the underlying implementation details of specific file systems, providing the kernel and user space with an API for operations such as reading, writing, creating, moving, and deleting files and directories. \subsection{VFS} @@ -4827,6 +4874,8 @@ \subsubsection{VFS Module} } \end{cppcode} +\clearpage + \subsubsection{File System Interface} The \texttt{vfs::Filesystem} struct acts as a uniform interface that all concrete file system drivers must expose (Listing~\ref{lst:vfsFilesystem}). It contains function pointers for various file and directory manipulations, alongside metadata about the file system. Each function pointer accepts a \texttt{void* ctx} argument, allowing the generic VFS layer to pass the concrete driver instance. @@ -4905,6 +4954,8 @@ \subsubsection{File System Driver} Each \texttt{Fat} derivative implements its specific logic (e.g., Getting FAT entries differs between FAT12 and FAT32/16, as shown in Listing~\ref{lst:fat12GetFatEntry}) and provides a static \texttt{IsValid(IO \&io)} method to probe whether a given I/O device contains a valid instance of that file system. +\clearpage + \begin{cppcode}[caption={Fat12 GetFATEntry Implementation}, label={lst:fat12GetFatEntry}] NODISCARD FORCE_INLINE_F ClusterNumT GetFATEntry_(ClusterNumT cluster) const { @@ -4946,6 +4997,8 @@ \subsubsection{VFS Interfaces and Concepts} The VFS I/O interface abstracts low-level data access. The \texttt{VFSIO} concept (Listing \ref{lst:vfsioConcept}) establishes a contract for block-based input and output. By adhering to this concept, file system drivers remain decoupled from the underlying hardware, enabling seamless interaction with diverse storage backends --- such as RAM disks, physical partitions, or network storage --- without requiring implementation changes. +\clearpage + \begin{cppcode}[caption={VFSIO Concept Definition}, label={lst:vfsioConcept}] template concept VFSIO = @@ -5021,6 +5074,9 @@ \subsubsection{OpenFileTable (Global)} The \texttt{FileHandle} utilizes a \texttt{NonOwningTaggedPtr} to hold either a pointer to a \texttt{File} object or a \texttt{Pipe} instance, enabling the \texttt{OpenFileEntry} to manage both cases seamlessly. +\clearpage + + \subsubsection{FileTable (Global)} The \texttt{FileTable} manages unique \texttt{File} objects system-wide (Listing~\ref{lst:fileTableGetOrCreate}). Each \texttt{File} object acts as the in-memory representation of a physical file on the file system, storing static attributes such as the path and file size. This ensures that a file on the VFS has a single representation in memory, regardless of how many times it has been opened. @@ -5052,6 +5108,8 @@ \subsubsection{FileTable (Global)} } \end{cppcode} +\clearpage + \subsubsection{FdManager} The \texttt{FdManager} integrates these three tables and exposes file descriptor operations (e.g., \texttt{Open}, \texttt{Close}, \texttt{Read}). When a process requests to open a file, the \texttt{FdManager} coordinates the interaction: it validates existence via the VFS module, retrieves the \texttt{File} object, creates a new entry in the \texttt{OpenFileTable}, and finally assigns a file descriptor in the process's \texttt{FdTable} (Listing~\ref{lst:fdManagerOpen}). @@ -5096,6 +5154,8 @@ \subsection{Process Structure} The process structure is defined in Listing~\ref{lst:processStruct}: +\clearpage + \begin{cppcode}[caption={Process Structure}, label={lst:processStruct}] struct PACK Pid { u16 id; @@ -5182,9 +5242,7 @@ \subsubsection{Threads} \subsubsection{Live Threads and Threads to Clean} -These counters are used for synchronization with the \textbf{ProcessRipper}. Refer to Kernel Workers \ref{subsubsec:kworkers} for more details. - -\subsubsection{Status} +These counters are ucore systems, as discussed later in this document. The exit status of the process, which is passed to any thread waiting for the process to complete. @@ -5196,6 +5254,8 @@ \subsection{Kernel Address Space} The entire operating system shares a single Kernel Address Space. Every kernel process operates within this space. Additionally, the kernel address space is mapped into every user-space process's address space. This design choice enhances performance and simplifies architecture, as user threads frequently invoke system calls. Switching address spaces on every system call would incur significant performance overhead and complicate the logic required for handling syscalls. +\clearpage + \subsection{Thread Structure} The thread structure is defined as follows: @@ -5337,6 +5397,8 @@ \subsection{Intrusive Data Structures} Since threads frequently migrate between different linked lists and data structures, allocating and freeing list nodes for every operation introduces significant overhead. To address this, the system employs intrusive data structures \ref{subsec:intrusive}. Instead of allocating a node that contains a pointer to the object, the node structure is embedded directly within the object itself. Operations on the lists are performed by simply modifying the fields of the object. +\clearpage + \begin{cppcode}[caption={Intrusive Nodes}, label={lst:intrusiveNodes}] template struct IntrusiveListNode { @@ -5401,6 +5463,8 @@ \subsection{Policies} \noindent These policies establish a hierarchy defined by the enumeration shown in Listing~\ref{lst:policyHierarchy}: +\clearpage + \begin{cppcode}[caption={Scheduling Policy Hierarchy}, label={lst:policyHierarchy}] // PO > P1 > .. > P4 enum SchedulingPolicy { @@ -5425,6 +5489,8 @@ \subsection{Policies} \textbf{Background Tasks} --- Tasks that are not time-critical and should only be executed when the system is otherwise idle. Examples include update checks or non-urgent cleanup operations. +\clearpage + \subsubsection{Policies Abstraction} \label{subsec:policies} @@ -5659,6 +5725,8 @@ \subsubsection{File I/O Verification} This sequence validates path resolution, file descriptor table management in the kernel, and the read/write logic of the file system driver (Listing~\ref{lst:hello_fileio}). +\clearpage + \begin{cppcode}[caption={File I/O Verification Logic}, label={lst:hello_fileio}] // Open an existing file with read/write permissions FILE *fp = fopen("/docs/greet.txt", "r+"); @@ -5701,6 +5769,8 @@ \subsubsection{Video Session Initialization} The application begins by invoking the \texttt{create\_graphic\_session} system call (Listing~\ref{lst:gui_init}). This request instructs the kernel's window manager to allocate a physical memory buffer acting as a backbuffer for the process. The kernel then maps this physical memory into the virtual address space of the process and returns a \texttt{GuiBufferInfo} structure. +\clearpage + \begin{cppcode}[caption={Initializing Graphical Session}, label={lst:gui_init}] // userspace/programs/gui_test/main.cpp @@ -5727,6 +5797,8 @@ \subsubsection{Direct Framebuffer Manipulation} Once the session is established, the application treats the video buffer as a linear array of pixels. It implements a rendering loop that generates a moving pattern by calculating pixel colors based on coordinates and a time-variable offset (Listing~\ref{lst:gui_render}). +\clearpage + \begin{cppcode}[caption={Software Rendering Loop}, label={lst:gui_render}] while (true) { for (size_t y = 0; y < info.height; ++y) {