Ett av de mest intressanta och vanligaste koncepten i x86-arkitekturen är Protected mode och dess stöd för fyra lägen (även kallade ringar):

Det var en utmanande idé att förstå och jag ska försöka förklara den så tydligt som möjligt i detta inlägg. Vi kommer att täcka följande begrepp:

  • GDT, LDT, IDT.
  • Virtuellt minnesöversättning.
  • ASLR och Kernel ASLR (KASLR).

Låt oss börja med grunderna, alla datorer har åtminstone (förhoppningsvis) följande komponenter: CPU, disk och RAM-minne. Var och en av dessa komponenter har en nyckelroll i systemets flöde. Processorn utför kommandon och operationer på minnet (RAM), RAM innehåller de data vi använder och möjliggör snabb och tillförlitlig åtkomst till dem, disken innehåller beständiga data som vi behöver för att existera även efter omstart eller avstängning. Jag utgår från detta eftersom även om detta är det allra mest grundläggande är det viktigt att ha detta i åtanke och när du läser den här artikeln frågar du dig själv vilken komponent vi pratar om i det ögonblicket.

Odriftssystemet är den mjukvara som styr allt, och också den som möjliggör ett snabbt, bekvämt, konsekvent och effektivt gränssnitt för att få tillgång till alla dess funktioner – varav en del är tillgång till hårdvaran och andra är för att öka bekvämligheten och prestandan.

Likt all bra mjukvara arbetar operativsystemet i lager, och kärnan är det första lagret och – enligt min åsikt – det viktigaste. För att vi ska förstå kärnans betydelse måste vi först förstå dess handlingar och de utmaningar den står inför, så låt oss titta på några av dess ansvarsområden:

  • Hantera systemanrop (samma gränssnitt som vi talade om).
  • Allokera resurser (RAM, CPU och mycket mer) till de processer/trådar som är i arbete.
  • Säkra de operationer som utförs.
  • Intermedierar mellan hårdvara och mjukvara.

Många av dessa åtgärder utförs med processorns generösa hjälp, när det gäller x86 är skyddat läge det läge som gör det möjligt för oss att begränsa kraften (instruktionsuppsättning) i den för närvarande körda exekveringskontexten.

Låt oss anta att vi har två världar – användarens värld och övervakarens värld. Vid varje given tidpunkt kan du bara befinna dig i en av dessa världar. När du är i användarens värld ser du världen så som handledaren vill att du ska se den. Låt oss se vad jag menar med det:

Vad sägs om att du är en process. En process är en behållare för en eller flera trådar. En tråd är en exekveringskontext, det är den logiska enhet i vilken maskininstruktionerna exekveras. Det betyder att när tråden utför, låt oss säga läser från minnesadressen 0x8080808080, hänvisar den faktiskt till den virtuella adressen 0x808080808080 för den aktuella processen. Som du kan gissa kommer adressens innehåll att vara annorlunda mellan två processer. Det virtuella adressutrymmet finns på processnivå, vilket innebär att alla trådar i samma process har samma adressutrymme och kan komma åt samma virtuella minne. För att ge ett exempel på en resurs som finns på trådnivå kan vi använda den berömda stacken.

Så jag har en tråd som utför följande kod:

Vår tråd utför huvudfunktionen som kommer att anropa vår funktion ”func”. Låt oss säga att vi bryter tråden vid rad 9. Stacken kommer att se ut på följande sätt:

  1. variabel_a.
  2. parameter.
  3. returadress – adress till rad 20.
  4. variabel_b.

För att illustrera:

I den givna koden skapar vi tre trådar för vår process och var och en av dem skriver ut sitt id, sitt stacksegment och sin stackpointer.

En möjlig utgång av det programmet är:

Som du kan se hade alla trådar samma stacksegment eftersom de har samma virtuella adressrum. Stackpekaren för var och en av dem är annorlunda eftersom var och en har sin egen stack att lagra sina värden i.

Notis om stacksegmentet – jag kommer att förklara mer om segmentregister i GDT/LDT-avsnittet – men för tillfället kan du ta mitt ord för det.

Varför är detta viktigt? Processorn kan när som helst frysa tråden och ge kontrollen till vilken annan tråd som helst. Som en del av kärnan är det schemaläggaren som tilldelar processorn till de för närvarande existerande (och ”färdiga”) trådarna. För att trådarna skall kunna köras på ett tillförlitligt och effektivt sätt är det viktigt att varje tråd har en egen stapel som den kan spara sina relevanta värden i (t.ex. lokala variabler och returadresser).

