Skip to content

angOS- Part One

** Update 11/09 - Someone told me the name might be confused with another project out in the wild so updating to reflect my working title **angOS** (I just mushed angular and OS together - nothing fancy)

My earliest memory of a Windows PC dates back to when I was five. The information age had started to pick up steam and Microsoft was sporting it’s latest operating system, Windows 95 - a powerhouse for business, connectivity and leisure marketed as a must-have tool for the modern family.

My parents who were operating a small business and raising a young family certainly thought so and bought home the Compaq Presario 4160, a technological marvel for its time. This machine not only served me throughout most of my adolescent life, but also sparked a joy for technology that would be lifelong.

compaq_4160

I reference this because even at that young age, wanting to understand the ins and outs of my machine, how all the software operated was something that excited me. That’s why I recently decided to go deeper and attempt to simulate an operating system using a modern web language - these projects are often described as a webOS and are generally used to learn and grow as a developers.

This project has been very rewarding for me, it has taught me a great deal on how larger teams make various decisions. For those who are at an intermediate level and looking to branch from one type of development to another (in my case it was web to Systems/OS) - this is a great way to think outside the box.

At the end of June 2025 I came across whispers that the Windows 11 Start Menu was built with React Native. After further research, it seemed to be some casual “clickbait” that surfaced every few months and nothing of note (the Start Menu itself is built with C++ and XAML with the recommendations feature using React Native). It was a nice little rabbit hole at the time however it did spark a small proof of concept.

The nostalgia of older Microsoft operating systems notably 95, 98 and XP brings me joy, reminiscing about aero icons and MSN Messenger dings after school I set myself a goal of visually recreating a Windows XP desktop environment in React - not the hardest front end styling exercise but one I came to enjoy.

It was around this time that my curiosity and learning topics started to mingle - I had recently started reading Clean Architecture by Robert C. Martin (Uncle Bob), alongside working through some courseware from Princeton around Data Structures and Algorithms. which was taught in Java. Putting the two together I had fully bought into the OO paradigm and was itching to practice my knowledge and theory - as I started crafting my UI design, I asked myself some fundamental questions.

  • “How does Windows get to the front end?”
  • “Is Windows different to other operating systems in how it achieves this?”
  • “How do we manage themes, preferences, start menu applications and other users?”

The more I asked these questions the more I realised this project was going to go beyond the scope of just a front end recreation.

I had made a decision to turn this small project into a “learning companion” giving me a higher level understanding of core components that go into an Operating System, to keep it simple it would be written in a language I am currently familiar and comfortable in (TypeScript). I wanted the project to push me to think outside of the box with my current language whilst introducing me to concepts found in different development practices (Kernel Development, Drivers, Firmware, OS etc. ).

I first needed to set myself some rules - after careful consideration these were the main ones.

  1. Pick a reference architecture.
  2. Choose a Paradigm.
  3. Stick to SOLID principles .
  4. Modularity and expansion prioritised in design and structure.
  5. Constrain realism to limitations of language and tools we are building in.
  6. Be open to changing these rules if it meets the goals of simulating our reference architecture.

These rules resulted in some early decisions. I first elected to align with the Object Oriented Paradigm for this project, this would mean some changes to frameworks and structure (more on that shortly). I also decided to choose the NT Architecture as my reference architecture considering I was already working on a Windows front end.

Some other “standards” I set for myself.

  • kebab-case naming convention with dot separators to identify our files with ease etc: example-file.class.ts
  • Separation of concerns including modularised type.ts and const.ts
  • Test driven development to milestone major functionality implementations with unit testing methodology.

I believe the above to be more foundational, I realised the more important decisions were paradigm and reference architecture which helped shape and grow these standards organically.

In addition I chose some technologies to assist with the developer experience - the main ones being jest, prettier, tailwind, angular, ts-jest and jsdom.

  • jest with js-dom: Is the main testing suite being used as it allows for both functional unit tests and DOM related output validation.
  • prettier: I used to ensure consistent formatting across our files.
  • tailwind: Is a utility class based CSS library allowing for us to have simple inline styling across our html components
  • angular: Is an OO front end framework which we will discuss in more detail in the next section.

I had done a considerable amount of work making a functional front end using vite. I had built a replica of the Windows XP taskbar which visually matched the old Start Menu. I had even built multiple themes being served through a contextProvider, originally the vision was to allow for users to make their own themes to replicate other popular operating systems - the only issue, vite and React (whilst no issue mixing in OO principles after all everything is an object in javascript) are primarily better suited to the functional paradigm.

I had my first decision to make - one that I now believe to be the right one.

I elected to port my project in full to Angular. This was a great experience overall and my approach helped speed things up, if you are ever looking to port a project from one framework to another - my suggestion is to speak in the language of the Framework you are familiar with to an LLM (my current choice is Gemini for “pair programming ” purposes) - I’m not suggesting the responses are gospel however, questions like.

“What is the React useState equivalent in the Angular framework?”

Helped speed up finding the right reference materials for me to learn the basics of the framework, other more nuanced techniques were reserved to reading documentation and self research.

Here I want to show how different the two frameworks are, let’s take the Windows XP Start Menu (see reference picture from my frontend).

webos_startmenu

The original functional component code (vite) looked like this:

StartMenu.tsx

src/applications/system/desktop-environment/components/StartMenu.tsx
import Divider from '../divider/Divider'
import PowerPanel from './PowerPanel'
import StartMenuLeftPanel from './StartMenuLeftPanel'
import StartMenuRightPanel from './StartMenuRightPanel'
import { forwardRef } from 'react'
import { useTheme } from '../../themes/ThemeContext'
interface StartMenuProps {
isOpen: boolean
}
const StartMenu = forwardRef<HTMLDivElement, StartMenuProps>(
({ isOpen }, ref) => {
// 'ref' is the second argument provided by forwardRef
const theme = useTheme()
const startMenuTheme = theme.startmenustyles
const startMenuWrapperClasses = [
startMenuTheme.wrapper.border,
startMenuTheme.wrapper.color,
startMenuTheme.wrapper.display,
]
.filter(Boolean)
.join(' ')
const startMenuHeaderClasses = [
startMenuTheme.startMenuHeader.border,
startMenuTheme.startMenuHeader.color,
startMenuTheme.startMenuHeader.display,
]
.filter(Boolean)
.join(' ')
const innerMenuClasses = [
startMenuTheme.innermenu.border,
startMenuTheme.innermenu.display,
]
.filter(Boolean)
.join(' ')
return (
<div
className={`${
isOpen ? 'absolute' : 'hidden'
} ${startMenuWrapperClasses}`}
ref={ref}
>
       {' '}
<div className={startMenuHeaderClasses}>
         {' '}
<img
src={theme.startmenustyles.profilepicture.iconSrc}
className={
theme.startmenustyles.profilepicture.iconStyle
}
alt={theme.startmenustyles.profilepicture.iconAlt}
/>
          <span
className={theme.startmenustyles.username.text}
>
            Administrator          {' '}
</span>       {' '}
</div>
        <div className={innerMenuClasses}>
          <Divider dividerColor="orange" />          <div
className={theme.startmenustyles.pannelwrapper.display}
>
            <StartMenuLeftPanel />
            <StartMenuRightPanel />         {' '}
</div>
          <PowerPanel />       {' '}
</div>     {' '}
</div>
)
}
)
export default StartMenu

