C++, Rust en de Windows Kernel: waar veiligheid helpt en grenzen nog steeds bijten

C++, Rust en de Windows Kernel: waar veiligheid helpt en grenzen nog steeds bijten

C++, Rust en de Windows Kernel: waar veiligheid helpt en grenzen nog steeds bijten

Invoering

De Windows-kernel is waar zuivere whiteboard-overtuigingen naartoe gaan om te ontdekken dat ze schulden aan de realiteit verschuldigd zijn. Bij gewoon applicatiewerk kan een team zich soms een vage verklaring veroorloven waarom iets kapot ging. Bij kernelwerk hebben vage verklaringen de neiging om te veranderen in bugcontroles, blauwe schermen, boze operators en foutopsporingssessies die je het gevoel geven dat de machine persoonlijk teleurgesteld is in je opvoeding.

Dat is de reden waarom de C++ en Rust gesprekken rond de Windows kernel ertoe doen. Niet omdat de ene kant nostalgisch is en de andere verlicht, maar omdat laag Windows-werk elke claim dwingt om contact met IOCTL-grenzen, IRQL-regels, DMA-aannames, synchronisatie, levenslange discipline en tooling te overleven die nog steeds van je verwacht dat je je als een volwassene gedraagt, zelfs als je architectuurdeck dat niet deed.

Rust verdient hier zijn momentum. Geheugenveiligheid is niet nep. Duidelijker eigendom is niet nep. Meer expliciete faaloppervlakken zijn niet nep. Als je systeemcode schrijft die bijna bevoorrecht is en de taal een hele categorie gemakkelijk te maken bugs kan verwijderen, is dat niet cosmetisch. Dat is een serieus technisch voordeel en een voordeel dat C- en C++-teams historisch gezien moesten herscheppen met discipline, evaluatie en een zekere mate van milde paranoia.

Maar de Windows-kernel deelt geen prijzen uit voor goede bedoelingen. Het beloont teams die kunnen opereren binnen het ecosysteem dat daadwerkelijk bestaat: WDK-beperkingen, op C gebaseerde interfaces, oudere stuurprogramma's, bestaande codebases, WinDbg-workflows, regels voor de levensduur van kernelobjecten, DMA en synchronisatie-realiteiten, en de pijnlijk belangrijke vraag of de hele debugging- en uitrolketen begrijpelijk blijft als er iets mislukt in de productie.

De nuttige vraag is dus niet "Rust of C++?" De nuttige vraag is deze: waar creëert Rust een echt voordeel, waar blijft C++ de praktische standaard, en hoe ontwerp je de grens zodat het systeem veiliger wordt in plaats van alleen maar zelfgenoegzaam?

Waarom de Windows Kernel geen generieke systeemspeeltuin is

Mensen praten vaak over systeemprogrammering alsof elk domein op een laag niveau één emotioneel klimaat deelt. Dat is niet waar. De Windows-kernel is niet zomaar "een code op laag niveau". Het is een werkomgeving met strikte contracten en zeer dure manieren om te ontdekken dat je ze verkeerd hebt begrepen.

IRQL bestaat. Er bestaan ​​verzendpaden. Er bestaan ​​beperkingen voor pagina's. Er bestaan ​​apparaatstacks. Er bestaan ​​IOCTL-contracten. Synchronisatiefouten blijven niet lang theoretisch. Slechte opschoonlogica zorgt niet alleen voor een rommelige procesuitgang. Het kan de status beschadigen, een apparaatpad blokkeren of een machine laten crashen die iemand anders liever had willen laten functioneren.

Dit betekent dat de kernel twee tegengestelde illusies bestraft. De eerste illusie is dat al het werk op laag niveau voor altijd in C of C++ moet blijven, omdat de wereld altijd zo is aangesloten. De tweede illusie is dat het gebruik van Rust het werk automatisch omzet in een morele overwinning. Beide zijn luie manieren om het echte ontwerpprobleem te omzeilen.

Het echte probleem is om het systeem zo vorm te geven dat de gevaarlijkste grens klein, meetbaar, debugbaar en duidelijk beheersbaar is. Soms betekent dit dat C++ in de praktijk nog steeds het beste past, omdat het coureurmodel, de bestaande code, de tooling en de teamervaring allemaal daar naartoe wijzen. Soms betekent het dat een Rust-component het risico daadwerkelijk verlaagt en de duidelijkheid vergroot. Meestal betekent dit dat de antwoorden gemengd zijn, en dat alleen volwassenen zich op hun gemak voelen met gemengde antwoorden.