För att hantera sina trådar har operativsystemet en särskild struktur för varje tråd som kallas TCB (Thread Control Block), i den strukturen sparas – bland annat – sammanhanget för tråden och dess tillstånd (körd / klar / osv…). Kontexten innehåller – återigen – bland annat CPU-registervärden:

  • EBP -> Stackens basadress, varje funktion använder denna adress som basadress från vilken den förskjuts för att få tillgång till lokala variabler och parametrar.
  • ESP -> Den aktuella pekaren till det sista värdet (första att poppa upp) på stacken.
  • Register för allmänna ändamål -> EAX, EBX, etc….
  • Flags-register.
  • C3 -> innehåller platsen för sidkatalogen (kommer att diskuteras senare).
  • EIP – Nästa instruktion som ska exekveras.

Bortsett från trådar måste operativsystemet hålla reda på efter en hel del andra saker, inklusive processer. För processer sparar operativsystemet PCB-strukturen (Process Control Block), vi har sagt att det för varje process finns ett isolerat adressutrymme. Låt oss nu anta att det finns en tabell som mappar varje virtuell adress till en fysisk adress och att den tabellen är sparad i PCB:n. Operativsystemet ansvarar för att uppdatera tabellen och hålla den uppdaterad till det fysiska minnets korrekta tillstånd. Varje gång schemaläggaren byter ut utförandet till en viss tråd tillämpas den tabell som sparats för den trådens ägande process på processorn så att den kan översätta de virtuella adresserna korrekt.

Det räcker med begreppen, låt oss förstå hur det faktiskt går till. För att göra det ska vi titta på världen från processorns perspektiv:

Global Descriptor Table

Vi vet alla att processorn har register som hjälper honom att göra beräkningar, vissa register mer än andra (;)). Genom designen stödjer x86 flera lägen men de viktigaste är användare och övervakad, processorn har ett speciellt register som kallas gdtr (Global Descriptor Table Register) som innehåller adressen till en mycket viktig tabell. Denna tabell mappar varje virtuell adress till motsvarande processors läge, den innehåller också behörigheterna för den adressen (READ | WRITE | EXECUTE). Som en del av processorns exekvering kontrollerar den vilken instruktion som ska exekveras härnäst (och vilken adress den befinner sig på), den kontrollerar den adressen mot GDT och på så sätt vet den om det är en giltig instruktion utifrån dess önskade läge (matcha processorns aktuella läge med läget i GDT) och behörigheter (om den inte kan exekveras – ogiltig). Ett exempel är instruktionen ”lgdtr” som läser in ett värde i gdtr-registret, och den kan endast utföras i övervakat läge enligt vad som anges. Den viktigaste punkten att betona här är att allt skydd över minnesoperationer (exekvering av instruktioner/skrivning till ogiltig plats/läsning från ogiltig plats) utförs av GDT och LDT (kommer härnäst) på processorns nivå med hjälp av dessa strukturer som byggts upp av operativsystemet.

Detta är hur innehållet i en post i GDT/LDT ser ut:

http://wiki.osdev.org/Global_Descriptor_Table

Som du kan se har den ett intervall av adresser som posten är relevant för, och dess attribut (behörigheter) som du förväntar dig.

Local Descriptor Table

Allt vi sagt om GDT gäller även för LDT med liten (men stor) skillnad. Som namnet antyder tillämpas GDT globalt på systemet medan LDT tillämpas lokalt. Vad menar jag med globalt och lokalt? GDT håller koll på behörigheterna för alla processer, för varje tråd, och den ändras inte mellan kontextbyten, vilket LDT däremot gör. Om varje process har sitt eget adressutrymme är det bara logiskt att det är möjligt att adressen 0x10000000 för en process är körbar och för en annan endast läs/skrivbar. Detta gäller särskilt om ASLR är aktiverad (kommer att diskuteras senare). LDT är ansvarig för att hålla de behörigheter som skiljer varje process åt.