And the ported object oriented component (Angular) looks like this:

StartMenu.ts

src/applications/system/desktop-environment/components/StartMenu.ts
import { Component, inject, computed, Input } from '@angular/core'
import { CommonModule } from '@angular/common'
import { NgOptimizedImage } from '@angular/common'
import { ThemeContextService } from '../../themes/theme-context'
import Divider from '../divider/divider'
import StartMenuLeftPanel from './start-menu-left-panel'
import StartMenuRightPanel from './start-menu-right-panel'
import StartMenuPowerPanel from './power-panel'
import { StyleHelpers } from '../../style-helpers'
@Component({
selector: 'start-menu',
standalone: true,
imports: [
CommonModule,
NgOptimizedImage,
Divider,
StartMenuLeftPanel,
StartMenuRightPanel,
StartMenuPowerPanel,
],
template: `
<div [ngClass]="startMenuWrapperClasses">
<div [ngClass]="startMenuHeaderClasses">
<img
[ngSrc]="profilePicture.iconSrc"
[ngClass]="profilePicture.iconStyle"
[alt]="profilePicture.iconAlt"
/>
<span [ngClass]="startMenuUsernameClasses">
Administrator
</span>
</div>
<div [ngClass]="startMenuInnerMenuClasses">
        <divider color="orange" />
<div [ngClass]="startMenuPanelClasses">
          <start-menu-left-panel />
<start-menu-right-panel />
</div>
        <start-menu-power-panel />
</div>
</div>
`,
})
export default class StartMenu {
@Input() isOpen: boolean = false
private theme = inject(ThemeContextService)
styles = computed(() => this.theme.theme().startmenustyles)
get profilePicture() {
return StyleHelpers.returnIconValues(this.styles().profilepicture)
}
get startMenuPanelClasses() {
return StyleHelpers.joinStyleValues(this.styles().pannelwrapper)
}
get startMenuInnerMenuClasses() {
return StyleHelpers.joinStyleValues(this.styles().innermenu)
}
get startMenuHeaderClasses() {
return StyleHelpers.joinStyleValues(this.styles().startMenuHeader)
}
get startMenuUsernameClasses() {
return StyleHelpers.joinStyleValues(this.styles().username)
}
get startMenuWrapperClasses() {
const classes = [StyleHelpers.joinStyleValues(this.styles().wrapper)]
if (!this.isOpen) {
classes.push('hidden')
} else {
classes.push('absolute')
}
return classes.filter(Boolean).join(' ')
}
}

Two completely different approaches ultimately leading to the same rendered component - I had moved from useContext to inject (and compute); props to @decorators; state to signals and of course was now returning classes instead of functions.

Whilst the conscious choice to port to Angular was made early this wasn’t necessarily due to one framework being better then the other. It aligned more to those opinions and rules I had set - in particular adhering to an OOP approach upfront.

A computer is made up of components, those components put together make it compute. So how do we simulate those components without losing focus on the overall project and its learning outcomes. My first task was to validate a configuration with some simulated core hardware components.

During my initial research I made a decision to forgo simulating a CPU at this stage - I made this decision as I saw the task as one that would consume a significant amount of early stage development that could cause the project to lose focus and fail to meet its initial learning goals.

Should I have included the CPU I would need to consider, threading, concurrency, logic gate functionality, orchestrating kernel executions and much more. My approach was simple, allow for the structure to “plugin” a CPU at a later date and for now continue with the main goals.

Memory from a functionality perspective is fairly simple with the two main functions being read and write. (technically it is storing and releasing but for now let’s relegate to read and write).

Accessing memory has no linearity, allowing for non-sequential reads and writes to ensure fast access. As this is a simulation we were not going to get into the granularity of control logic, address decoders (at least in detail) or multiplexers. The function of our various memory modules was to meet and address a few points.

  • A way to simulate and limit concurrent running processes
  • Abstracted pointers to Hexadecimal values (Uint8Array was used for this).
  • Simple read and write that could eventually be accessed through our win32API (More on this in part two of this article in the near future).

The first technical decision made was to limit the simulated storage parameters to be kilobytes (kb/1024*1024) noting I would scale this up in the front end environment for rendering/visual purposes. The reason was due to the constraints for object creation lengths in JavaScript. Having a Uint8Array with a length that equated to the amount of bytes in a GB would not be acceptable or even required for this project.

As mentioned I made a choice of a Uint8Array to represent our “address decoder” (using the term very loosely) because it supports the working with arbitrary binary data. Should I decide to dip my toes into WASM and decoding binary data then we have a good typed array to support this in the future!

I also decided to not implement GDI level simulation which meant limiting the GPU to memory functionality, this means that both the GPU and RAM classes would be identical for now separated in name.

ram.class.ts

