x86-64 · UEFI · NativeAOT · CC0

Операционная система, целиком написанная на C#.

Загрузчик, ядро, сборщик мусора, драйверы, оболочка и приложения. Ядро, DOOM и оболочка написаны на чистом C#: ни одного файла .c, .h или .asm. Стоковый PowerShell 7 работает поверх этого через опциональный форк CoreCLR: это единственный компонент на C++, и он лежит в отдельном подмодуле. Всё это, включая CoreCLR, грузится и работает на реальном ноутбуке (HP 255 G10, Ryzen 3 7320U, прошивка AMI), а не только в QEMU.

COM1 — qemu-system-x86_64
BdsDxe: loading Boot0002 "UEFI QEMU QEMU USB HARDDRIVE 1-0000:00:02.0-3"
BdsDxe: starting Boot0002 "UEFI QEMU QEMU USB HARDDRIVE 1-0000:00:02.0-3"
[uefi] image protection: base=0x7CBBB000 size=0xD07000
[info] boot info ready, switching to the kernel stack
[info] on the kernel stack, installing the IDT
[info] SharpOS 0.1
[info] build: 931671e+dirty.1014-2355-pipe-dev
[info] boot: UEFI  fw: EDK II / rev 65536
[info] caps: TextOutput Shutdown MemoryMap ExternalElf KeyboardInput
[info] idt installed
[info] memory regions: 83
[info] usable pages: 509110
[info] heap init ok
[info] iface dispatch bridge installed
[info] throw-ex shellcode: bytes=150 head=0x000000007D7A6358
[info] call-catch-funclet shellcode installed
[info] gc collect hook installed (precise once the walker is up)
[info] coff-pdata: records=25800 managed=0x00C50000..0x00CF6684
[cr3] readstub iced=legacy OK len=0x4
[cr3] writestub iced=legacy OK len=0x7
[info] pager root activated (clone CR3 live)
[info] VA 0 unmapped (null-deref trap)
[info] tss: double fault on its own stack (IST1)
[info] framebuffer mapped: 1280x800 va=0x80000000 modes=30
[info] acpi madt: lapic=0xFEE00000 cpus=1 ioapics=1
[info] hpet: freq=100000000 Hz comparators=3 64bit=yes
[serial] COM1 own 16550: PRESENT (loopback OK)
[gop] 1280x800 base=0x80000000 stride=1280 fmt=1
[pci] devs=6 ahci=0:31.2 id=0x8086:0x2922 PASS
[usb] xHCI(3.x) at 0:2.0 vid=0x1B36 did=0xD mmio=0xC000000000
[ebs] ExitBootServices OK -- POST-EBS substrate LIVE
[apic] id=0 ver=0x14 timer=1563 kHz
[xhci] port 2 connected speed=4 (usb3) slot=1 functions=storage
[xhci] port 4 connected speed=3 (usb2) slot=2 functions=keyboard
[xhci] usb keyboard attached as system input
[fat] mount=Y FAT32 bps=512 spc=8 lfn:CoreLib.dll sz=16003072 PASS
=== CoreClrProbe ===
[host] GC heap limit 994 MiB of 1988 MiB usable
[host] FileOpen path="\sharpos\System.Private.CoreLib.dll"
[fsread] done sz=16003072 read_ms=38 total_ms=69
[r2r] System.Private.CoreLib: Ready to Run initialized successfully
coreclr_initialize hr=0x0
=== S_OK — CoreCLR initialized ===
[info] pe launcher start
[info] config: \config\system.env read
[info] app run start: \bin\LAUNCHER.EXE

SharpOS shell — 'help' for what exists, Tab completes, Up/Down for history, 'exit' to leave
\home $ pipegen 100 | KQL 'summarize n = count() by Level'
[kql] start ms: declared=0 bind1=0 bind2=1 first=5 total=6
Expando { Level = 0, n = 20 }
Expando { Level = 1, n = 20 }
Expando { Level = 2, n = 20 }
Expando { Level = 3, n = 20 }
Expando { Level = 4, n = 20 }
\home $ DOOM.EXE
Последний коммит · сборка 931671e · история коммитов · обсуждения
Инвариант №1

Весь исходный код на C#, даже то, что обычно пишут на ассемблере.

MSBuild и PowerShell только оркестрируют сборку. Обработчики прерываний, write barrier'ы, трамплины interface dispatch, чтение CR3: всё решается одним из трёх способов.