En sak att notera är att allt som sagts är syftet med strukturen, men i verkligheten kan det hända att vissa operativsystem använder vissa delar av strukturen eller inte alls, t.ex. är det möjligt att bara använda GDT och ändra den mellan kontextbyten och aldrig använda LDT. Allt detta är en del av utformningen av operativsystemet och avvägningar. Posterna i den tabellen liknar posterna i GDT.

Selectors

Hur vet processorn var den ska leta i GDT eller LDT när den utför en viss instruktion? Processorn har särskilda register som kallas segmentregister:

https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture

- Stack Segment (SS). Pointer to the stack. - Code Segment (CS). Pointer to the code. - Data Segment (DS). Pointer to the data. - Extra Segment (ES). Pointer to extra data ('E' stands for 'Extra'). - F Segment (FS). Pointer to more extra data ('F' comes after 'E'). - G Segment (GS). Pointer to still more extra data ('G' comes after 'F').

Varje register är 16 bitar långt och har följande struktur:

http://www.c-jump.com/CIS77/ASM/Memory/M77_0290_segment_registers_protected.htm

Så vi har indexet till GDT/LDT, vi har också biten som säger om det är LDT eller GDT och vilket läge det ska vara (RPL 0 är supervisor, 4 är user).

Interrupt Descriptor Table

När GDT och LDT har vi också IDT (Interrupt Descriptor Table), IDT är helt enkelt en tabell som innehåller adresserna till en mycket viktig funktion, en del av dem tillhör operativsystemet, andra tillhör drivrutiner och fysiska enheter som är anslutna till datorn. Liksom gdtr har vi idtr som, som du antagligen gissat, är ett register som innehåller IDT-adressen. Vad är det som gör IDT så speciell? När vi initierar ett avbrott växlar CPU:n automatiskt till övervakat läge, vilket innebär att varje funktion inuti IDT körs i övervakat läge. Varje tråd från varje läge kan utlösa ett avbrott genom att utfärda ”int”-instruktionen följt av ett nummer som talar om för CPU:n vilket index målfunktionen befinner sig på. Med detta sagt är det nu uppenbart att varje funktion inom IDT är en potentiell inkörsport till övervakat läge.

Så vi vet att vi har GDT/LDT som talar om för CPU:n vilka behörigheter som gäller för varje virtuell adress och vi har IDT som pekar ut ”inkörsport”-funktionerna till vår älskade kärna (som uppenbarligen befinner sig inom den övervakade delen av minnet). Hur beter sig dessa strukturer i ett fungerande system?

Virtuellt minne

För att förstå hur allt detta samspelar måste vi ta upp ytterligare ett begrepp – virtuellt minne. Minns du när jag sa att det finns en tabell som mappar varje virtuell minnesadress till dess fysiska adress? Det är faktiskt lite mer komplicerat än så. För det första kan vi inte helt enkelt mappa varje virtuell adress eftersom det kommer att ta mer utrymme än vad vi faktiskt har, och om vi lägger behovet av att vara effektiva åt sidan kan operativsystemet också byta minnessidor till disken (för effektivitet och prestanda), det är möjligt att minnessidan för den virtuella adress som behövs inte finns i minnet för tillfället, så förutom att översätta den virtuella adressen till den fysiska måste vi också spara om minnet finns i RAM och om inte, var finns det (det kan finnas mer än en sidofil). MMU (Memory Management Unit) är den komponent som ansvarar för att översätta det virtuella minnet till ett fysiskt.

En sak som är mycket viktig att förstå är att varje instruktion i varje läge går igenom processen med översättning av den virtuella adressen, även kod i övervakat läge. När CPU:n väl befinner sig i skyddat läge använder varje instruktion som den utför virtuell adress – aldrig fysisk (det finns vissa knep som gör att den faktiska virtuella adressen alltid översätts till exakt samma virtuella minne, men det ligger utanför det här inläggets räckvidd).

Så när CPU:n väl befinner sig i skyddat läge, hur vet CPU:n då var den ska leta när den behöver översätta den virtuella adressen? svaret är CR3-registret, i detta register finns adressen till strukturen som innehåller den information som krävs – sidkatalogen. Dess värde ändras med den pågående processen (återigen, olika virtuella adressrum).

Hur ser denna Page Directory ut? När det gäller effektivitet måste vi kunna söka i denna ”tabell” så snabbt som möjligt, och den måste också vara så liten som möjligt eftersom denna tabell kommer att skapas för varje process. Lösningen på detta problem är inget mindre än lysande. Den bästa bild jag kunde hitta för att illustrera översättningsprocessen är den här (från wikipedia):