src/simulated-hardware/ram/ram.class.ts
// GPU and RAM have the same functions at this stage.
// technical decision to use kb instead of mb to simulate actual memory reads within javascript
export class RAM {
public totalMemoryKB: number
public memoryContent: Uint8Array
public usedMemoryKB: number = 0
public availableMemoryKB: number = 0
public totalBytes: number
constructor(memoryKB: number) {
this.totalMemoryKB = memoryKB
this.availableMemoryKB = this.totalMemoryKB
this.totalBytes = this.totalMemoryKB * 1024
this.memoryContent = new Uint8Array(this.totalBytes)
}
public allocateMemory(expectedMemoryUsageKB: number): number | null {
if (expectedMemoryUsageKB <= 0) {
console.warn('RAM: Cannot allocate 0 or negative memory')
return null
}
if (expectedMemoryUsageKB >= this.availableMemoryKB) {
console.warn(
`RAM: Not enough RAM Memory available. Requested ${expectedMemoryUsageKB}KB, but only ${this.availableMemoryKB}KB remaining.`
)
return null
} else {
const allocatedAddressBytes = this.usedMemoryKB * 1024
this.availableMemoryKB -= expectedMemoryUsageKB
this.usedMemoryKB += expectedMemoryUsageKB
console.log(
`RAM: Allocated ${expectedMemoryUsageKB}KB of memory. Used: ${this.usedMemoryKB}KB, Remaining: ${this.availableMemoryKB}KB`
)
return allocatedAddressBytes
}
}
public readMemory(addressBytes: number, lengthBytes: number): Uint8Array {
const totalBytes = this.totalMemoryKB * 1024
if (addressBytes < 0 || addressBytes + lengthBytes > totalBytes) {
console.error(
`RAM: Memory read out of bounds. Address: ${addressBytes}, Length: ${lengthBytes} bytes. Total Size: ${totalBytes} bytes.`
)
return new Uint8Array(0)
}
return this.memoryContent.subarray(
addressBytes,
addressBytes + lengthBytes
)
}
public writeMemory(addressBytes: number, data: Uint8Array): void {
const totalBytes = this.totalMemoryKB * 1024
if (addressBytes < 0 || addressBytes + data.length > totalBytes) {
console.error(
`RAM: Memory read out of bounds. Address: ${addressBytes}, Data Length: ${data.length} bytes. Total Size: ${totalBytes} bytes.`
)
return
}
this.memoryContent.set(data, addressBytes)
}
public getAvailableMemory(): number {
return this.availableMemoryKB
}
public getUsedMemory(): number {
return this.usedMemoryKB
}
}

Whilst the storage device would also take the same functions a small addition was added to the constructor to ensure I could adapt the class to have different type properties. I did this by using a StorageType export. I was also initially considering how to check “port compatibility” and this structure would help me address that across components.

storage/types.ts

src/simulatedHardware/storage/types.ts
export const StorageTypes = {
DISC_DVD: 'DISC_DVD',
IDE_HDD_ONE: 'IDE_HDD_ONE',
IDE_HDD_TWO: 'IDE_HDD_TWO',
IDE_HDD_THREE: 'IDE_HDD_THREE',
USB_FLASHDRIVE: 'USB_FLASHDRIVE',
}
export type StorageType = (typeof StorageTypes)[keyof typeof StorageTypes]

storage-device.class.ts

src/simulated-hardware/storage/storage-device.class.ts
constructor(storageType: StorageType, memoryKB: number) {
if (memoryKB <= 0) {
console.error('STORAGE: Cannot create a storage device with no memory.');
this.availableMemoryKB = null;
this.totalMemoryKB = 0;
this.totalBytes = 0;
this.memoryContent = new Uint8Array(0);
this.hardwareProfile = storageType;
return;
}
this.totalMemoryKB = memoryKB;
this.totalBytes = this.totalMemoryKB * 1024;
this.availableMemoryKB = this.totalMemoryKB;
this.memoryContent = new Uint8Array(this.totalBytes);
this.hardwareProfile = storageType;
}

The motherboard serves as a I/O board that verifies if its slots are occupied with valid devices and returns a class with “what’s plugged in”. I decided to use custom interfaces to construct types that would ensure the compatibility requirements of slots were met.

This approach allows me to come back to this module and expand on the idea in the future. For now the pcieOne and pcieTwo ports as per pcConstructorProperties take a pcieSlotConfig type which would offer us with either a null value (nothing plugged in) or an interface that’s gpu or storage.

The motherboard can ultimately have anything plugged in (assuming compatibility requirements are met), at a minimum it needs GPU, Storage and RAM to be a valid construction of a motherboard otherwise returning a configurationError.

motherboard/types.ts

src/simulated-hardware/motherboard/types.ts
import { GPU } from '../gpu/gpu.class'
import { RAM } from '../ram/ram.class'
import { StorageDevice } from '../storage/storage-device.class'
interface GpuPcieProperties {
type: 'gpu'
profile: GPU
}
interface StorageProperties {
type: 'storage'
profile: StorageDevice
}
export type PCIeDevice = GPU | StorageDevice | null
export type pcieSlotConfig = GpuPcieProperties | StorageProperties | null
export interface pcConstructorProperties {
dimSlotOne?: RAM
dimSlotTwo?: RAM
dimSlotThree?: RAM
dimSlotFour?: RAM
ideOne?: StorageProperties
ideTwo?: StorageProperties
sataOne?: StorageProperties
pcieOne?: pcieSlotConfig
pcieTwo?: pcieSlotConfig
}

motherboard.class.ts

src/simulated-hardware/motherboard/motherboard.class.ts
import { RAM } from '../ram/ram.class'
import { StorageDevice } from '../storage/storage-device.class'
import type { pcConstructorProperties } from './types'
import type { PCIeDevice } from './types'
export class MotherBoard {
public dimSlotOne: RAM | null = null
public dimSlotTwo: RAM | null = null
public dimSlotThree: RAM | null = null
public dimSlotFour: RAM | null = null
public ideOne: StorageDevice | null = null
public ideTwo: StorageDevice | null = null
public sataOne: StorageDevice | null = null
public pcieOne: PCIeDevice | null = null
public pcieTwo: PCIeDevice | null = null
constructor({
dimSlotOne,
dimSlotTwo,
dimSlotThree,
dimSlotFour,
ideOne,
ideTwo,
sataOne,
pcieOne,
pcieTwo,
}: pcConstructorProperties) {
if (!dimSlotOne && !dimSlotTwo && !dimSlotThree && !dimSlotFour) {
this.configurationError(`Missing RAM`)
}
if (pcieOne?.type !== 'gpu' && pcieTwo?.type !== 'gpu') {
this.configurationError(`Missing GPU`)
}
if (
!ideOne &&
!ideTwo &&
!sataOne &&
pcieOne?.type !== 'storage' &&
pcieTwo?.type !== 'storage'
) {
this.configurationError(`Missing Storage`)
}
if (dimSlotOne) this.dimSlotOne = dimSlotOne
if (dimSlotTwo) this.dimSlotTwo = dimSlotTwo
if (dimSlotThree) this.dimSlotThree = dimSlotThree
if (dimSlotFour) this.dimSlotFour = dimSlotFour
if (ideOne && ideOne.type == 'storage') this.ideOne = ideOne.profile
if (ideTwo && ideTwo.type == 'storage') this.ideTwo = ideTwo.profile
if (sataOne && sataOne.type == 'storage') this.sataOne = sataOne.profile
if (pcieOne) this.pcieOne = pcieOne.profile
if (pcieTwo) this.pcieTwo = pcieTwo.profile
}
private configurationError(errorMessage: string) {
throw new Error(`PC: ${errorMessage}`)
}
public currentConfig() {
return {
dimSlotOne: this.dimSlotOne,
dimSlotTwo: this.dimSlotTwo,
dimSlotThree: this.dimSlotThree,
dimSlotFour: this.dimSlotFour,
ideOne: this.ideOne,
ideTwo: this.ideTwo,
sataOne: this.sataOne,
pcieOne: this.pcieOne,
pcieTwo: this.pcieTwo,
}
}
}