Waar Rust daadwerkelijk helpt bij Windows-werk op laag niveau

Rust helpt het meest als het verwarring uit code verwijdert die niet het recht heeft verwarrend te zijn. Het ontleden van grenzen, staatsmachine-hygiëne, expliciet eigenaarschap, duidelijkere opruimingspatronen en een strakkere discipline rond wat een alias kan zijn of kan overleven wat allemaal betekenisvolle overwinningen zijn. In kernel-aangrenzende of driver-zware systemen is dat van belang, omdat bugs op een laag niveau zelden poëtisch zijn. Ze zijn repetitief, structureel vertrouwd en vernederend op een manier waarop technische teams decennia lang hebben gedaan alsof ze deel uitmaakten van de romance.

Rust is vooral aantrekkelijk in begrensde componenten waarbij de interface expliciet kan worden gehouden. Hulpprogrammalagen, helpermodules, goed gedefinieerde parsers, enkele gebruikersmodus-companions voor stuurprogramma's, interne tools en zorgvuldig geïsoleerde stukjes kernel- of stuurprogrammalogica kunnen profiteren van de beperkingen van de taal als het omringende technische verhaal volwassen genoeg is om deze te ondersteunen.

Het helpt ook cultureel. Teams die Rust in een Windows-systeemomgeving brengen, krijgen vaak gezondere gesprekken over levens, aliasing, opschoning en wat een grens precies belooft. Dat is zelfs handig als de uiteindelijke architectuur hybride blijft. Talen vormen discussies, en soms is een betere discussie al materiële vooruitgang.

Maar niets van dit alles betekent dat de Windows-kernel nu een speeltuin is waar teams door theologisch enthousiasme moeten herschrijven. Een begrensde overwinning is nog steeds een begrensde overwinning. Dat onderscheid zorgt ervoor dat serieuze techniek voorkomt dat het een heel duur lifestylemerk wordt.

Waar C++ nog steeds een echte basis houdt

C++ blijft sterk in Windows kernel- en driverwerk om redenen die hardnekkig praktisch zijn. Er is een enorme hoeveelheid bestaande stuurprogrammacode, voorbeelden, patronen, foutopsporingskennis en leveranciersintegratiegeschiedenis opgebouwd rond C en C++. Teams die in deze ruimte werken, beginnen zelden vanaf een leeg terrein. Ze erven stuurprogramma's, filterketens, apparaatcontracten, gebruikersmodusclients, oudere C++, aannames en operationele gewoonten die al C++ zijn gevormd, zelfs als de code half C is en emotioneel voor 100 procent schulden is.

Het gereedschapsverhaal is ook belangrijk. WinDbg, WDK-samples, KMDF- en WDM-gewoonten, driver-verifier-workflows, symboolinterpretatie, crash-dump-onderzoek en de bredere foutopsporingscultuur rond Windows kernelwerk hebben allemaal nog steeds diepe wortels in de bestaande oorspronkelijke wereld. Wanneer een team onder druk staat, is de volwassenheid van de diagnose geen decoratief voordeel. Zo voorkomt het werk dat het een seizoen van archeologie in het openbaar wordt.

Er is ook het integratieprobleem. Stuurprogramma's leven vaak naast oude code, helpers in de gebruikersmodus, bestaande installatielogica, leverancier SDKs of beveiligingstools die al gebonden zijn aan C- en C++-aannames. C++ is in abstracto niet automatisch beter. Het is vaak beter in de onmiddellijke omgeving, omdat het omringende systeem al is getraind om het uit te spreken.

Dat maakt Rust niet ongeldig. Het betekent eenvoudigweg dat de bewijslast verandert afhankelijk van waar het onderdeel zich bevindt. Een nieuwe geïsoleerde module is één argument. Een andere drijfveerstapel die door jaren van inheemse aannames is geregen, is een ander voorbeeld. Serieuze teams stoppen met te doen alsof dit dezelfde situatie is.

Het onveilige oppervlak verdwijnt niet. Het beweegt.