MMU:n har två ingångar, den virtuella adress som ska översättas och CR3 (adress till den aktuella sidkatalogen). I x86-specifikationen hackas den virtuella adressen i tre delar:

  • 10 bitars nummer – index till sidkatalogen.
  • 10 bitars nummer – index till sidtabellen.
  • 12 bitars nummer – offset till själva den fysiska adressen.

Processorn tar alltså det första 10-bitarsnumret och använder det som index till sidkatalogen, för varje post i sidkatalogen har vi sidtabellen, som sedan processorn använder nästa 10-bitarsnummer som index. Varje post i katalogtabellen pekar på en 4K-gränssida i minnet och den sista 12-bitars förskjutningen från den virtuella adressen används för att peka ut den exakta platsen i det fysiska minnet. Det briljanta i den lösningen är:

  • Flexibiliteten i att varje virtuell adress kan lokaliseras till en helt orelaterad fysisk adress.
  • Den effektiva användningen av utrymme för de inblandade strukturerna är fantastisk.
  • Inte varje post i varje tabell används, utan endast de virtuella adresser som faktiskt används och mappas av processen finns med i tabellerna.

Jag är verkligen ledsen för att jag inte förklarar denna process mer detaljerat, detta är en väldokumenterad process som många människor arbetat hårt för att förklara bättre än vad jag någonsin skulle kunna göra – googla det.

Kernel vs User

Det är här det blir intressant (och magiskt om jag får lov att säga det).

Vi inledde den här artikeln med att konstatera att operativsystemet är att iscensätta allt, det gör det genom att använda kärnan. Som redan nämnts körs kärnan i en minnessektion som är mappad som supervised mode only i GDT för alla processer. Ja, jag vet att varje process har sitt eget adressutrymme, men kärnan skär av det adressutrymmet (vanligtvis den övre halvan, beroende på operativsystemet) för eget bruk, inte bara för att skära av adressutrymmet utan också på samma adress för alla processer. Detta är viktigt eftersom kärnans kod är fast och alla referenser till variabler och strukturer måste finnas på samma plats för alla processer. Man kan se på kärnan som ett särskilt bibliotek som laddas till varje process på exakt samma plats.

Djupare in i avbrott

Vi vet att IDT innehåller adresser till funktioner, dessa funktioner kallas ISR (Interrupt Service Routine), en del av dem utförs när en hårdvaruhändelse inträffar (tangenttryckning på tangentbordet) och andra när programvaran initierar avbrottet, t.ex. för att växla till kärnans läge.

Windows har ett häftigt koncept om avbrott och prioritering av dem: Ett särskilt viktigt avbrott är klockans tickande. Varje gång klockan tickar finns det ett avbrott som hanteras av dess ISR. Operativsystemets schemaläggare använder denna klockhändelse för att kontrollera hur mycket tid varje process körs och om det är en annan process tur eller inte. Som du kan förstå är detta avbrott superviktigt och måste betjänas så snart det inträffar, men alla ISR:er har inte samma betydelse och det är här som prioriteringarna mellan avbrotten kommer in. Låt oss ta tangenttryckning på tangentbordet som exempel och anta att den har prioriteten 1. Jag har just tryckt på en tangent på tangentbordet och dess ISR körs, medan tangentbordets ISR körs ignoreras alla avbrott med samma prioritet och lägre. Medan ISR utförs utlöses klockans ISR med prioritet 2 (vilket är anledningen till att den inte är inaktiverad), en omedelbar omkoppling sker till klockans ISR, när klockan är klar återgår kontrollen till tangentbordets ISR från den plats där den stannade. Dessa avbrottsprioriteringar kallas IRQLs (Interrupt ReQuest Level), när avbrottets IRQL stiger är dess prioritet högre. De avbrott som har högst prioritet är aldrig avbrott i mitten, de körs till slutet, alltid. IRQLs är Windows-specifikt – IRQL är ett nummer mellan 0-31, för Linux finns det däremot inte, Linux hanterar alla avbrott med samma prioritet och stänger helt enkelt av alla avbrott när det verkligen behövs för att den specifika rutinen inte ska störas. Som du kan se är allt en fråga om design och preferenser.