I introduced the concept of firmware when I was separating concerns related to verifying an option ROM and the master boot record. In a normal scenario an operating system would initially be installed off external boot media with a valid boot signatures however in this scenario the simulation would assume that the operating system was “pre-installed” and these installation steps had been completed. I made a choice to relegate the master boot record to a “firmware” function, this would allow for me to create different signatures, should I for example want to dynamically load a Linux inspired MBR onto a Storage device in the future.

To create the master boot record the approach I took wasn’t dissimilar to how it would work in a real operating system. The first step was to set some variables.

  • diskSectorSize - This would be the smallest size allocated for each sector, similar to a real device we chose 512 as the smallest divisible number.
  • pointerVariables - These variables are the memory addresses reserved to identify characteristics of the the boot record we have:
    • 446: Pointer which returns if partition is Active or Inactive.
    • 450: Partition type (NTFS, FAT etc.)
    • 510 and 511: These pointers are reserved to let the BIOS know if the partition has a boot signature.
    • 328 and 329: For the simulation I reserved pointer 328 and 329 to be our OS Signature, in a real operating system unreserved bytes in the MBR would store the bootloader software. As I was going straight into kernel mode depending on signature found I simplified the logic to match - it also means we can make different operating systems without needing to build a bootloader for each one.
  • partitionVariables: To accompany the above memory addresses the partition variables are set into two categories.
    • Storage Specific - File Type, Active/Inactive
    • OS Specific - Boot Signature Values, OS Signature Values (For our simulation we only have the Windows ones for now.)

const.ts

src/firmware/storage/const.ts
// MBR Constants
export const diskSectorSize = 512
// MBR - Pointers
export const pointerBootablePartition = 446
export const pointerPartitionType = 450
export const pointerBootSignatureOne = 510
export const pointerBootSignatureTwo = 511
export const pointerOSSignatureOne = 328
export const pointerOSSignatureTwo = 329
// MBR - Flags/Data
export const partitionActive = 0x80
export const partitionInactive = 0x00
export const partitionTypeNTFS = 0x07
export const partitionTypeFat32 = 0x0c
// MBR - OS Sigantures
export const osBootSignatureOne = 0x55
export const osBootSignatureTwo = 0xaa
export const osSignatureBindowsXDOne = 0xff
export const osSignatureBindowsXDTwo = 0xee

With the values set now it was time to translate this to a class structure - I opted to take four variables for the constructor as captured by the types file:

  • partitionType: a simple union type that would translate different filesystem types.
  • sectorSize: a number representing the number of bytes per sector.
  • osSingatureOne and osSignatureTwo: our unique system implemented to invoke the correct operating system from BIOS.

types.ts

src/firmware/storage/types.ts
type MasterBootRecordPartitionType = 'ntfs' | 'fat32'
export interface MasterBootRecordTypeConstructorProperties {
partitionType: MasterBootRecordPartitionType
sectorSize: number
osSignatureOne: number | null
osSignatureTwo: number | null
}

Finally I combined the variables and put together the MasterBootRecord class when constructed would return a Uint8Array which could be directly written into the previous StorageDevice class.

The class when constructed would first ensure the constructor arguments adhered to the original architecture specifications (512 byte length, active partition, valid OS signature and valid filesystem type). Once checks are complete it sets the this.masterBootRecord property to the typed array with the crucial pointers having the correct corresponding data.

I also included a validateMasterBootRecord function which would return true if all specifications were met else returning false.

master-boot-record.class.ts

src/firmware/storage/master-boot-record.class.ts
import type { MasterBootRecordTypeConstructorProperties } from './types'
import {
pointerBootSignatureOne,
pointerBootSignatureTwo,
pointerBootablePartition,
pointerOSSignatureOne,
pointerOSSignatureTwo,
pointerPartitionType,
partitionActive,
partitionInactive,
partitionTypeFat32,
partitionTypeNTFS,
osBootSignatureOne,
osBootSignatureTwo,
} from './const'
export class MasterBootRecord {
public masterBootRecord: Uint8Array
constructor({
partitionType,
sectorSize,
osSignatureOne,
osSignatureTwo,
}: MasterBootRecordTypeConstructorProperties) {
this.masterBootRecord = new Uint8Array(sectorSize).fill(0x00)
if (sectorSize !== 512) {
this.masterBootRecord[pointerBootablePartition] = partitionInactive
throw new Error('Master Boot Record: Sector Size not supported.')
}
this.masterBootRecord[pointerBootablePartition] = partitionActive
if (!partitionType) {
this.masterBootRecord[pointerBootablePartition] = partitionInactive
throw new Error('Master Boot Record: Missing Partition Type')
}
if (partitionType === 'ntfs') {
this.masterBootRecord[pointerPartitionType] = partitionTypeNTFS
} else if (partitionType === 'fat32') {
this.masterBootRecord[pointerPartitionType] = partitionTypeFat32
}
if (!osBootSignatureOne || !osBootSignatureTwo) {
this.masterBootRecord[pointerBootablePartition] = partitionInactive
throw new Error(
'Master Boot Record: Cannot create a master boot record without boot signature'
)
}
this.masterBootRecord[pointerBootSignatureOne] = osBootSignatureOne
this.masterBootRecord[pointerBootSignatureTwo] = osBootSignatureTwo
if (!osSignatureOne || !osSignatureTwo) {
this.masterBootRecord[pointerBootablePartition] = partitionInactive
throw new Error('Master Boot Record: No OS Signature provided.')
}
this.masterBootRecord[pointerOSSignatureOne] = osSignatureOne
this.masterBootRecord[pointerOSSignatureTwo] = osSignatureTwo
}
public validateMasterBootRecord(): boolean {
if (!this.masterBootRecord) return false
return (
this.masterBootRecord.length === 512 &&
this.masterBootRecord[pointerBootablePartition] ===
partitionActive &&
(this.masterBootRecord[pointerPartitionType] ===
partitionTypeNTFS ||
this.masterBootRecord[pointerPartitionType] ===
partitionTypeFat32) &&
this.masterBootRecord[pointerBootSignatureOne] ===
osBootSignatureOne &&
this.masterBootRecord[pointerBootSignatureTwo] ===
osBootSignatureTwo &&
this.masterBootRecord[pointerOSSignatureOne] !== 0x00 &&
this.masterBootRecord[pointerOSSignatureTwo] !== 0x00
)
}
}