Dit is het belangrijkste punt in het hele gesprek. Onveilig gedrag verdwijnt niet omdat een deel van de codebase is geschreven in Rust. Het verhuist. Het verzamelt zich aan FFI randen, buffergrenzen, synchronisatienaden, toewijzingspaden, apparaatcontracten en plaatsen waar het bedieningsmodel nog steeds wordt gedefinieerd door Windows zelf in plaats van door de finesses van een enkele taal.

Dat is de reden waarom teams een slechte ruil kunnen maken als ze de taal te vroeg en de grens te laat vieren. Een Rust-module die slordig overgaat in de oude stuurprogrammacode kan nog steeds de oude chaos overnemen, plus een nieuwe integratiebelasting. Een C++-driver die het gevaarlijke oppervlak isoleert, de invarianten ervan documenteert, de IOCTL-semantiek saai houdt en diep testbaar blijft, zal wellicht minder totale verrassingen opleveren dan een meer modieuze architectuur die de grens verlegt en tegelijkertijd de deugd ervan zeer zelfverzekerd vertelt.

De ontwerpvraag voor volwassenen is daarom kleiner en scherper. Welke module kan worden geïsoleerd? Welke interface kan stabiel blijven? Welke eigendomsregels kunnen zo duidelijk worden geformuleerd dat taalverschillen geen runtime-verwarring veroorzaken? Welk debugging-pad zal nog werken als het systeem al in brand staat en niemand zin heeft in filosofische nuance?

Deze vragen zijn niet anti-Rust. Ze zijn vóór overleving.

Hoe goed eruit ziet

Goede Windows-kernel-engineering klinkt niet heroïsch. Het klinkt rustig.

Het risicovolle pad is bekend. Het IOCTL-contract is expliciet. Het gelijktijdigheidsverhaal is op de beste manier saai. De eigendomsaannames zijn gedocumenteerd. Crash-dump-analyse is mogelijk. Het uitrolplan is geen durf. De grens van de bestuurder is zo smal dat iemand buiten het oorspronkelijke implementatieteam nog steeds kan begrijpen wat veilig is om te veranderen en wat veilig is om met rust te laten.

Als Rust wordt gebruikt, moet het duidelijk zijn waarom. Het zou er niet moeten zijn omdat "toekomst" in een sterk lettertype op een dia is geschreven. Het zou er moeten zijn omdat een gedefinieerde component echt profiteert van de beperkingen van de taal en omdat het team het debugging-, bouw- en operationele verhaal dat uit die keuze volgt, kan ondersteunen.

Als C++ op het kritieke pad blijft, mag dat niet als lot worden verdedigd. Het moet worden verdedigd met bewijsmateriaal: volwassenheid van de tooling, integratiekosten, beperkingen van de bestuurder, teamervaring en een afgemeten beeld van waar de instabiliteit feitelijk vandaan zou komen als het onderdeel zou worden verplaatst. C++ zou in het ontwerp moeten zitten omdat het de stoel verdiende, niet omdat het systeem te moe was om ruzie te maken.

Praktische gevallen die de moeite waard zijn om eerst op te lossen

IOCTL-grensopruiming

Veel systemen met veel bestuurders lopen minder gevaar door hun slimste code dan door slordige contractgrenzen. Het opschonen van IOCTL-afhandeling, validatie, structuurversiebeheer en aannames van gebruiker naar kernel levert vaak sneller veiliger resultaten op dan ambitieuze herschrijvingen.

Smalle driver-kernverharding

Een kleine drijfveerkern met expliciete invarianten en een betere eigendomsdiscipline is doorgaans meer waard dan een enorme theoretische migratie. Soms gebeurt die verharding in C++. Soms maakt het een toekomstige Rust-component mogelijk. Hoe dan ook, de uitbetaling is reëel.

Begeleiders en tools in de gebruikersmodus

Dit is waar Rust vaak schittert zonder drama. Diagnostische hulpmiddelen, replay-hulpprogramma's, configuratievalidators, capture-analyzers of gecontroleerde helperprocessen kunnen duidelijker en veiliger worden zonder het meest broze kernelpad naar een nieuwe integratiereligie te slepen voordat het systeem gereed is.

Praktijklab: decodeer een Windows IOCTL op een saaie manier