Låt oss koppla allt detta till vårt älskade användarläge . ISR för den klockhändelsen kommer att exekvera oavsett vilken tråd som för närvarande körs och kan till och med avbryta till en annan ISR för orelaterad uppgift. detta är ett perfekt exempel på varför kärnan är på samma adress för alla processer vi vill inte ändra GDT och Page Directory (i C3) varje gång vi exekverar avbrott eftersom det händer MÅNGA gånger under ens en enda funktion i en given process i användarläge. Det händer mycket mellan de rader av kod som du skriver när du utvecklar ditt användarlägesprogram (;)).

Ett annat sätt att se på avbrott är som externa och oberoende ingångar till vårt operativsystem, denna definition är inte korrekt (inte alla avbrott är externa eller oberoende), men den är bra för att göra en poäng, en stor del av kärnans uppgift är att förstå de händelser som inträffar hela tiden från alla platser (inmatningsenheter) och att från den ena sidan tjäna dessa händelser och från den andra se till att allting korreleras på rätt sätt.

För att göra allt detta begripligt börjar vi med ett enkelt program i användarläge som utför följande instruktion:

0x0000051d push ebp;

För varje instruktion som CPU:n utför undersöker den först instruktionsadressen (i det fallet ”0x0000051d”) mot GDT/LDT med hjälp av kodsegmentregistret (”cs” eftersom det är instruktionen som skall exekveras) för att få reda på vilket index den skall leta efter i tabellen (kom ihåg att segmentregistret talar om för CPU:n exakt var den skall leta). När CPU:n vet att instruktionen befinner sig på en körbar plats och att vi befinner oss i rätt ring (användarläge/kernelläge) fortsätter den att utföra instruktionen. I det här fallet påverkar instruktionen ”push ebp” inte bara registret utan även programmets stapel (den pushar stacken med ebp-innehållet), så CPU:n kontrollerar även GDT/LDT för adressen i esp-registret (adressen till den aktuella platsen på stacken, och eftersom det är stapelplatsen vet CPU:n att den skall använda stack-segmentregistret för detta) för att försäkra sig om att det är skrivbart i den specifika ringen. Observera att om denna instruktion också skulle läsa från minnet skulle CPU också ha kontrollerat den relevanta adressen för lästillgång.

Detta är inte allt, efter att CPU kontrollerat alla säkerhetsaspekter måste den nu få tillgång till och manipulera minnet, som du minns är adresserna i sitt virtuella format. MMU:n översätter nu varje virtuellt minne som anges i instruktionen till en fysisk minnesadress med hjälp av CR3-registret som pekar på sidkatalogen (som pekar på sidtabellen) som gör det möjligt för oss att så småningom översätta adressen till en fysisk adress. Observera att adressen kanske inte finns i minnet när den behövs, i så fall kommer operativsystemet att generera ett page fault (ett undantag som genererar ett avbrott) och kommer att föra data till det fysiska minnet åt oss och sedan fortsätta utförandet (detta är genomskinligt för appen i användarläge).

Från användare till kärna

Varje utbyte mellan användarläge och kärnläge sker med hjälp av IDT. Från användarlägesprogrammet överförs instruktionen ”int <num>” till funktionen i IDT vid index num. När utförandet sker i kärnläge ändras många av reglerna, varje tråd har olika stackar för användar- och kärnläge, kontroller av minnesåtkomst är mycket mer komplicerade och obligatoriska, i kärnläge finns det väldigt lite man inte kan göra och mycket man kan bryta.

ASLR och KASLR

Merparten av gångerna är det ”bara” bristen på kunskap som hindrar oss från att uppnå det omöjliga.

ASLR (Address Space Layout Randomization) är ett koncept som implementeras på olika sätt i varje operativsystem, konceptet är att randomisera de virtuella adresserna för processerna och deras inlästa bibliotek.

Innan vi dyker in vill jag notera att jag bestämde mig för att inkludera ASLR i det här inlägget eftersom det är ett trevligt sätt att se hur skyddat läge och dess strukturer möjliggjorde den här typen av kapacitet även om det inte är den som implementerar det eller ansvarar för det för den delen.

Varför ASLR?

Varför är det enkelt, för att förhindra attacker. När någon kan injicera kod i en pågående process är det att inte känna till adresserna till vissa fördelaktiga funktioner som kan få attacken att misslyckas.