Finally we can create a custom const for a “Windows” master boot record (if you haven’t noticed yet my simulated OS working title is Bindows XD).

windows-master-boot-record.ts

src/firmware/storage/windows-master-boot-record.ts
import { MasterBootRecord } from './master-boot-record.class'
import { osSignatureBindowsXDOne, osSignatureBindowsXDTwo } from './const'
export const windowsMasterBootRecord = new MasterBootRecord({
partitionType: 'ntfs',
sectorSize: 512,
osSignatureOne: osSignatureBindowsXDOne,
osSignatureTwo: osSignatureBindowsXDTwo,
})

Now that I have explained the logic behind how pointers and data is set when dealing with storage - I can expand on the GPU’s role. For now I have decided to use Angular as a “Graphical Device Interface Service” rather then building something custom to simulate the OS GDI Layer. There is however a crucial role a GPU can play in the boot process - mainly housing the option ROM functionality.

An option ROM allows you to use basic peripherals prior to your device loading an operating system (as an example display and Input support when configuring your BIOS). Like a Master Boot Record, an Option Rom also has a unique signature noted by the first two bytes accessed on the devices onboard storage. For our simulation the GPU houses the option ROM and the option ROM’s function is merely to be validated during POST (nothing more at this stage). This means I can come back later and expand on this idea should I decide to expand on the BIOS functionality or work on a GDI service/expand the GPU’s role and functions.

const.ts

src/firmware/gpu/const.ts
export const optionRomBootSignatureOne: number = 0x55
export const optionRomBootSignatureTwo: number = 0xaa
export const pointerOptionRomBootSignatureOne = 0
export const pointerOptionRomBootSignatureTwo = 1

option-rom.class.ts

src/firmware/gpu/option-rom.class.ts
import {
pointerOptionRomBootSignatureOne,
pointerOptionRomBootSignatureTwo,
optionRomBootSignatureOne,
optionRomBootSignatureTwo,
} from './const'
export class OptionRom {
public optionRomData: Uint8Array
constructor() {
this.optionRomData = new Uint8Array(2).fill(0x00)
this.optionRomData[pointerOptionRomBootSignatureOne] =
optionRomBootSignatureOne
this.optionRomData[pointerOptionRomBootSignatureTwo] =
optionRomBootSignatureTwo
}
public validateOptionRom(): boolean {
if (!this.optionRomData) return false
return (
this.optionRomData.length === 2 &&
this.optionRomData[pointerOptionRomBootSignatureOne] ===
optionRomBootSignatureOne &&
this.optionRomData[pointerOptionRomBootSignatureTwo] ===
optionRomBootSignatureTwo
)
}
}

Now we have our building blocks to build a PC ready to be inspected by a BIOS function. Below is what all the parts together look like. First I created a type interface which allocated compatibility for each slot available in a fully constructed PC

types.ts

src/simulated-hardware/pc-builds/types.ts
import { RAM } from '../ram/ram.class'
import { StorageDevice } from '../storage/storage-device.class'
import { PCIeDevice } from '../motherboard/types'
export interface FullSystem {
dimSlotOne: RAM | null
dimSlotTwo: RAM | null
dimSlotThree: RAM | null
dimSlotFour: RAM | null
ideOne: StorageDevice | null
ideTwo: StorageDevice | null
sataOne: StorageDevice | null
pcieOne: PCIeDevice | null
pcieTwo: PCIeDevice | null
}

Following this a helper function was created to return a default system spec - for now this is functional. I will move this to a class at a later stage with the ability to choose multiple builds for testing.

src/simulated-hardware/pc-builds/pc-builds.ts
import { GPU } from '../gpu/gpu.class'
import { RAM } from '../ram/ram.class'
import { StorageDevice } from '../storage/storage-device.class'
import { MotherBoard } from '../motherboard/motherboard.class'
import { windowsMasterBootRecord } from '../../firmware/storage/windows-master-boot-record'
import { OptionRom } from '../../firmware/gpu/option-rom.class'
import type { FullSystem } from './types'
export function defaultSystemSpecs(): FullSystem {
const ram1 = new RAM(64)
const gpu = new GPU(64)
const ideone = new StorageDevice('IDE_HDD_ONE', 64)
const windowsMBR = windowsMasterBootRecord
const gpuOptionRom = new OptionRom()
if (windowsMBR.validateMasterBootRecord()) {
// represent kb as bytes for memory allocation
ideone.allocateMemory(windowsMBR.masterBootRecord.length / 1024)
ideone.writeMemory(0, windowsMBR.masterBootRecord)
}
if (gpuOptionRom.validateOptionRom()) {
// represent kb as bytes for memory allocation
gpu.allocateMemory(gpuOptionRom.optionRomData.length / 1024)
gpu.writeMemory(0, gpuOptionRom.optionRomData)
}
const pc = new MotherBoard({
dimSlotOne: ram1,
pcieOne: { type: 'gpu', profile: gpu },
ideOne: { type: 'storage', profile: ideone },
})
return pc.currentConfig()
}

