A baremetal C# kernel - Frank Ray, Better Software UK.
Built using a combination of Microsoft's C# IL and native AOT compilers, and the GNU toolchain.
Learnings from PatienceOS are being captured in a series of blog posts, and a project wiki does exist, although remains largely a collection of working notes. The following README remains the best source of infrequently changing, production quality documentation.
Important
PatienceOS has been superseded by InstructionOS, an ANSI C kernel, for reasons described here.
- Commentary on the name
- Progress
- Playing with PatienceOS
- Developing PatienceOS
- Contributing
- Build toolchain
PatienceOS was chosen to remind Frank this is a 12-month initial project (at least), as part of his 2024 professional development goals. The dopamine hit from quickly pushing out PR's, like he regularly enjoys from contributing to other open-source repos (eg. spectre.console), simply won't be possible with OS development. Hence the need for patience, and perseverance.
PatienceOS isn't a fully-fledged 'operating system', but it does boot and is being actively developed. The obligatory screenshot (which is pretty underwhelming at the moment) is here:
As of 15 April 2024, PatienceOS is a 32-bit kernel that boots in protected mode on an emulated x86 machine. The bootstrap is written in assembly and supports the Multiboot Specification, version 2. The main kernel is written in C#, linked against a custom runtime library, and compiled to bare metal using the Microsoft .Net AOT compiler. There is a basic console class that supports writing characters to the screen in VGA text mode. Extensive unit tests have been written for the console and the underlying framebuffer video memory.
See the description of each Release for the most accurate list of features supported by the PatienceOS kernel.
Tip
Please upvote ๐ the issues you are most interested in, the Top Issues Dashboard tracks community interest.
I love Visual Studio and find it a far superior IDE compared to VS Code. Unfortunately, the lack of a Linux Visual Studio version means I'm tied to Windows, at least for now (probably forever). This is my primary reason for using Windows as my development machine ('nix users, don't hate on me).
Perform the following one-off installs, ideally in a virtual machine:
- Microsoft .Net 8, Visual Studio 2022
- MSYS2 MINGW64, see: MSYS2-Installation
- QEMU, the native Windows binaries, see: Download QEMU, nb. I used the latest Stefan Weil 64-bit Windows installer
ILC needs to be installed locally. It's a nuget package and will be present if you have configured and built at least one C# .Net project that emits native AOT code. I'm using version 8.0.1 of the IL compiler and so expect to see it installed in the nuget cache for my user, info
, here: C:\Users\info\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.1\tools
. If you don't see something similar, then perform the following steps in a temporary directory to create a temporary application, simply for the purpose of installing ILC locally:
dotnet new console -n MyConsoleApp
cd MyConsoleApp
dotnet add package Microsoft.DotNet.ILCompiler
EITHER:
- Load the project in Visual Studio and enable 'Publish native AOT', or
- Edit the csproj file and add <PublishAot>True</PublishAot> within the PropertyGroup
dotnet build
ilc.exe
should now be installed here: C:\Users\info\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\8.0.1\tools
Open the MSYS2 MINGW64 console and perform the following actions:
- A full package upgrade:
pacman -Syuu
, - Install binutils:
pacman -S mingw-w64-x86_64-binutils
(required to get a Windows compiledobjcopy
) - Install NASM:
pacman -S mingw-w64-x86_64-nasm
Several tools are required in the build and link process. Update src\setpath.cmd
to point to the correct install locations on your machine. The existing entries in the file will give you a good idea of where to find them, if you have followed the instructions above and accepted default install locations.
- Open
x64 Native Tools Command Prompt for VS 2022
- Set the correct tool paths in the console session,
src\setpath.cmd
(only do this once, after opening the command prompt) - Compile, link and boot,
src\build.cmd
Patience OS, a baremetal C# kernel, should boot in QEMU.
There are three different ways to build PatienceOS, each with a very different purpose in mind. These are:
File | Type | Purpose |
---|---|---|
build\build.cmd |
Windows Command script | Builds the kernel and boots in QEMU (see Playing with PatienceOS above for instructions) |
src\PatienceOS.NoStdLib.sln |
Visual Studio 2022 solution; only contains the PatienceOS project | Builds and links the kernel against the custom .Net runtime, zerosharp.cs . Handy for when you are coding PatienceOS within Visual Studio and want to quickly check building against the custom runtime types. |
src\PatienceOS.sln |
Visual Studio 2022 solution; contains the PatienceOS project and accompanying unit test project | Builds and links the kernel against the standard .Net 8.0 runtime. Allows you to run the PatienceOS unit tests within the built-in Visual Studio Test Explorer, as per any other unit test project. |
Important
I'm not currently accepting pull requests as this is a personal learning project.
Please upvote ๐ any existing issues you are interested in being worked on. You can also contribute to discussions with your fellow GitHub users and me by commenting on existing issues, and by raising new issues you want to be considered for development.
All reasonable interactions are welcomed but please don't be offended if I close or delete issues or comments etc, as I see fit, as this is my personal learning project. You are welcome to fork the repository for your own purposes.
Relying on Visual Studio specific, MSBuild or csproj files to build the kernel has been avoided in favour of directly calling the individual build tools. I want fine-grained control over the compile and link process, MSBuild feels like a poorly documented 'black box' that changes every .Net release in subtle ways that aren't clear, and I don't want to invest a huge amount of time developing a C# kernel only to find I can't build it in some future .Net release.
I would have sincerely loved to use bflat as my IL to native compiler, but even that didn't seem to allow intermediate object file output for class libraries, and I didn't want to raise an issue requesting the CLI options expose more of the underlying compiler switches. And so I've stuck to the approach demo'd in zerosharp, namely a Windows command/batch file (build.cmd).
The following lines in the PatienceOS src\build.cmd
are of particular note:
csc /debug:embedded /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 ../src/kernel.cs ../src/zerosharp.cs /out:kernel.ilexe /langversion:latest /unsafe
/noconfig: Don't reference the standard set of assemblies and configuration files.
/nostdlib: Don't reference the standard library assemblies.
/unsafe: Allow unsafe code blocks in C#, enabling pointers and other low-level constructs that aren't type-safe.
ilc --targetos windows --targetarch x86 kernel.ilexe -g -o kernel.obj --systemmodule kernel --map kernel.map -O
--targetos windows: The code is intended to run on the Windows operating system.
--targetarch x86: The code is being compiled for the x86 architecture, typically used in 32-bit Windows systems.
Targeting machine-specific architectures can be done at the compiler call (eg. csc /platform:x64
or ilc --targetarch x86
) and/or the linker call (eg. link /machine:x64
)
Believe me, I really did try to have a single environment for the end-to-end build process. It was much harder than I thought, and unfortunately, I've not managed to do it (yet). However, I have managed to get everything to run inside a single x64 Native Tools Command Prompt for VS 2022
, no small feat. The build tools called, in order, are csc
and ilc
the .Net compilers; nasm
, ld
and objcopy
from within MSYS2/MINGW; and then qemu-system-i386
, installed natively on the host Windows 10 machine.
It sounds mad, but Philipp Oppermann had the same experience when developing his Rust kernel, relying on various GNU tools that also made it difficult to build on macOS and Windows (nb. a major hurdle seems to be getting a native Windows build of GRUB and grub-mkrescue, see here and here). Then Philipp decided to re-write the entire toolchain in Rust for the second edition of his blog, see Writing an OS in pure Rust, thus making it possible to build the OS natively on Windows, macOS, and Linux without any non-Rust dependendencies.
Here are the various build environments I tried, and the reasons why they were discarded:
Environment | Findings |
---|---|
Windows only |
kernel.bin linker output is 'PE32 executable (console) Intel 80386, for MS Windows', which is not supported by QEMU direct kernel boot. Hence needing objcopy from MSYS2/MINGW64 to convert it to the elf32-i386 format. |
WSL only |
ilc, the .Net AOT compiler, doesn't seem to support 32-bit x86 compilation output on Linux, which I really want. OSDev Wiki agrees, saying "If this is your first operating system project, you should do a 32-bit kernel first.", see: Bare Bones. |
MSYS2/MINGW64 |
Heaps of issues with spaces in path names, see mingw make can't handle spaces in path? as an example. Unfortunately, no amount of escaping, 8.3 shortening or quotes could get the csc compiler running from its default install location, C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\Roslyn\csc.exe |
bflat |
bflat decides to output an executable when it finds a main() function, also seemingly without the intermediate object files. This is no good, given I also need to link in the compiled boot loader. (nb. with a bit of further effort, I suspect I might be able to include the bootloader asm directly in the csproj file, somehow) |
ยฉ Frank Ray, owner of Better Software UK, author of Better Software Requirements: A handbook for software development teams and their managers.