Vi har redan olika adressutrymmen för varje process, vilket innebär att utan ASLR skulle alla processer ha samma basadresser, detta beror på att när varje process har sitt eget virtuella adressutrymme behöver vi inte vara försiktiga med kollisioner mellan processer. När vi länkar programmet väljer länkaren en fast basadress som den länkar den körbara filen mot. På pappret kommer alla körbara filer som länkas av samma länkare med standardparametrarna (basadressen kan konfigureras vid behov) att ha samma basadress. Som exempel har jag skrivit två program, ett som heter ”1.exe” och ett som heter ”2.exe”.exe”, båda är olika projekt i Visual Studio och ändå har båda samma basadress (jag använde exeinfo PE för att se basadressen i PE-filen):

Det är inte bara så att de här två exekverbara programmen har samma basadress, de har inte heller stöd för ASLR (jag inaktiverade det):

Du kan också se att det finns med i PE-formatet under File Characteristics:

Nu kör vi båda körprogrammen samtidigt och båda delar samma basadress (jag kommer att använda vmmap från Sysinternals för att se basbilden):

Vi kan se att de båda processerna inte använder ASLR och har samma basadress 0x00400000. Om vi var angripare och hade tillgång till den här körbara filen kunde vi ha vetat exakt vilka adresser som skulle vara tillgängliga för den här processen när vi hade hittat en väg att injicera oss själva i dess exekvering.Låt oss aktivera ASLR i vår körbara fil 1.exe och se hur det fungerar:

Det förändrades!

KASLR (Kernel ASLR) är samma sak som ASLR, men den fungerar på kärnnivå, vilket innebär att när en angripare har lyckats ta sig in i kärnans kontext kommer han (förhoppningsvis) inte att kunna veta vilka adresser som innehåller vilka strukturer (t.ex. var GDT finns i minnet). En sak som bör nämnas här är att ASLR utför sin magi vid varje start av en ny process (som har stöd för ASLR förstås) medan KASLR gör det vid varje omstart eftersom det är då som kärnan är ”spawnad”.

Hur fungerar ASLR?

Hur fungerar det och hur hänger det ihop med skyddat läge? Den som är ansvarig för att implementera ASLR är laddaren. När en process startas är det laddaren som måste placera den i minnet, skapa de relevanta strukturerna och starta dess tråd. Laddaren kontrollerar först om den körbara filen har stöd för ASLR och om så är fallet slumpar den ut en basadress inom de tillgängliga adresserna (kärnutrymmet är t.ex. inte tillgängligt). Baserat på den adressen initialiserar laddaren nu Page Directory för den processen för att peka ut det slumpmässiga adressutrymmet till det fysiska. LDT:s flexibilitet kommer också till vår undsättning eftersom laddaren helt enkelt skapar en LDT som motsvarar den randomiserade adressen med relevanta behörigheter. Det fina här är att det skyddade läget inte ens är medvetet om att ASLR används, det är tillräckligt flexibelt för att inte bry sig.

En intressant genomförandedetalj är att i Windows är den slumpmässiga adressen för en specifik körbar dator fastställd av effektivitetsskäl. Vad jag menar med det är att om vi randomiserar adressen för till exempel calc.exe, kommer basadressen att vara densamma andra gången den exekveras. Så om jag öppnar två miniräknare samtidigt kommer de att ha samma basadress. När jag stänger båda kalkylatorerna och öppnar dem igen kommer de båda att ha samma adress igen, men denna kommer att skilja sig från de tidigare kalkylatorernas adress. Varför är detta inte effektivt frågar du dig? Tänk på vanligt förekommande DLL:er. Många processer använder dem och om deras basadresser var olika för varje process skulle deras kod också vara annorlunda (koden hänvisar till data med hjälp av denna basadress) och om koden är annorlunda måste DLL:en laddas in i minnet för varje process. I verkligheten laddar operativsystemet bilderna endast en gång för alla processer som använder denna bild. Det sparar utrymme – mycket utrymme!

Slutsats

Nu bör du kunna föreställa dig kärnan i arbete och förstå hur alla nyckelstrukturer i x86-arkitekturen samspelar i en större helhet och gör det möjligt för oss att köra eventuellt farliga tillämpningar i användarläge utan (eller med liten) rädsla.

Articles

Lämna ett svar

Din e-postadress kommer inte publiceras.