As you can see the RAM, GPU, Storage, Option ROM and Windows MBR classes are instantiated. A validation of the option ROM and MBR then allocate and write memory to storage and GPU. Resulting in the return of a constructed PC via the MotherBoard class.

I mentioned earlier following a test driven development model for each of these components iterating on functionality by building to pass tests. This methodology has not only been useful in accounting for edge cases early but it has also been an invaluable tool when I refactored code or separated concerns helping me rapidly find variables/changes that may have causes chain reactions across my project where there were joint dependencies. Below I have included some simulated hardware tests as an example. I have made it a a point to go back and ensure these tests are updated to reduce any burdens caused by refactoring at a later stage.

gpu.test.ts

src/simulated-hardware/__tests__/gpu.test.ts
import { GPU } from '../gpu/gpu.class'
/// <reference types="jest" />
describe('GPU', () => {
let simulatedGPU: GPU
beforeEach(() => {
simulatedGPU = new GPU(64)
})
it(`Should return the hardware specifications as per it's constructor correctly`, () => {
expect(simulatedGPU.totalMemoryKB).toBe(64)
expect(simulatedGPU.availableMemoryKB).toBe(64)
expect(simulatedGPU.memoryContent.length).toBe(64 * 1024)
})
it('Should allocate and return the first "byte address" being used', () => {
expect(simulatedGPU.allocateMemory(10)).toBe(0)
expect(simulatedGPU.allocateMemory(20)).toBe(10240)
expect(simulatedGPU.allocateMemory(30)).toBe(30720)
})
})

motherboard.test.ts

src/simulated-hardware/__tests__/MotherBoard.test.ts
/// <reference types="jest" />
import { MotherBoard } from '../motherboard/motherboard.class'
import { GPU } from '../gpu/gpu.class'
import { RAM } from '../ram/ram.class'
import { StorageDevice } from '../storage/storage-device.class'
describe('PC', () => {
let simulatedPC: MotherBoard
beforeEach(() => {})
it('should construct a valid PC build with minimum requirements', () => {
const ram1 = new RAM(64)
const gpu = new GPU(64)
const ideone = new StorageDevice('IDE_HDD', 64)
const pc = new MotherBoard({
dimSlotOne: ram1,
pcieOne: { type: 'gpu', profile: gpu },
ideOne: { type: 'storage', profile: ideone },
})
expect(pc.currentConfig()).toStrictEqual({
dimSlotOne: ram1,
dimSlotTwo: null,
dimSlotThree: null,
dimSlotFour: null,
ideOne: ideone,
ideTwo: null,
sataOne: null,
pcieOne: gpu,
pcieTwo: null,
})
})
it('should throw an error if no RAM is provided', () => {
const pcieGPU = new GPU(64)
const bootDriveIDE = new StorageDevice('IDE_HDD', 64)
expect(() => {
new MotherBoard({
pcieOne: { type: 'gpu', profile: pcieGPU },
ideOne: { type: 'storage', profile: bootDriveIDE },
})
}).toThrow('PC: Missing RAM')
})
it('should throw an error if no GPU is provided in PCIe slots', () => {
const slotOneRam = new RAM(64)
const bootDriveIDE = new StorageDevice('IDE_HDD', 64)
expect(() => {
new MotherBoard({
dimSlotOne: slotOneRam,
ideOne: { type: 'storage', profile: bootDriveIDE },
})
}).toThrow('PC: Missing GPU')
})
it('should throw an error if no storage device is provided', () => {
const slotOneRam = new RAM(64)
const pcieGPU = new GPU(64)
expect(() => {
new MotherBoard({
dimSlotOne: slotOneRam,
pcieOne: { type: 'gpu', profile: pcieGPU },
})
}).toThrow('PC: Missing Storage')
})
it('should allow multiple RAM and storage devices', () => {
const ram1 = new RAM(16)
const ram2 = new RAM(16)
const sataDrive = new StorageDevice('SATA_SSD', 128)
const gpu = new GPU(128)
const pc = new MotherBoard({
dimSlotOne: ram1,
dimSlotTwo: ram2,
sataOne: { type: 'storage', profile: sataDrive },
pcieOne: { type: 'gpu', profile: gpu },
})
expect(pc.currentConfig()).toStrictEqual({
dimSlotOne: ram1,
dimSlotTwo: ram2,
dimSlotThree: null,
dimSlotFour: null,
ideOne: null,
ideTwo: null,
sataOne: sataDrive,
pcieOne: gpu,
pcieTwo: null,
})
})
})

At this stage I felt ready to start tackling the BIOS - at a high level my plan was to:

  1. Take an assembled PC in its constructor
  2. Detect components in PC at a BIOS level.
  3. Conduct a Power on Self Test (POST) to ensure the PC would boot
  4. Check and validate a boot signature and option ROM signature
  5. Invoke an interrupt (19h) to boot into Kernel

The BIOS class would mirror the motherboard when it comes to slot properties alongside a few tracking variables.

  public eventLog: Array<string> = [];
  public postResults: boolean = false;
private dimSlotOne: RAM | null = null;
  private dimSlotTwo: RAM | null = null;
  private dimSlotThree: RAM | null = null;
  private dimSlotFour: RAM | null = null;
  private ideOne: StorageDevice | null = null;
  private ideTwo: StorageDevice | null = null;
  private sataOne: StorageDevice | null = null;
  private pcieOne: PCIeDevice | null = null;
  private pcieTwo: PCIeDevice | null = null;
  private bootTable: Array<StorageDevice> = [];
  private totalMemory: number = 0;
  private totalStorage: number = 0;
  private totalGpuMemory: number = 0;

The two public properties would be the eventLog and postResults with the rest being reserved for the internal state of the class.

The constructor takes an assembled PC, runs a detectDevices function followed by a postResults (Power on Self Test), if the POST fails it returns an error, if it passes it invokes Interrupt 19h (further explained below).

  constructor(assembledPC: FullSystem) {
    if (!assembledPC) {
      this.updateEventLog(`PC: Assembled PC Not Found`, true);
      return;
    }
    this.detectDevices(assembledPC);
    this.postResults = this.powerOnSelfTest();
    if (!this.postResults) {
      this.updateEventLog(
        'BIOS: POST FAILED During boot-up. System cannot proceed.',
        true,
      );
    }
    this.interrupt('19h');
  }