Ярусы 0 и 1, то есть ядро и все нативные приложения, не содержат ни строки на C, C++ или ассемблере. Единственное исключение, ярус 2: стоковому PowerShell нужны JIT и рефлексия, поэтому он работает на опциональном форке CoreCLR. Форк написан на C++ и для сборки и загрузки системы не нужен.

Насколько известно, других managed-ОС с таким инвариантом нет: Cosmos на нижнем уровне опускается до X#, собственного диалекта ассемблера, а Singularity держала загрузчик и HAL на C++ и ассемблере. Если задача кажется нерешаемой на C#, значит, она неверно поставлена.

1 · Средства языка

Сам C#

[RuntimeExport], [UnmanagedCallersOnly], delegate* unmanaged, fixed, арифметика указателей.

2 · Машинный код из C#

Ассемблер Iced: при сборке и на лету

Source generator превращает 16 стабов раннего старта в байтовые шаблоны. Когда куча поднята, Iced вызывается напрямую.

var a = new Assembler(64);
a.mov(rax, rcx);
a.Assemble(writer, rip);
3 · Символы из атрибута

COFF-объекты без единой строки на C

Если линкеру нужен символ, который ILC не эмитит, MSBuild-задача сама создаёт .obj.

[BootAsm.CoffDataSymbol(
    "__security_cookie",
    Section = ".data")]
public static ulong SecurityCookie
    = 0x2B992DDFA232UL;
Архитектура

Три яруса исполнения

От голого железа до стоковых .NET-сборок, которые грузятся байт-в-байт. Unikernel: одно адресное пространство, свой планировщик, свой GC.

Ярус 0

Kernel-AOT

Ядро, загрузка, драйверы, планировщик. Прямой доступ к CR3, PCI, MMIO, IDT, local APIC.

Где
OS/
Сборка
NativeAOT + NoStdLib + MinimalRuntime
Ярус 1

PE-приложение

Freestanding-приложения: лаунчер, оболочка, DOOM, эмуляторы NES. У каждого свой сборщик мусора и своя куча.

Где
apps_native/
Сборка
NativeAOT + NoStdLib + общий SDK
Ярус 2

CoreCLR-hosted

Стоковые .NET DLL байт-в-байт: JIT, рефлексия, полный BCL. Так работает PowerShell.

Где
\sharpos\*.dll на FAT
Сборка
опциональный форк CoreCLR (C++), слинкован с ядром
Демо

Это не hello world: PowerShell и DOOM на голом железе.

Самые требовательные тесты всего стека сразу: TPL, исключения, рефлексия, GC, FAT32, ANSI-консоль, framebuffer.

PowerShell 7.6.5 работает в SharpOS: Get-ChildItem, Tab-дополнение, переменные

PowerShell 7.6.5

Стоковый pwsh грузится с FAT32 до интерактивного приглашения и выполняет настоящие командлеты. PSReadLine работает полностью: цвета, Tab, история, Ctrl+C.

Get-ChildItem · конвейеры · [Math]::Sqrt · PSReadLine · XtermSharp

DOOM

ManagedDoom собран как PE-приложение поверх своей std. WAD читается с FAT32, картинка через GOP с увеличением 2×, PS/2-клавиатура, 35 Гц по HPET.

ManagedDoom · FAT32 · GOP · HPET · PS/2
Лаунчер SharpOS и вывод fetch.exe

Своя среда

Лаунчер на Terminal.Gui, оболочка с синтаксисом bash и историей, до 16 процессов через Process.Start, свой FAT32 с записью, USB-стек на xHCI, эмуляторы NES. Весь набор, ядро, оболочка, приложения, трубы и CoreCLR с PowerShell, тестируется на реальном ноутбуке, HP 255 G10 с Ryzen 3 7320U, 8 ГБ памяти и прошивкой AMI, и полный набор проб там проходит.

Terminal.Gui · bash-like shell · xHCI USB · FAT32 r/w · NES
Трубы

Программы передают объекты, а не байты.

PIPEGEN | PIPEFILT 3 | PIPECNT работает как в bash, но каждая стадия получает настоящий объект с полями. Объект лежит в общей памяти обмена: без парсинга и без копирования в кучу получателя.

›PIPEGEN 1000LogEntry × 1000
?PIPESPY a@ 0x7F3A2000
?PIPESPY b@ 0x7F3A2000
ƒKQL 'where Level >= 3 | summarize n = count() by Level'

