finalising report
This commit is contained in:
118
notes/report/1.tex
Normal file
118
notes/report/1.tex
Normal file
@@ -0,0 +1,118 @@
|
||||
\section{Boot Process and Processor Setup}
|
||||
|
||||
The first stage of the project established a more complete early boot path for the kernel. Before this work, the kernel entered C code, initialized the VGA terminal, printed \texttt{Hello, World!}, and then remained in an infinite halt loop. The visible output remained the same after this stage, but the processor setup became more explicit and reliable.
|
||||
|
||||
The main change was the addition of a minimal Global Descriptor Table for the i386 kernel. Even though the kernel uses a flat memory model, protected mode still requires valid segment descriptors. The GDT therefore gives the processor valid code and data segment definitions before the kernel continues into higher-level initialization.
|
||||
|
||||
\subsection{Kernel Entry Flow}
|
||||
After the GDT work, the early kernel flow became:
|
||||
\begin{enumerate}
|
||||
\item the bootloader transfers control to the kernel entry point,
|
||||
\item the kernel begins execution in C,
|
||||
\item the GDT is initialized and loaded,
|
||||
\item segment registers are reloaded,
|
||||
\item the VGA terminal is initialized,
|
||||
\item \texttt{Hello, World!} is written to the screen,
|
||||
\item the kernel enters an infinite halt loop.
|
||||
\end{enumerate}
|
||||
|
||||
This keeps processor setup before terminal setup. Later stages build on this ordering by adding interrupts, memory management, timing, and applications after the low-level CPU state has been initialized.
|
||||
|
||||
\subsection{GDT Layout}
|
||||
The implemented GDT contains three descriptors:
|
||||
\begin{itemize}
|
||||
\item a null descriptor,
|
||||
\item a kernel code segment descriptor,
|
||||
\item a kernel data segment descriptor.
|
||||
\end{itemize}
|
||||
|
||||
The code and data segments both use base address \texttt{0x00000000}, a 4 GiB address span, 4 KiB granularity, and ring 0 privilege level. The code segment uses selector \texttt{0x08}, while the data segment uses selector \texttt{0x10}.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void GdtInitialize(void) {
|
||||
gdtDescriptor.size = sizeof(gdtEntries) - 1;
|
||||
gdtDescriptor.offset = (uint32_t)&gdtEntries;
|
||||
|
||||
GdtSetEntry(0, 0, 0, 0, 0);
|
||||
GdtSetEntry(1, 0, 0x000FFFFF, 0x9A, 0xCF);
|
||||
GdtSetEntry(2, 0, 0x000FFFFF, 0x92, 0xCF);
|
||||
|
||||
GdtFlush((uint32_t)&gdtDescriptor);
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Minimal GDT initialization with null, kernel code, and kernel data descriptors.}
|
||||
\end{listing}
|
||||
|
||||
\subsection{Descriptor Construction}
|
||||
The GDT setup is split between C and assembly. The C code builds the descriptor table and prepares the GDT descriptor that contains the table size and address. The helper that creates each descriptor splits the base and limit into the fields expected by the processor.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
static void GdtSetEntry(
|
||||
uint32_t index,
|
||||
uint32_t base,
|
||||
uint32_t limit,
|
||||
uint8_t access,
|
||||
uint8_t granularity
|
||||
) {
|
||||
gdtEntries[index].base_low = (uint16_t)(base & 0xFFFF);
|
||||
gdtEntries[index].base_middle = (uint8_t)((base >> 16) & 0xFF);
|
||||
gdtEntries[index].base_high = (uint8_t)((base >> 24) & 0xFF);
|
||||
|
||||
gdtEntries[index].limit_low = (uint16_t)(limit & 0xFFFF);
|
||||
gdtEntries[index].granularity = (uint8_t)((limit >> 16) & 0x0F);
|
||||
gdtEntries[index].granularity |= (uint8_t)(granularity & 0xF0);
|
||||
gdtEntries[index].access = access;
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Building an x86 GDT descriptor from base, limit, access, and granularity fields.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\subsection{Reloading Segment Registers}
|
||||
Loading the GDT with \texttt{lgdt} is not enough by itself. The CPU caches segment descriptor information in the segment registers, so the kernel must reload those registers after installing the new table. The assembly routine performs the architecture-specific part: it loads the GDT, performs a far jump to reload \texttt{cs}, and then reloads the data segment registers.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{nasm}
|
||||
GdtFlush:
|
||||
mov eax, [esp + 4]
|
||||
lgdt [eax]
|
||||
|
||||
jmp GDT_CODE_SELECTOR:.reload_cs
|
||||
|
||||
.reload_cs:
|
||||
mov ax, GDT_DATA_SELECTOR
|
||||
mov ds, ax
|
||||
mov es, ax
|
||||
mov fs, ax
|
||||
mov gs, ax
|
||||
mov ss, ax
|
||||
ret
|
||||
\end{minted}
|
||||
\caption{Loading the GDT and refreshing the cached code and data segment registers.}
|
||||
\end{listing}
|
||||
|
||||
\section{Build and Verification}
|
||||
|
||||
The build configuration was updated so that both the C GDT implementation and the assembly reload routine are compiled and linked into the kernel binary. New build artifacts were generated, including an updated kernel binary and bootable ISO image.
|
||||
|
||||
\subsection{Static Binary Inspection}
|
||||
The generated kernel binary was inspected to confirm that the expected GDT setup sequence was present. The inspection showed a call to the GDT load routine, a far jump to selector \texttt{0x08}, and data segment reloads using selector \texttt{0x10}.
|
||||
|
||||
\subsection{Runtime Debugging}
|
||||
The GDT setup was also verified in QEMU using \texttt{gdb-multiarch}. Execution was stopped inside the GDT initialization and reload path. The GDT descriptor limit was \texttt{0x17}, which matches three 8-byte entries minus one. After the far jump, \texttt{cs} contained \texttt{0x08}; after the reload instructions, \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss} contained \texttt{0x10}.
|
||||
|
||||
\section{State After Processor Setup}
|
||||
|
||||
At the end of this stage, the kernel:
|
||||
\begin{itemize}
|
||||
\item builds inside the development container,
|
||||
\item includes a minimal i386 GDT implementation,
|
||||
\item loads the GDT during startup,
|
||||
\item reloads the code and data segment registers correctly,
|
||||
\item still initializes the VGA terminal,
|
||||
\item still prints \texttt{Hello, World!} after boot.
|
||||
\end{itemize}
|
||||
|
||||
This stage did not add new user-facing behaviour. Its purpose was to make the early processor state explicit so later kernel subsystems could be added on top of a controlled protected-mode setup.
|
||||
182
notes/report/2.tex
Normal file
182
notes/report/2.tex
Normal file
@@ -0,0 +1,182 @@
|
||||
\newpage
|
||||
|
||||
\section{Interrupt Handling}
|
||||
|
||||
The next stage added interrupt support to the kernel. This required an Interrupt Descriptor Table so the processor has defined entry points for CPU exceptions and hardware interrupts. The implementation follows the same model used by the OSDev Wiki material, the \texttt{os-tutorial} project, and the lecturer-provided assignment files \cite{osdevInterruptsTutorial,fenollosaOsTutorial,assignmentFiles}.
|
||||
|
||||
Interrupt support changed the kernel from a purely linear boot program into a system that can react to events while it is running. CPU exceptions are handled through ISR stubs, hardware interrupts are handled through IRQ stubs, and both paths eventually reach C handlers with a saved register state.
|
||||
|
||||
\subsection{IDT Descriptor Setup}
|
||||
Each IDT entry stores the handler address, the kernel code segment selector, and the descriptor attributes used by the processor. The handler address is split into low and high parts because this is the layout expected by the 32-bit x86 IDT format.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void IdtSetDescriptor(uint8_t vector, uint32_t interrupt, uint8_t flags) {
|
||||
struct IdtEntry* descriptor = &idt[vector];
|
||||
|
||||
descriptor->interrupt_low = (uint32_t)interrupt & 0xFFFF;
|
||||
descriptor->kernel_cs = GDT_CODE_SELECTOR;
|
||||
descriptor->attributes = flags;
|
||||
descriptor->interrupt_high = (uint32_t)interrupt >> 16;
|
||||
descriptor->reserved = 0;
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Constructing an IDT gate for a specific interrupt vector.}
|
||||
\end{listing}
|
||||
|
||||
\subsection{IDT Initialization}
|
||||
During initialization, the kernel fills vectors \texttt{0} through \texttt{31} with CPU exception stubs and vectors \texttt{32} through \texttt{47} with hardware IRQ stubs. This means the implementation provides ISR coverage for all 32 standard CPU exception vectors, which is more complete than only defining the minimum three handlers required for basic testing. The PIC is then remapped before the IDT is loaded with \texttt{lidt} and interrupts are enabled with \texttt{sti}.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void IdtInitialize(void) {
|
||||
idtr.base = (uint32_t)&idt[0];
|
||||
idtr.limit = (uint16_t)sizeof(struct IdtEntry) * IDT_ENTRIES - 1;
|
||||
|
||||
for (uint8_t iNum = 0; iNum < 32; iNum++) {
|
||||
IdtSetDescriptor(iNum, (uint32_t)isr_stub_table[iNum], 0x8E);
|
||||
}
|
||||
|
||||
for (uint8_t iNum = 32; iNum < 48; iNum++) {
|
||||
IdtSetDescriptor(iNum, (uint32_t)irq_stub_table[iNum - 32], 0x8E);
|
||||
}
|
||||
|
||||
PicRemap();
|
||||
__asm__ volatile ("lidt %0" : : "m"(idtr));
|
||||
__asm__ volatile ("sti");
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Initializing the IDT with CPU exception and hardware IRQ entries.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\section{CPU Exceptions}
|
||||
|
||||
Basic Interrupt Service Routines were implemented for CPU exceptions. The low-level stubs save the CPU state and pass a \texttt{Registers} structure into the shared C handler. This lets the kernel inspect which exception occurred and print useful debugging information to the terminal. Instead of each ISR containing its own separate print logic, the individual stubs share the same C handler, which prints the interrupt number and message for the triggered exception.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void IsrHandler(struct Registers* registers) {
|
||||
TerminalWriteString("\n=== INTERRUPT RECEIVED ===\n");
|
||||
|
||||
TerminalWriteString("Interrupt Number: ");
|
||||
TerminalWriteUInt(registers->int_no);
|
||||
TerminalWriteString("\n");
|
||||
|
||||
TerminalWriteString("Message: ");
|
||||
if (registers->int_no < 32) {
|
||||
TerminalWriteString(isrMessages[registers->int_no]);
|
||||
} else {
|
||||
TerminalWriteString("Unknown Interrupt");
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
__asm__ volatile("cli; hlt");
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Shared CPU exception handler using the saved interrupt number.}
|
||||
\end{listing}
|
||||
|
||||
The handler stops the kernel after reporting the exception. This is appropriate for the current stage because exception recovery has not yet been implemented, and halting prevents the kernel from continuing after an undefined or unsafe processor state.
|
||||
|
||||
\section{Hardware Interrupts}
|
||||
|
||||
Hardware interrupt support was added for IRQ0 through IRQ15. These interrupts come from devices through the Programmable Interrupt Controller. The PIC is remapped so that hardware IRQs begin at vector \texttt{32} instead of overlapping with the CPU exception vectors.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void PicRemap(void) {
|
||||
OutPortByte(PIC1_COMMAND, 0x11);
|
||||
OutPortByte(PIC2_COMMAND, 0x11);
|
||||
|
||||
OutPortByte(PIC1_DATA, 0x20);
|
||||
OutPortByte(PIC2_DATA, 0x28);
|
||||
|
||||
OutPortByte(PIC1_DATA, 0x04);
|
||||
OutPortByte(PIC2_DATA, 0x02);
|
||||
|
||||
OutPortByte(PIC1_DATA, 0x01);
|
||||
OutPortByte(PIC2_DATA, 0x01);
|
||||
|
||||
OutPortByte(PIC1_DATA, 0x00);
|
||||
OutPortByte(PIC2_DATA, 0x00);
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Remapping the PIC so IRQs use interrupt vectors \texttt{32} through \texttt{47}.}
|
||||
\end{listing}
|
||||
|
||||
The IRQ handler dispatches to a registered C handler when one exists. After the interrupt has been handled, the kernel sends an end-of-interrupt signal to the PIC. This tells the controller that the current interrupt has been processed and that new interrupts may be delivered.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void IrqHandler(struct Registers* registers) {
|
||||
uint8_t irqNum = (uint8_t)(registers->int_no - 32);
|
||||
|
||||
if (interruptHandlers[registers->int_no] != 0) {
|
||||
interruptHandlers[registers->int_no](registers);
|
||||
} else if (irqNum != 0) {
|
||||
TerminalWriteString("IRQ triggered: ");
|
||||
TerminalWriteUInt(irqNum);
|
||||
TerminalWriteString("\n");
|
||||
}
|
||||
|
||||
PicSendEoi(irqNum);
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Dispatching hardware interrupts and acknowledging them with the PIC.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\section{Keyboard Input}
|
||||
|
||||
Keyboard support was implemented using IRQ1. The keyboard handler reads the scancode from port \texttt{0x60}, stores it in a small buffer, ignores key-release events, and translates key-press scancodes into ASCII through a lookup table.
|
||||
|
||||
The assignment describes the keyboard task as printing translated ASCII characters to the screen. In this project, the keyboard logic was extended slightly beyond a direct logger: the handler stores the latest translated ASCII character, and higher-level terminal or application code consumes it. This allows the same keyboard path to support the application menu and Snake input instead of only echoing characters immediately from the interrupt handler.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void KeyboardHandler(struct Registers* registers) {
|
||||
(void) registers;
|
||||
|
||||
uint8_t scancode = InPortByte(KEYBOARD_DATA_PORT);
|
||||
|
||||
if (index < KEYBOARD_BUFFER_SIZE) {
|
||||
keyboardBuffer[index] = scancode;
|
||||
index++;
|
||||
}
|
||||
|
||||
if (scancode & 0x80) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scancode < 128) {
|
||||
char ascii = scancodeToAscii[scancode];
|
||||
if (ascii != 0) {
|
||||
lastKeyPressed = ascii;
|
||||
}
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Keyboard IRQ handler reading scancodes and storing the latest ASCII key.}
|
||||
\end{listing}
|
||||
|
||||
The most recent translated key can then be consumed by higher-level code through \texttt{GetLastKeyPressed()}. This becomes important in later stages where the application menu and Snake game need keyboard input without directly handling raw scancodes.
|
||||
|
||||
\section{State After Interrupt Work}
|
||||
|
||||
At the end of this stage, the kernel:
|
||||
\begin{itemize}
|
||||
\item initializes and loads an Interrupt Descriptor Table,
|
||||
\item handles CPU exceptions through ISR support,
|
||||
\item supports hardware IRQs from IRQ0 to IRQ15,
|
||||
\item remaps the PIC so hardware interrupts do not overlap with CPU exceptions,
|
||||
\item dispatches hardware interrupts to registered handlers,
|
||||
\item sends end-of-interrupt signals after IRQ handling,
|
||||
\item handles keyboard input through IRQ1,
|
||||
\item reads keyboard scancodes from port \texttt{0x60},
|
||||
\item translates key presses into ASCII characters,
|
||||
\item stores the latest key for later application-level input.
|
||||
\end{itemize}
|
||||
|
||||
This stage moved the kernel from static startup output to event-driven execution. The later PIT, music, menu, and Snake work all depend on this interrupt layer.
|
||||
164
notes/report/3.tex
Normal file
164
notes/report/3.tex
Normal file
@@ -0,0 +1,164 @@
|
||||
\section{Memory Management}
|
||||
|
||||
The next stage added basic memory management to the kernel. This work introduced kernel heap initialization, paging, dynamic allocation, page-aligned allocation, and memory debugging output. Together these changes allow later kernel components to request memory dynamically instead of relying only on static storage.
|
||||
|
||||
\subsection{Kernel Heap Initialization}
|
||||
The heap is initialized from the linker-provided end of the kernel image. The kernel stores this address in \texttt{last\_alloc}, then reserves a normal heap region below the page heap area. A separate page heap is placed below \texttt{0x400000}, with one descriptor byte per page-aligned allocation.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void InitKernelMemory(uint32_t* kernel_end) {
|
||||
uint32_t kernelEndAddr = (uint32_t)kernel_end;
|
||||
|
||||
last_alloc = kernelEndAddr + 0x1000;
|
||||
heap_begin = last_alloc;
|
||||
pheap_end = 0x400000;
|
||||
pheap_begin = pheap_end - (MAX_PAGE_ALIGNED_ALLOCS * 4096);
|
||||
heap_end = pheap_begin;
|
||||
memset((char*)heap_begin, 0, heap_end - heap_begin);
|
||||
pheap_desc = (uint8_t *)malloc(MAX_PAGE_ALIGNED_ALLOCS);
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Kernel heap initialization using the linker-provided kernel end symbol.}
|
||||
\end{listing}
|
||||
|
||||
This creates two allocation areas:
|
||||
\begin{itemize}
|
||||
\item a byte-addressed heap for normal \texttt{malloc()} allocations,
|
||||
\item a page heap for 4 KiB page-aligned allocations through \texttt{pmalloc()}.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Paging}
|
||||
Paging was implemented using a page directory at \texttt{0x400000} and page tables starting at \texttt{0x404000}. The setup identity-maps the first 4 MiB of memory and the 4 MiB region beginning at \texttt{0x400000}. This keeps virtual addresses equal to physical addresses for the early kernel environment while still enabling the processor's paging mechanism.
|
||||
|
||||
The paging setup writes the page directory address to \texttt{cr3} and sets the paging bit in \texttt{cr0}. After that point, memory references are translated through the page tables.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void paging_enable() {
|
||||
asm volatile("mov %%eax, %%cr3": :"a"(page_dir_loc));
|
||||
asm volatile("mov %cr0, %eax");
|
||||
asm volatile("orl $0x80000000, %eax");
|
||||
asm volatile("mov %eax, %cr0");
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Enabling paging by loading \texttt{cr3} and setting the paging bit in \texttt{cr0}.}
|
||||
\end{listing}
|
||||
|
||||
\subsection{Dynamic Allocation}
|
||||
A simple heap allocator was implemented for \texttt{malloc()} and \texttt{free()}. Each allocation is preceded by a small metadata header containing the allocation status and size. New allocations advance \texttt{last\_alloc}, while freed blocks are marked as unused and may be reused by later allocations.
|
||||
|
||||
The allocator scans existing blocks before creating a new one. If it finds an unused block large enough for the request, that block is reactivated and returned. This was verified by freeing one allocation, requesting a smaller block, and observing that the allocator reused the same address.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
if(a->size >= size) {
|
||||
a->status = 1;
|
||||
memset(mem + sizeof(alloc_t), 0, size);
|
||||
memory_used += a->size + sizeof(alloc_t) + 4;
|
||||
return (char*)(mem + sizeof(alloc_t));
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Reusing a freed heap block when it is large enough for a new allocation.}
|
||||
\end{listing}
|
||||
|
||||
\subsection{Memory Debugging}
|
||||
The kernel also gained memory debugging output. \texttt{PrintMemoryLayout()} prints the amount of memory used, the amount free, the heap size, and the start and end addresses of both the normal heap and page heap. \texttt{TerminalWriteHex()} was added so addresses can be printed in hexadecimal instead of being confused with decimal values.
|
||||
\clearpage
|
||||
|
||||
\section{Programmable Interval Timer}
|
||||
|
||||
The Programmable Interval Timer was then added so the kernel can measure time and schedule delays. The PIT is configured on channel 0 at \texttt{1000 Hz}, so one timer tick corresponds approximately to one millisecond.
|
||||
|
||||
\subsection{PIT Initialization}
|
||||
The PIT driver defines the command port, channel 0 port, base frequency, target frequency, divider, and tick conversion in \texttt{pit.h}. During initialization, the kernel registers an IRQ0 handler, programs the PIT command byte, and writes the divisor as low byte followed by high byte.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void PitInitialize(void){
|
||||
uint16_t divisor = DIVIDER;
|
||||
|
||||
RegisterInterruptHandler(IRQ0, PitIrqHandler);
|
||||
|
||||
OutPortByte(PIT_CMD_PORT, 0x36);
|
||||
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)(divisor & 0xFF));
|
||||
OutPortByte(PIT_CHANNEL0_PORT, (uint8_t)((divisor >> 8) & 0xFF));
|
||||
}
|
||||
\end{minted}
|
||||
\caption{PIT channel 0 initialization and IRQ0 handler registration.}
|
||||
\end{listing}
|
||||
|
||||
The IRQ0 handler increments a \texttt{volatile} tick counter. Keeping the counter inside \texttt{pit.c} reduces the chance of accidental writes from unrelated kernel code.
|
||||
|
||||
\subsection{Sleep Functions}
|
||||
Two sleep functions were implemented. \texttt{SleepBusy()} repeatedly checks the tick counter until enough time has passed. This is simple but consumes CPU time for the whole delay.
|
||||
|
||||
\texttt{SleepInterrupt()} uses \texttt{sti; hlt} inside the loop. This enables interrupts and halts the CPU until the next interrupt occurs. The loop then rechecks the current tick and halts again if the requested delay has not elapsed.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void SleepInterrupt(uint32_t ticks_to_wait){
|
||||
uint32_t start_tick = GetCurrentTick();
|
||||
uint32_t end_tick = start_tick + ticks_to_wait;
|
||||
|
||||
while(GetCurrentTick() < end_tick){
|
||||
__asm__ volatile ("sti; hlt");
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Interrupt-based sleeping using the PIT tick counter and \texttt{hlt}.}
|
||||
\end{listing}
|
||||
|
||||
\subsection{Verification}
|
||||
Several integration issues were fixed during PIT work. Incorrect inline assembly syntax was corrected, terminal output functions replaced unsuitable \texttt{printf()} calls, and the missing \texttt{TerminalWriteHex()} declaration was added to \texttt{terminal.h}.
|
||||
|
||||
The build process also showed that rebuilding only \texttt{uiaos-kernel} updates \texttt{kernel.bin}, but does not update the bootable ISO image. Rebuilding \texttt{uiaos-create-image} was necessary before QEMU booted the newest kernel. Runtime testing then showed paging output, heap output, allocation output, and repeated sleep-test messages for both busy-wait and interrupt-based sleeping.
|
||||
|
||||
\section{PC Speaker Music Player}
|
||||
|
||||
Assignment 5 used the PIT and PC speaker to implement a simple music player. The PC speaker is controlled through I/O port \texttt{0x61}, while PIT channel 2 generates the square wave used for audible notes.
|
||||
|
||||
\subsection{Sound Generation}
|
||||
The speaker is enabled by setting bits 0 and 1 on port \texttt{0x61}. To play a note, the kernel calculates a divisor from the PIT base frequency and the requested note frequency, programs PIT channel 2, and then enables the speaker.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
void PlaySound(uint32_t frequency) {
|
||||
if (!frequency) return;
|
||||
|
||||
uint32_t divisor = PIT_BASE_FREQ / frequency;
|
||||
|
||||
OutPortByte(PIT_CMD_PORT, 0xB6);
|
||||
OutPortByte(PIT_CHANNEL2_PORT, divisor & 0xFF);
|
||||
OutPortByte(PIT_CHANNEL2_PORT, (divisor >> 8) & 0xFF);
|
||||
|
||||
EnableSpeaker();
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Programming PIT channel 2 to generate a PC speaker tone.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\subsection{Song Representation and Playback}
|
||||
Songs are represented as arrays of notes. Each note stores a frequency and duration in milliseconds. A rest is represented by frequency \texttt{R}, which is defined as \texttt{0}. Playback therefore consists of selecting a frequency, sleeping for the note duration, and then stopping the speaker.
|
||||
|
||||
The implementation uses \texttt{SleepInterrupt()} for note timing. This replaced busy-wait delays because the interrupt-based delay gave better timing and avoided wasting CPU cycles while each note was playing.
|
||||
|
||||
QEMU's PC speaker support made fast note changes unclear. To make playback more distinguishable, durations were doubled during playback.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
if (currentNote.frequency == R) {
|
||||
StopSound();
|
||||
SleepInterrupt(currentNote.duration * 2);
|
||||
} else {
|
||||
PlaySound(currentNote.frequency);
|
||||
SleepInterrupt(currentNote.duration * 2);
|
||||
StopSound();
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Song playback using rests, PC speaker tones, and interrupt-based timing.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
|
||||
104
notes/report/4.tex
Normal file
104
notes/report/4.tex
Normal file
@@ -0,0 +1,104 @@
|
||||
\section{Application Framework and Snake}
|
||||
|
||||
The final stage integrated the individual OS components into a simple application framework. Instead of booting into a single test routine, the kernel now presents a terminal menu where the user can choose between the music player and Snake.
|
||||
|
||||
\subsection{Menu-Based Control Flow}
|
||||
The kernel initialization path now sets up the terminal, GDT, IDT, PIT, keyboard interrupt handler, kernel memory, and paging. After that initialization, the kernel repeatedly clears the terminal, asks for an application number, and dispatches to either the music player or Snake.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
while (1) {
|
||||
TerminalClear();
|
||||
TerminalWriteString("Enter application number (0 for music, 1 for snake): ");
|
||||
char input = TerminalGetChar();
|
||||
|
||||
switch (input) {
|
||||
case '0':
|
||||
PlayMusic();
|
||||
break;
|
||||
case '1':
|
||||
PlayGame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Application menu dispatch in the kernel main loop.}
|
||||
\end{listing}
|
||||
|
||||
This demonstrates that the terminal, keyboard, interrupt, timer, memory, and application code can cooperate through a single kernel control flow.
|
||||
\clearpage
|
||||
|
||||
\subsection{Snake Game State}
|
||||
Snake uses a dynamically allocated game state. The game state contains the board, snake, food, score, and pseudo-random state. Creating and destroying the game therefore exercises the kernel's \texttt{malloc()} and \texttt{free()} implementation in a more realistic setting than isolated allocation tests.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
struct GameState* CreateGame(void) {
|
||||
struct GameState* game = (struct GameState*)malloc(sizeof(struct GameState));
|
||||
if (!game) return 0;
|
||||
|
||||
game->snake = (struct Snake*)malloc(sizeof(struct Snake));
|
||||
if (!game->snake) {
|
||||
free(game);
|
||||
return 0;
|
||||
}
|
||||
|
||||
game->food = (struct Food*)malloc(sizeof(struct Food));
|
||||
if (!game->food) {
|
||||
free(game->snake);
|
||||
free(game);
|
||||
return 0;
|
||||
}
|
||||
|
||||
game->score = 0;
|
||||
game->rngState = GetCurrentTick();
|
||||
return game;
|
||||
}
|
||||
\end{minted}
|
||||
\caption{Snake creates its game objects using the kernel heap allocator.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\subsection{Input, Timing, and Feedback}
|
||||
The Snake loop reads the last key pressed by the keyboard handler, updates the snake direction, moves the snake, checks collisions, redraws the board, and then sleeps using the PIT. The game uses \texttt{GAME\_SPEED\_MS} to control pacing.
|
||||
|
||||
Sound is also integrated into the game. Eating food, dying, and winning each trigger short PC speaker effects. This connects the application layer back to the PIT and PC speaker support implemented earlier.
|
||||
|
||||
\begin{listing}[H]
|
||||
\begin{minted}{c}
|
||||
input = GetLastKeyPressed();
|
||||
HandleInput(game, input);
|
||||
tail = MoveSnake(game->snake);
|
||||
|
||||
collisionType = CheckCollision(game->snake, game->food);
|
||||
if (collisionType == FOOD) {
|
||||
game->score++;
|
||||
PlayFoodSound();
|
||||
AddSegment(game->snake, tail.x, tail.y);
|
||||
SpawnFood(game);
|
||||
}
|
||||
|
||||
DrawBoard(game);
|
||||
SleepInterrupt(GAME_SPEED_MS);
|
||||
\end{minted}
|
||||
\caption{Main Snake loop combining keyboard input, game state, sound, drawing, and PIT timing.}
|
||||
\end{listing}
|
||||
\clearpage
|
||||
|
||||
\section{Final State of the Project}
|
||||
|
||||
At the end of the documented work, the kernel contains:
|
||||
\begin{itemize}
|
||||
\item early GDT setup and segment register reloading,
|
||||
\item IDT, ISR, IRQ, PIC, and keyboard interrupt support,
|
||||
\item kernel heap initialization and simple dynamic allocation,
|
||||
\item identity-mapped paging for the early kernel address space,
|
||||
\item PIT-based timer ticks and sleep functions,
|
||||
\item PC speaker sound generation through PIT channel 2,
|
||||
\item a menu-driven application flow,
|
||||
\item a music player application,
|
||||
\item an interactive Snake game using memory allocation, keyboard input, timing, terminal drawing, and sound feedback.
|
||||
\end{itemize}
|
||||
|
||||
The project therefore moved from a minimal booting kernel that printed static text into a small interactive operating-system environment with hardware interrupts, timing, memory management, terminal input, sound, and application-level control flow.
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
\section{Boot Process}
|
||||
|
||||
\subsection{Bootloader Handoff}
|
||||
The kernel starts execution when control is transferred from the bootloader to the kernel entry point. In this stage, the main change was to insert Global Descriptor Table setup into the early boot path without changing the visible kernel behaviour.
|
||||
|
||||
\subsection{Early Kernel Control Flow}
|
||||
Before this change, the kernel entered C code, initialized the VGA terminal, printed \texttt{Hello, World!}, and then remained in an infinite halt loop.
|
||||
|
||||
After the GDT work, the flow became:
|
||||
\begin{enumerate}
|
||||
\item the bootloader hands control to the kernel,
|
||||
\item the kernel begins execution in C,
|
||||
\item the GDT is initialized and loaded,
|
||||
\item segment registers are reloaded,
|
||||
\item the VGA terminal is initialized,
|
||||
\item \texttt{Hello, World!} is written to the screen,
|
||||
\item the kernel enters an infinite halt loop.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Processor Setup}
|
||||
|
||||
\subsection{Why a GDT Is Needed}
|
||||
The first summary note introduces a minimal Global Descriptor Table for the i386 version of the kernel. Even with a flat memory model, the processor still requires valid segment descriptors in protected mode.
|
||||
|
||||
\subsection{Minimal GDT Structure}
|
||||
The implemented GDT contains three entries:
|
||||
\begin{itemize}
|
||||
\item a null descriptor,
|
||||
\item a kernel code segment descriptor,
|
||||
\item a kernel data segment descriptor.
|
||||
\end{itemize}
|
||||
|
||||
The code and data segments were configured using:
|
||||
\begin{itemize}
|
||||
\item base address \texttt{0x00000000},
|
||||
\item effective 4 GiB address space,
|
||||
\item 4 KiB granularity,
|
||||
\item ring 0 privilege level.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Descriptor Construction}
|
||||
The GDT logic constructs the segment descriptors in memory and prepares a GDT descriptor structure for the processor. The implementation is split into:
|
||||
\begin{itemize}
|
||||
\item building the table contents in C,
|
||||
\item performing the architecture-specific load and reload steps in assembly.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Reloading Segment Registers}
|
||||
An important detail is that \texttt{lgdt} alone is not sufficient. Loading a new GDT does not automatically refresh the segment registers already cached by the CPU.
|
||||
|
||||
For this reason, the implementation includes an assembly routine that:
|
||||
\begin{itemize}
|
||||
\item loads the new GDT using \texttt{lgdt},
|
||||
\item performs a far jump to reload \texttt{cs},
|
||||
\item reloads \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss}.
|
||||
\end{itemize}
|
||||
|
||||
\section{Kernel Initialization}
|
||||
|
||||
\subsection{Integration into Startup}
|
||||
The kernel startup code was changed so that GDT initialization happens before terminal setup. This places processor setup before higher-level initialization.
|
||||
|
||||
\subsection{Preserved Behaviour}
|
||||
After the GDT is loaded and the segment registers are reloaded, the kernel still initializes the VGA terminal and prints \texttt{Hello, World!} to the text buffer.
|
||||
|
||||
\section{Build and Development Environment}
|
||||
|
||||
\subsection{Development Container}
|
||||
The work was carried out inside the project's development container, which already includes the cross-compilation and debugging tools required for freestanding kernel development.
|
||||
|
||||
\subsection{Build System Changes}
|
||||
The build configuration was updated so that the new GDT-related C source file and the corresponding assembly reload routine were both compiled and linked into the kernel binary.
|
||||
|
||||
As a result, new build artifacts were generated, including an updated kernel binary and a bootable ISO image.
|
||||
|
||||
\section{Documentation}
|
||||
|
||||
\subsection{Documenting the Boot Path}
|
||||
The project documentation was updated to explain:
|
||||
\begin{itemize}
|
||||
\item the general kernel boot flow,
|
||||
\item where the GDT setup takes place,
|
||||
\item why \texttt{lgdt} is not enough by itself,
|
||||
\item why a far jump and explicit segment-register reload are required.
|
||||
\end{itemize}
|
||||
|
||||
\section{Verification}
|
||||
|
||||
\subsection{Static Binary Inspection}
|
||||
The first verification step was static inspection of the generated kernel binary to confirm that the expected GDT-related symbols and machine code sequences were present.
|
||||
|
||||
The inspection showed that the binary contained:
|
||||
\begin{itemize}
|
||||
\item a call to the GDT load routine,
|
||||
\item a far jump to selector \texttt{0x08},
|
||||
\item data segment reloads using selector \texttt{0x10}.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Runtime Debugging in QEMU and GDB}
|
||||
The second verification step was runtime debugging using QEMU together with \texttt{gdb-multiarch}.
|
||||
|
||||
During debugging, the following observations were made:
|
||||
\begin{itemize}
|
||||
\item the GDT descriptor limit was \texttt{0x17},
|
||||
\item the code segment entry contained the expected values,
|
||||
\item after the far jump, \texttt{cs} changed to \texttt{0x08},
|
||||
\item after stepping through the reload instructions, \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss} all became \texttt{0x10}.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{State of the Project After the First Note}
|
||||
At the end of this stage, the kernel:
|
||||
\begin{itemize}
|
||||
\item builds inside the development container,
|
||||
\item includes a minimal i386 GDT implementation,
|
||||
\item loads the GDT during startup,
|
||||
\item reloads the code and data segment registers correctly,
|
||||
\item still initializes the VGA terminal,
|
||||
\item still prints \texttt{Hello, World!} after boot.
|
||||
\end{itemize}
|
||||
19
notes/report/bib.tex
Normal file
19
notes/report/bib.tex
Normal file
@@ -0,0 +1,19 @@
|
||||
@misc{osdevInterruptsTutorial,
|
||||
author = {{OSDev Wiki}},
|
||||
title = {{Interrupts Tutorial}},
|
||||
howpublished = {\url{https://wiki.osdev.org/Interrupts_Tutorial}},
|
||||
note = {Accessed 2026-04-16}
|
||||
}
|
||||
|
||||
@misc{fenollosaOsTutorial,
|
||||
author = {Carlos Fenollosa},
|
||||
title = {{os-tutorial}},
|
||||
howpublished = {\url{https://github.com/cfenollosa/os-tutorial/tree/master}},
|
||||
note = {Accessed 2026-04-16}
|
||||
}
|
||||
|
||||
@misc{assignmentFiles,
|
||||
author = {{Course lecturer}},
|
||||
title = {{assignment\_files.zip}},
|
||||
note = {Source code and assignment material provided by the lecturer}
|
||||
}
|
||||
101
notes/report/boot_topics.tex
Normal file
101
notes/report/boot_topics.tex
Normal file
@@ -0,0 +1,101 @@
|
||||
\section{Boot Process Background}
|
||||
|
||||
This section gives background context for the boot process before the kernel-specific implementation described in the rest of the report. It covers the firmware stages, bootloader responsibilities, memory layout, and the effect of virtualization on booting.
|
||||
|
||||
\subsection{Power-On Self-Test}
|
||||
When a computer is powered on or reset, the processor begins execution from a firmware-defined reset vector. At this point, the operating system is not running yet. The first responsibility belongs to the system firmware, either BIOS on older x86 systems or UEFI on newer systems.
|
||||
|
||||
One of the earliest firmware tasks is the Power-On Self-Test, usually shortened to POST. POST checks that the basic hardware needed to continue booting is present and responding. This includes components such as:
|
||||
\begin{itemize}
|
||||
\item the CPU,
|
||||
\item system memory,
|
||||
\item firmware storage,
|
||||
\item video output,
|
||||
\item keyboard or input controllers,
|
||||
\item storage controllers and bootable devices.
|
||||
\end{itemize}
|
||||
|
||||
POST is important because later boot stages assume that the basic machine state is usable. If a required device fails or memory cannot be initialized, the firmware may stop the boot process and report the failure through screen output, status LEDs, or beep codes.
|
||||
|
||||
The interaction between POST and hardware is therefore direct and low-level. Firmware initializes chipset state, configures memory controllers, discovers attached devices, and prepares enough of the platform that a bootloader can be found and executed. This is different from normal operating-system hardware management, because POST happens before the OS has loaded drivers or enabled its own abstractions.
|
||||
|
||||
\subsection{Boot Sequence After POST}
|
||||
After a successful POST, the firmware chooses a boot device according to its configured boot order. On BIOS systems, this usually means reading the first sector of a bootable disk into memory and jumping to it. On UEFI systems, the firmware instead loads an EFI application from a filesystem on the EFI System Partition.
|
||||
|
||||
The high-level sequence after POST is:
|
||||
\begin{enumerate}
|
||||
\item firmware completes hardware initialization,
|
||||
\item firmware selects a boot device,
|
||||
\item the first bootloader stage or EFI boot application is loaded,
|
||||
\item the bootloader prepares the environment expected by the kernel,
|
||||
\item the kernel image is loaded into memory,
|
||||
\item control is transferred to the kernel entry point.
|
||||
\end{enumerate}
|
||||
|
||||
In a BIOS boot flow, the first loaded code is small because the initial boot sector is limited to 512 bytes. This usually forces the bootloader to use multiple stages. The first stage is responsible for loading a larger second stage, and the second stage can then parse filesystems, load the kernel, and prepare boot information.
|
||||
|
||||
In a UEFI boot flow, the firmware provides more services before the operating system starts. A UEFI bootloader can use firmware-provided filesystem and device services, and it is loaded as a normal executable rather than as raw code from a fixed disk sector. This makes UEFI bootloaders more flexible, but the kernel must still eventually take control and stop depending on firmware runtime assumptions.
|
||||
|
||||
\subsection{Bootloaders}
|
||||
A bootloader is the bridge between firmware and the operating-system kernel. Its purpose is not only to start the kernel, but also to place the machine into a state the kernel understands.
|
||||
|
||||
Common bootloader responsibilities include:
|
||||
\begin{itemize}
|
||||
\item locating and loading the kernel image,
|
||||
\item loading additional modules or initrd files,
|
||||
\item obtaining a memory map from the firmware,
|
||||
\item selecting or setting a graphics mode,
|
||||
\item preparing boot information structures,
|
||||
\item switching CPU mode when required,
|
||||
\item transferring control to the kernel entry point.
|
||||
\end{itemize}
|
||||
|
||||
Different bootloaders provide different levels of support. GRUB is a general-purpose bootloader with filesystem support, configuration files, multiboot support, and broad hardware compatibility. Limine is a modern boot protocol and bootloader often used in hobby OS development because it provides a clear boot protocol and supports both BIOS and UEFI systems. A manually implemented bootloader gives full control over the boot path, but requires more work and exposes the project to more early-stage hardware and filesystem details.
|
||||
|
||||
The choice of bootloader depends on the goals of the operating-system project. For a kernel-focused project, using an existing bootloader is usually the most practical choice because it avoids spending most of the work on disk loading, filesystem parsing, firmware differences, and CPU mode transitions. For a project specifically about bootstrapping, implementing a bootloader manually can be useful because it exposes the details normally hidden by existing tools.
|
||||
|
||||
Manual bootloader implementation is challenging because the environment is very limited. In a BIOS first-stage bootloader, code size is constrained, there is no standard library, only a small amount of state is initialized, and disk access must use firmware interrupts or direct hardware access. The bootloader must also be careful about where it places itself, the kernel, stacks, tables, and temporary buffers in memory.
|
||||
|
||||
\subsection{Memory Layout in the Boot Process}
|
||||
In a traditional i386 BIOS boot process, early memory layout is constrained by legacy conventions. The first MiB of memory contains several important regions, including the interrupt vector table, BIOS data area, conventional memory, video memory, and firmware regions.
|
||||
|
||||
The bootloader and kernel must avoid overwriting memory that is already used by firmware, hardware mappings, or the bootloader itself. A common simple kernel load address is \texttt{0x10000}. This address is above the lowest BIOS data structures and gives the kernel more room than the original boot sector area, while still being within conventional memory that is easy to access in early boot stages.
|
||||
|
||||
Starting near \texttt{0x10000} has practical implications:
|
||||
\begin{itemize}
|
||||
\item the bootloader needs to copy or load the kernel to a known address,
|
||||
\item the kernel linker script must match the address where the kernel expects to run,
|
||||
\item early stacks and temporary buffers must be placed so they do not overlap the kernel,
|
||||
\item later memory management must identify which regions are already occupied.
|
||||
\end{itemize}
|
||||
|
||||
Once the kernel has control, it can replace this simple early memory model with its own memory management. In this project, that later stage is represented by kernel heap initialization, page-aligned allocation, and paging setup.
|
||||
|
||||
\subsection{Boot Process in Modern Operating Systems}
|
||||
Modern operating systems use more complex boot processes than small teaching kernels, but the same basic idea remains: firmware initializes the platform, a bootloader or boot manager loads the kernel, and the kernel takes control of the machine.
|
||||
|
||||
Linux systems commonly use firmware to start a bootloader such as GRUB or systemd-boot. The bootloader loads the kernel and an initramfs, passes a command line and boot information, and then transfers control to the Linux kernel. Windows systems use Windows Boot Manager, which loads the Windows OS loader before the kernel and core system components are started. macOS uses Apple's boot chain, which is tightly integrated with Apple hardware, APFS, Secure Boot policies, and system volume verification.
|
||||
|
||||
Modern boot processes have changed because hardware and security requirements have changed. UEFI replaced many BIOS conventions, disks moved from MBR partitioning toward GPT, and Secure Boot introduced signature verification into the boot chain. Storage devices are also more complex than older BIOS-era disks, and modern systems often need early support for NVMe, encryption, graphics initialization, and platform security features.
|
||||
|
||||
The result is that modern booting is both more capable and more controlled. Firmware and bootloaders provide richer services, but the operating system must also participate in a stricter trust chain and handle more platform variation.
|
||||
|
||||
\subsection{Virtual Machines and Booting}
|
||||
Booting inside a virtual machine follows the same conceptual stages as booting on physical hardware, but the hardware being initialized is virtual. The guest operating system still sees firmware, CPU state, memory, storage devices, timers, and interrupt controllers. However, those devices are provided or emulated by the hypervisor.
|
||||
|
||||
The main difference is that a virtual machine does not start from real motherboard hardware. Instead, the hypervisor creates a virtual hardware environment and then starts the guest firmware inside it. The guest firmware performs a boot sequence that looks normal from inside the VM, but many device operations are handled by the hypervisor on the host.
|
||||
|
||||
The hypervisor has several roles in this process:
|
||||
\begin{itemize}
|
||||
\item allocating guest memory,
|
||||
\item exposing virtual CPUs,
|
||||
\item providing virtual disks and network devices,
|
||||
\item emulating or virtualizing interrupt controllers and timers,
|
||||
\item presenting BIOS or UEFI firmware to the guest,
|
||||
\item handling privileged operations that cannot run directly on the host CPU.
|
||||
\end{itemize}
|
||||
|
||||
Virtualized booting has advantages for operating-system development. It is faster to test than rebooting physical hardware, the virtual machine can be reset easily, debugging is easier with tools such as QEMU and GDB, and hardware behaviour is more reproducible. This is why the kernel in this project was tested through QEMU rather than directly on physical hardware.
|
||||
|
||||
There are also limitations. Emulated hardware may not behave exactly like real hardware, and some devices are simplified compared to physical machines. For example, PC speaker output in QEMU can be limited, which affected the clarity of fast note changes in the music player. Even so, virtualization is highly useful for kernel development because it provides a controlled environment for testing boot, interrupts, memory management, and device interaction.
|
||||
|
||||
14
notes/report/conclusion.tex
Normal file
14
notes/report/conclusion.tex
Normal file
@@ -0,0 +1,14 @@
|
||||
\section{Conclusion}
|
||||
|
||||
The project started with a minimal bootable kernel and developed it into a small interactive operating-system environment. The final kernel includes early processor setup, interrupt handling, memory management, PIT-based timing, PC speaker sound, keyboard input, terminal output, a menu system, a music player, and a Snake game.
|
||||
|
||||
The first major step was to make the processor setup explicit by adding a minimal Global Descriptor Table and reloading the segment registers correctly. This provided a controlled protected-mode foundation for later work. Interrupt handling then extended the kernel from linear startup code into an event-driven system, with CPU exceptions, hardware IRQs, PIC remapping, and keyboard input.
|
||||
|
||||
Memory management and paging added another important layer. The kernel gained heap initialization, dynamic allocation, page-aligned allocation, memory utility functions, and identity-mapped paging. These features made it possible for later code to allocate state dynamically rather than relying only on static data.
|
||||
|
||||
The PIT then provided a timing source through IRQ0. This supported both busy-wait and interrupt-based sleeping, with the interrupt-based version becoming important for later features. The same timing infrastructure was reused by the music player and Snake game. The PC speaker work showed how PIT channel 2 could be used for sound generation, while also showing the practical limits of testing audio through QEMU.
|
||||
|
||||
The final application framework tied the lower-level pieces together. The menu system used terminal output and keyboard input to choose between applications. The music player used PIT timing and PC speaker output. Snake combined memory allocation, keyboard input, PIT-based pacing, terminal drawing, and sound effects. This made the final application stage a practical test of whether the kernel subsystems could work together.
|
||||
|
||||
Overall, the project demonstrates how small operating-system mechanisms build on each other. GDT setup makes the early CPU state reliable, interrupts make asynchronous hardware events usable, memory management allows dynamic state, timing enables delays and pacing, and device output makes applications interactive. The result is not a complete general-purpose operating system, but it is a working kernel environment that shows the relationship between low-level hardware control and higher-level program behaviour.
|
||||
|
||||
11
notes/report/intro.tex
Normal file
11
notes/report/intro.tex
Normal file
@@ -0,0 +1,11 @@
|
||||
\section{Introduction}
|
||||
|
||||
This report documents both the boot-process background behind operating-system startup and the development of a small 32-bit operating-system kernel for the \texttt{OSDev\_18} project. It starts by explaining the firmware and bootloader stages that happen before a kernel runs, then follows the project implementation from a minimal bootable kernel into a more interactive environment with processor setup, interrupts, memory management, timing, sound, keyboard input, and simple applications.
|
||||
|
||||
The background section covers the general boot sequence: Power-On Self-Test, firmware handoff, BIOS and UEFI differences, bootloader responsibilities, early i386 memory layout, modern operating-system boot chains, and the role of virtualization. This gives context for why the project can rely on a bootloader, why the kernel starts from a constrained early machine state, and why QEMU is useful for testing this kind of low-level work.
|
||||
|
||||
The implementation itself is intentionally low-level. Most of the kernel interacts directly with x86 processor structures and hardware interfaces, including the Global Descriptor Table, Interrupt Descriptor Table, Programmable Interrupt Controller, Programmable Interval Timer, VGA text output, keyboard controller, and PC speaker. Because the kernel runs in a freestanding environment, basic facilities that are normally provided by an operating system or standard library also had to be implemented inside the project.
|
||||
|
||||
After the boot background, the report follows the same order as the implementation work. The first implementation part covers the early boot path and processor setup, with particular focus on the Global Descriptor Table and the need to reload segment registers after installing it. The second part describes interrupt handling, including CPU exceptions, hardware IRQs, PIC remapping, and keyboard input. The third part introduces memory management, paging, PIT-based timing, sleep functions, and PC speaker music playback. The final part shows how these individual subsystems were combined into a small menu-driven application environment with a music player and an interactive Snake game.
|
||||
|
||||
The overall goal was not to build a complete general-purpose operating system, but to demonstrate how core operating-system mechanisms fit together. Each stage adds a specific capability, and later stages reuse earlier ones: the PIT depends on interrupt handling, the music player depends on PIT timing and PC speaker output, and Snake depends on memory allocation, keyboard input, terminal drawing, timing, and sound feedback. By the end of the project, the kernel has moved from static startup output to an event-driven system that can run simple interactive applications.
|
||||
41
notes/report/problems.tex
Normal file
41
notes/report/problems.tex
Normal file
@@ -0,0 +1,41 @@
|
||||
\section{Problems and Challenges}
|
||||
|
||||
Several issues appeared during the project because the kernel runs in a freestanding environment where there is little separation between build configuration, CPU state, hardware programming, and application behaviour. Most problems were not isolated to one source file; they came from interactions between the boot image, low-level initialization, interrupts, timing, and terminal output.
|
||||
|
||||
\subsection{Processor State and Verification}
|
||||
The GDT implementation required careful verification because a mistake in this stage can prevent the kernel from continuing at all. One important detail was that loading the GDT with \texttt{lgdt} does not automatically update the cached segment registers. The implementation therefore needed a far jump to reload \texttt{cs}, followed by explicit reloads of \texttt{ds}, \texttt{es}, \texttt{fs}, \texttt{gs}, and \texttt{ss}.
|
||||
|
||||
This was verified both statically and at runtime. Static inspection confirmed that the kernel binary contained the expected GDT load sequence, far jump, and data segment reloads. Runtime debugging in QEMU with \texttt{gdb-multiarch} confirmed that \texttt{cs} became \texttt{0x08} and that the data segment registers became \texttt{0x10}. This was necessary because the code could compile correctly while still failing if the processor did not actually enter the expected segment state.
|
||||
|
||||
\subsection{Build and Boot Image Mismatch}
|
||||
A major integration problem appeared during the PIT work. Rebuilding only the kernel target updated \texttt{kernel.bin}, but did not update the bootable ISO image. As a result, QEMU could still boot an older image even after the source code and kernel binary had changed.
|
||||
|
||||
This made the runtime output misleading. The newer \texttt{kernel.bin} contained PIT and memory-management strings, while the older \texttt{kernel.iso} still showed the earlier \texttt{Hello, World!} output. The issue was resolved by rebuilding the bootable image target so the ISO contained the updated kernel.
|
||||
|
||||
This problem showed that kernel development requires verifying not only the source code, but also the exact artifact being booted. When the build system produces multiple outputs, it is possible to test the wrong one without noticing immediately.
|
||||
|
||||
\subsection{Freestanding C and Header Consistency}
|
||||
Because the kernel does not run with a normal hosted C environment, ordinary library assumptions do not always apply. During the PIT integration, \texttt{printf()} calls had to be removed from test code and replaced with the kernel's terminal output functions. The terminal interface also needed to expose \texttt{TerminalWriteHex()} through the header because the function was implemented in \texttt{terminal.c} but not declared for other modules.
|
||||
|
||||
Inline assembly syntax also caused a build issue. An incorrect form of \texttt{\_\_asm\_\_volatile} had to be corrected to \texttt{\_\_asm\_\_ volatile}. This was a small syntax problem, but in low-level code it blocked the PIT sleep implementation because \texttt{sti; hlt} was part of the interrupt-based delay loop.
|
||||
|
||||
\subsection{Interrupt Dependencies}
|
||||
Several later features depended on interrupt handling being correct. The PIT relies on IRQ0, keyboard input relies on IRQ1, and both the music player and Snake depend on timer and keyboard behaviour. This meant that interrupt setup was not only an isolated assignment task; it became part of the foundation for the rest of the project.
|
||||
|
||||
The PIC also had to be remapped so hardware IRQs did not overlap with CPU exception vectors. Without this separation, hardware events and processor exceptions would be harder to distinguish, and debugging later features would become much less reliable.
|
||||
|
||||
\subsection{Terminal Output Limitations}
|
||||
The terminal output implementation was sufficient for early debugging and application display, but repeated output exposed a limitation. During the PIT sleep test, once the VGA text buffer wrapped back to the top of the screen, new text overwrote old text directly. This made long-running debug output harder to read.
|
||||
|
||||
The later application framework reduced this issue by clearing the terminal before drawing menus and game frames. However, the underlying limitation remains: the terminal would benefit from proper scrolling or a cleaner screen-management model for continuous logs.
|
||||
|
||||
\subsection{PC Speaker and QEMU Limitations}
|
||||
The music player used the PC speaker and PIT channel 2 to generate square wave tones. The implementation worked, but QEMU's PC speaker support limited the clarity of playback. Fast note changes could sound unclear, and songs degraded when played at the original speed.
|
||||
|
||||
The practical adjustment was to double note durations during playback. This slowed down the music and made individual notes easier to distinguish. The limitation was therefore not mainly in the note representation or PIT timing logic, but in the quality and behaviour of the emulated sound output.
|
||||
|
||||
\subsection{Subsystem Integration}
|
||||
The final Snake application exposed the main integration challenge of the project. Snake uses dynamic memory allocation for game state, keyboard interrupts for input, PIT timing for movement speed, terminal output for drawing, and PC speaker output for feedback. A problem in any of these lower-level systems can appear as an application-level bug.
|
||||
|
||||
This made the final stage useful as more than a demonstration. It acted as an integration test for the kernel. The menu and Snake game showed that the separate subsystems could cooperate through a single control flow, but they also made clear that kernel features must be built in a stable order: processor setup first, then interrupts, then memory and timing, then application behaviour.
|
||||
|
||||
4
notes/report/references.tex
Normal file
4
notes/report/references.tex
Normal file
@@ -0,0 +1,4 @@
|
||||
Used for assignments 3, 4, and 5
|
||||
https://wiki.osdev.org/Interrupts_Tutorial
|
||||
https://github.com/cfenollosa/os-tutorial/tree/master
|
||||
source code given by lecturer in `assignment_files.zip`
|
||||
Reference in New Issue
Block a user