Not much commentary required for the detectDevice Function; it simply translates the slots that are occupied from our assembledPC to the private BIOS variables.

  private detectDevices(assembledPC: FullSystem) {
    if (assembledPC.dimSlotOne) this.dimSlotOne = assembledPC.dimSlotOne;
    if (assembledPC.dimSlotTwo) this.dimSlotTwo = assembledPC.dimSlotTwo;
    if (assembledPC.dimSlotThree) this.dimSlotThree = assembledPC.dimSlotThree;
    if (assembledPC.dimSlotFour) this.dimSlotFour = assembledPC.dimSlotFour;
    if (assembledPC.ideOne) this.ideOne = assembledPC.ideOne;
    if (assembledPC.ideTwo) this.ideTwo = assembledPC.ideTwo;
    if (assembledPC.sataOne) this.sataOne = assembledPC.sataOne;
    if (assembledPC.pcieOne) this.pcieOne = assembledPC.pcieOne;
    if (assembledPC.pcieTwo) this.pcieTwo = assembledPC.pcieTwo;
  }

I included a utility function to track BIOS level events. The intent is to take an event object to lift up to the Kernel (and eventually Event Manager). For now the function takes the arguments of a message and isError. Pushing to our public Array<string> typed eventLog a message entry.

When the function is called a new Date object with the current system time is broken into a string representation of hours, minutes and seconds, padding the start with two zeros to create a time stamp. We then concatenate our timestamp with the event message to update our event log entry. The isError is used to display our message in the DOM as either a console.error or console.log.

  public updateEventLog(message: string, isError?: boolean): Array<string> {
    const date = new Date();
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    const timeStamp = `[${hours}:${minutes}:${seconds}]`;
    const concatEventMessage = `${timeStamp} ${message}`;
    this.eventLog.push(concatEventMessage);
    if (isError) {
      console.error(concatEventMessage);
    } else {
      console.log(concatEventMessage);
    }
    return this.eventLog;
  }

The Power on Self Test is responsible for verifying if the hardware is “functioning”. The function is private to the class and starts with an updateEventLog function call.

this.updateEventLog('BIOS: Starting Power-On Self-Test (POST)...')
let overallPostPassed = true
let atLeastOneGPUFound = false
let atLeastOneBootDriveFound = false

For the simulation the POST begins with a overallPostPassed boolean set to true, I also created a variable for atLeastOneGPUFound and atLeastOneBootDriveFound. Whilst technically both of these flags are not required for an actual POST function. I went with integrating these two “checks” into the POST to simulate a verifying an option ROM and bootloader. Should I decide to research bootloaders in the future I can easily create a class that sits between our BIOS and Kernel that can act as an additional interface.

Setting some constants helped maintain modularity when testing various slot without having to repeat code. Using the structure of an object entry inside an array I set up storageSupporingSlots, gpuSupportingSlots and ramSupportingSlots.

I also accounted for scenarios where a GPU or a StorageDevice could be using a PCIE slot by using typescripts instanceof type check to determine, and return, the matching class ensuring edge cases were covered.

const storageSupportingSlots = [
{ name: 'IDE ONE:', device: this.ideOne },
{ name: 'IDE TWO:', device: this.ideTwo },
{ name: 'SATA ONE:', device: this.sataOne },
{
name: 'PCIE ONE:',
device: this.pcieOne instanceof StorageDevice ? this.pcieOne : null,
},
{
name: 'PCIE TWO:',
device: this.pcieTwo instanceof StorageDevice ? this.pcieTwo : null,
},
]
const gpuSupportingSlots = [
{
name: 'PCIE ONE:',
device: this.pcieOne instanceof GPU ? this.pcieOne : null,
},
{
name: 'PCIE TWO:',
device: this.pcieTwo instanceof GPU ? this.pcieTwo : null,
},
]
const ramSupportingSlots = [
{ name: 'DIM SLOT ONE:', device: this.dimSlotOne },
{ name: 'DIM SLOT TWO:', device: this.dimSlotTwo },
{ name: 'DIM SLOT THREE:', device: this.dimSlotThree },
{ name: 'DIM SLOT FOUR:', device: this.dimSlotFour },
]

I was now able to construct a for loop that would take the array and run the right battery of tests based on “slot type” (this pattern is repeated for GPU and Storage). As the class slot properties are constructed with either null or a valid object the property would be mirrored for slotInfo.device reducing operations and only testing on slots that are “occupied”.

For RAM a postTestMemory is first called returning a boolean value followed by adding to the totalMemory of the bios class alongside an update to the overallPostPassed based on a combination of the existing boolean and ramTestPassed.

for (const slotInfo of ramSupportingSlots) {
if (slotInfo.device) {
const ramTestPassed = this.postTestMemory(
slotInfo.name,
slotInfo.device
)
this.totalMemory += slotInfo.device.availableMemoryKB
overallPostPassed = overallPostPassed && ramTestPassed
}
}

The postTestMemory function is used in both for loops for ramSupportingSlots and storageSupporingSlots. It initially logs an event message followed by a isFunctioning check. Once complete it ensures the memory modules available memory is above 0 before returning true for the test being passed.

  private postTestMemory(slot: string, detectedMemoryModule: RAM): boolean {
    this.updateEventLog(`POST: ${slot} Testing Memory.`);
    if (!this.isFunctioning(detectedMemoryModule, slot)) {
      return false;
    }
    if (detectedMemoryModule.totalMemoryKB <= 0) {
      this.updateEventLog(
        `POST: ${slot} ${detectedMemoryModule.constructor.name} Zero Memory Detected.`,
        true,
      );
      return false;
    }
    this.updateEventLog(
      `POST: ${slot} ${detectedMemoryModule.constructor.name}: ${detectedMemoryModule.constructor.name} Initialised.`,
    );
    return true;
  }

isFunctioning is purely decorative - we don’t really need this in typescript. We are just simulating that the slot is powered and returning a 1 or a 0 basically.

  private isFunctioning(
    testModule: RAM | StorageDevice | GPU | PCIeDevice | null,
    slotName: string,
  ): boolean {
    if (!testModule) {
      this.updateEventLog(`POST: ${slotName} Hardware Detection issue.`, true);
      return false;
    }
    this.updateEventLog(
      `POST: ${slotName} ${testModule.constructor.name} detected.`,
    );
    return true;
  }