De Windows-kernel straft teams die besturingscodes behandelen als decoratieve gehele getallen. Laten we een klein hulpprogramma bouwen dat een IOCTL-waarde decodeert, zodat de grens niet langer mysterieus is.

main.cpp

#include <cstdint>
#include <iomanip>
#include <iostream>

struct IoctlParts {
    std::uint32_t device_type;
    std::uint32_t access;
    std::uint32_t function;
    std::uint32_t method;
};

IoctlParts decode_ioctl(std::uint32_t code) {
    return IoctlParts{
        (code >> 16) & 0xFFFFu,
        (code >> 14) & 0x3u,
        (code >> 2) & 0x0FFFu,
        code & 0x3u
    };
}

int main() {
    constexpr std::uint32_t ioctl = 0x222004;
    const auto parts = decode_ioctl(ioctl);

    std::cout << "IOCTL 0x" << std::hex << std::uppercase << ioctl << "\n";
    std::cout << "device_type=0x" << parts.device_type << "\n";
    std::cout << "access=0x" << parts.access << "\n";
    std::cout << "function=0x" << parts.function << "\n";
    std::cout << "method=0x" << parts.method << "\n";
}

Bouwen

Op Windows met MSVC:

cl /O2 /std:c++20 main.cpp
.\main.exe

Op Linux of macOS met een platformonafhankelijke compiler:

g++ -O2 -std=c++20 -o ioctl_decode main.cpp
./ioctl_decode

Wat dit je leert

Het punt is niet de rekenkunde. Het punt is dat werken op laag niveau Windows gemakkelijker wordt zodra verborgen structuren niet langer als magie worden behandeld. Decodeer de grens, geef de velden een naam, maak het contract zichtbaar, en plotseling wordt het debuggesprek korter en minder religieus.

Testtaken voor liefhebbers

  1. Maak dezelfde decoder opnieuw in Rust en vergelijk niet alleen de codelengte, maar ook de helderheid van de grens die u zou blootstellen aan de rest van een driver-toolchain.
  2. Breid de decoder uit om voor mensen leesbare namen af ​​te drukken voor METHOD_BUFFERED, METHOD_IN_DIRECT en gerelateerde waarden.
  3. Voeg een parser toe voor een lijst met IOCTL-codes uit een tekstbestand en sorteer ze op apparaattype en functie.
  4. Bouw een kleine fuzz-invoerset met willekeurige IOCTL-waarden en controleer of uw decoder stabiel en saai blijft.
  5. Voeg daar een opzettelijk slordige aanname aan toe en onderzoek vervolgens hoe snel een ‘onschadelijke’ sluiproute het hele instrument in een leugenaar verandert.

Samenvatting

Rust is een echte verbetering in Windows low-level engineering wanneer het wordt gebruikt om verwarring te beperken, eigendom te verduidelijken en bepaalde categorieën van vermijdbare bugs te verkleinen. C++ blijft een echte en vaak gerechtvaardigde standaard wanneer het werk gekoppeld is aan bestaande drivers, bestaande tooling, bestaande debugging-cultuur en operationele paden die nog steeds in een sterk eigen ecosysteem leven.

De echte taak is niet om een ​​morele winnaar te kiezen. De echte taak is om grenzen te ontwerpen die begrijpelijk blijven als het systeem al onder druk staat. Bij kernelwerk is dat het verschil tussen techniek en optimisme.

Referenties

  1. Documentatie over Windows stuurprogramma's: Windows
  2. I/O-besturingscodes definiëren: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes
  3. Hardwareprioriteiten en IRQL beheren: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities
  4. Overzicht van de ontwikkeling van WDF-drivers: https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/
  5. Windows documentatie voor foutopsporingstools: Windows
  6. Onveilig Rust: Rust
Philip P.

Philip P. – CTO

Terug naar Blogs

Contact

Begin het gesprek

Een paar duidelijke lijnen zijn voldoende. Beschrijf het systeem, de druk en de beslissing die wordt geblokkeerd. Of schrijf rechtstreeks naar midgard@stofu.io.

01 Wat het systeem doet
02 Wat doet het nu pijn
03 Welk besluit is geblokkeerd
04 Optioneel: logs, specificaties, sporen, diffs
0 / 10000
Geen bestand gekozen