Обе стадии PIPESPY показывают один и тот же адрес: один объект проходит через четыре программы без копирования.

  • Ноль выделений на сообщениеКопия, пересылка, чтение на месте. На графе из 100 объектов в 5–6 раз быстрее, чем BinaryWriter + BinaryReader.
  • Запросы прямо в конвейереKusto Query Language над потоком сообщений: where, summarize, extend, project. Запрос связывается до прихода первых данных.
  • Один атрибут, и тип ходит по трубеПометьте тип атрибутом [Message], и его можно читать, фильтровать и писать обратно без копии.
    Pipe.Read<LogEntry>()
        .Where(e => e.Level >= min)
        .WriteTo();  // без копии
  • JSON, JSON Lines, CBORCONVERT работает потоково, в памяти одно сообщение: файл на 100 МиБ проходит через кучу в 64 МиБ.
Статус

Что уже работает

Каждая отметка «работает» подтверждена прогоном проб в ядре. Полная матрица с комментариями лежит в README.

ВозможностьKernel-AOTPE-прил.CoreCLR
Управляемая куча, GC с точным сканированием стекаработаетработаетработает
try / catch / finally / фильтры whenработаетработаетработает
async / await, yield return, лямбдыработаетработаетработает
Коллекции BCL, LINQработаетработаетработает
Task.Run, lock, многопоточные процессыработаетработаетработает
Math с правильным округлением (SharpLibm, CORE-MATH)работаетработаетчастично
dynamicпока нетчастичноработает
Рефлексияпока нетпока нетработает
USB (xHCI): клавиатура, флешки, аппаратный ключ U2Fчастичночастичнонеприменимо
Вытесняющая многозадачностьчастичночастичночастично
SMPзапланированозапланированозапланировано
Сетьпока нетпока нетпока нет
работает частично запланировано пока нет неприменимо
Быстрый старт

Соберите сами

Собирается обычным dotnet publish -r win-x64. Зависимости ставит mise или nix, образ запускается в QEMU.

  1. Поставьте mise или nix. Все версии инструментов закреплены в mise.toml и flake.lock.
  2. Клонируйте с подмодулями. Форк CoreCLR необязателен, без него нужен флаг -SkipCoreClr.
  3. Соберите и запустите. run_build.ps1 собирает ядро, делает образ и запускает QEMU.
  4. Положите payloads. DOOM1.WAD, картриджи .nes и PowerShell кладутся в payloads/.
# macOS / Linux / Windows
# или на Windows: winget install jdx.mise
brew install mise

git clone --recurse-submodules \
  https://github.com/daniilvaino/SharpOS.git
cd SharpOS
mise trust && mise bootstrap
mise exec -- pwsh

# дальше в PowerShell
./build.ps1 launcher
$env:SHARPOS_GUI = 1
./run_build.ps1 -UsbOnly -SkipCoreClr
# NixOS или любой Linux с включёнными флейками
git clone --recurse-submodules \
  https://github.com/daniilvaino/SharpOS.git
cd SharpOS
nix develop --command pwsh

# дальше в PowerShell
./build.ps1 launcher
$env:SHARPOS_GUI = 1
./run_build.ps1 -UsbOnly -SkipCoreClr
Благодарности

Библиотеки и вдохновение

Чужой код, который попадает в образ, и проекты, на которых SharpOS учился.

  • zerosharpстарт
    UEFI hello world на NativeAOT, отсюда всё началось.
  • dotnet/runtimeMIT
    Toolchain NativeAOT, форк CoreCLR, сотни портов BCL.
  • IcedMIT
    Кодировщик x86-64: весь ассемблер проекта.
  • ManagedDoomGPL-2.0
    DOOM на C#, изолирован в отдельном приложении.
  • Terminal.GuiMIT
    Текстовый интерфейс, на нём написан лаунчер.
  • XtermSharpMIT
    Движок эмулятора терминала: ANSI/VT, прокрутка.
  • Kusto.LanguageApache-2.0
    Разбор и связывание KQL для запросов в трубах.
  • Cosmosидея
    Сама идея managed-операционной системы. Её нижний уровень написан на X#, диалекте ассемблера, поэтому инвариант «всё на C#» она не держит.

C# от первой инструкции до DOOM.

SharpOS в общественном достоянии по лицензии CC0. Используйте, изменяйте, присылайте pull request'ы.