Again the for loop cycles through gpuSupporingSlots this time calling the postTestGPU function.

for (const slotInfo of gpuSupportingSlots) {
if (slotInfo.device) {
const gpuTestPassed = this.postTestGPU(slotInfo.name, slotInfo.device)
overallPostPassed = overallPostPassed && gpuTestPassed
this.totalGpuMemory += slotInfo.device.availableMemoryKB
if (gpuTestPassed) {
atLeastOneGPUFound = true
}
}
}

As relayed earlier to reduce complexity around deploying a full bootloader the POST relies on a GPU not only being loaded but also including a valid option rom signature within its first two bytes.

  private postTestGPU(slot: string, gpuModule: GPU): boolean {
    this.updateEventLog(`POST: ${slot} Testing GPU.`);
    if (!this.isFunctioning(gpuModule, slot)) {
      return false;
    }
    const gpuSignature = gpuModule.readMemory(0, 2);
    if (
      gpuSignature[0] !== optionRomSignatureOne ||
      gpuSignature[1] !== optionRomSignatureTwo
    ) {
      this.updateEventLog(
        `POST ERROR: ${slot} GPU Option ROM Signature Not Found.`,
        true,
      );
      return false;
    } else {
      this.updateEventLog(`POST: ${slot} GPU: Option Rom Signature Found.`);
    }
    this.updateEventLog(`POST: ${slot} GPU: GPU Initialised.`);
    return true;
  }

The final for loop cycle continues with the same tests on storage with an additional check to add devices to a bootTable if the check returns true on a bootSignature

for (const slotInfo of storageSupportingSlots) {
if (slotInfo.device) {
const storageTestPassed = this.postTestStorage(
slotInfo.name,
slotInfo.device
)
if (slotInfo.device.availableMemoryKB) {
this.totalStorage += slotInfo.device.availableMemoryKB
}
overallPostPassed = overallPostPassed && storageTestPassed
if (storageTestPassed) {
const isBootable = this.checkBootSignature(
slotInfo.name,
slotInfo.device
)
if (isBootable) {
this.bootTable.push(slotInfo.device)
atLeastOneBootDriveFound = true
}
}
}
}

and the checkbootSignature function similar to our GPU check returns true if the MBR sectors last two bytes return a boot signature (0x55, 0xaa).

  private checkBootSignature(
    slot: string,
    detectedStorageModule: StorageDevice,
  ): boolean {
    this.updateEventLog(
      `POST: ${slot} ${detectedStorageModule.hardwareProfile} Drive: Searching for Boot Signature in Storage Device.`,
    );
    const bootSignature = detectedStorageModule.readMemory(0, 512);
    if (
      bootSignature[510] !== bootSignatureOne ||
      bootSignature[511] !== bootSignatureTwo
    ) {
      this.updateEventLog(
        `POST: ${slot} ${detectedStorageModule.hardwareProfile} Drive: Boot Signature not found.`,
      );
      return false;
    }
    this.updateEventLog(
      `POST: ${slot} ${detectedStorageModule.constructor.name}: Boot Signature Found.`,
    );
    return true;
  }
}

The last step in POST is a boolean return with a final update to our event log assuming all the tests have passed. This marks the conclusion of the POST function with our Interrupt19h being called next in the constructor should POST be successful.

overallPostPassed =
overallPostPassed && atLeastOneBootDriveFound && atLeastOneGPUFound
this.updateEventLog('BIOS: POST Completed successfully...')
return overallPostPassed

Currently the only Interrupt I have implemented in 19h. Interrupt calls are invoked by operating systems and application programs as an interface to firmware. For our example we are using the 19h call to load our “bootloader”. As mentioned earlier as we are going straight from BIOS to Kernel our 19h call in this example will instantiate a new Kernel object. Using a switch case I have set the foundation for future interrupts with relevant event logging and comments as a reminder of the functionality of each call.

  private interrupt(
    interuptVector: string,
    device?: StorageDevice | GPU | RAM | null,
  ): void {
    switch (interuptVector) {
      // TODO: Expand Interupts if i ever decide to simulate CPU functionality - for now 19h is enough.
      // Proposed Interrupts for CPU Branch
      case '12h':
        this.updateEventLog('Interrupt 12h invoked - Returning Memory Size...');
        break;
      case '13h01h':
        this.updateEventLog('Interrupt 13h01h invoked - Check Drive Status...');
        break;
      case '13h01h':
        this.updateEventLog('Interrupt 13h01h invoked - Check Drive Status...');
        break;
      case '13h02h':
        this.updateEventLog('Interrupt 13h02h invoked - 02h   Read Sectors...');
        break;
      case '13h03h':
        this.updateEventLog('Interrupt 13h03h invoked - 02h   Write Sectors...');
        break;
      // Implemented Interrupt to find bootloader.
      case '19h':
        this.updateEventLog(
          'Interrupt 19h: Invoked - Searching for Boot Table...',
        );
        if (this.bootTable.length > 0) {
          this.updateEventLog(
            'Interrupt 19h: Boot Table Found - Searching for OS...',
          );
          this.interrupt19h();
        }
        break;
      default:
        break;
    }
  }

Finally the interrupt19h function will check the OS Signature and return a new kernel object accordingly (The return has not been implemented yet). This structure allows for me to create support for other operating systems in the future.

  private interrupt19h() {
    for (const bootDrive of this.bootTable) {
      this.updateEventLog(
        `Interrupt 19h: Checking OS Signature on ${bootDrive.constructor.name}`,
      );
      const osSignatureAddress = bootDrive.readMemory(328, 2);
      if (
        osSignatureAddress[0] === osSignatureOneBindows &&
        osSignatureAddress[1] === osSignatureTwoBindows
      ) {
        this.updateEventLog(
          `Interrupt 19h: OS Signature on ${bootDrive.constructor.name} matches BindowsXD`,
        );
      }
    }
  }

I hope you enjoyed part one of my journey so far. This project has helped me really think outside the box, it has driven me to change and adapt things to match a paradigm and constraints. At this stage I am working on a NT Architecture kernel splitting objects and functionality between User and Executive mode with various services which will make up the core of the operating system being orchestrated accordingly. In part two (once I finish most of the kernel level structure and code) I will provide a lot more insight into the NT Architecture and the challenges I faced implementing my simulated approach.