Deskriptory virtuálních adres 1: virtuální adresový prostor

Každá běžící aplikace disponuje vlastním virtuálním adresovým prostorem. Do jeho horní části operační systém mapuje paměť jádra. Horní poloviny všech virtuálních adresových prostorů jsou téměř identické. Zbývající část (2-3 GB na architektuře IA32, několik TB na 64bitových platformách) může proces využívat k ukládání vlastních dat, mapování souborů a knihoven a dalším účelům, které vás jen mohou napadnout. Tuto oblast adresového prostoru budu v tomto seriálu také nazývat jako privátní.

Privátní oblast adresového prostoru se skládá z jednotlivých paměťových stránek, které mohou být buď volné, rezervované, nebo alokované. Tato věta, stejně jako celý předchozí odstavec, neobsahuje žádnou objevnou informaci, kterou byste si nemohli přečíst v mé knize nebo z jiných zdrojů ([1], [2]). Tento text si však neklade za svůj hlavní cíl sdělovat základní vědomosti, ale informovat čtenáře o tom, jaké datové struktury a algoritmy se pod povrchem virtuálních adresových prostorů skrývají. V mé knize o této oblasti jádra naleznete jen poměrně povrchní informace a ukázkovou aplikaci Vad, která může posloužit (a v případě tohoto textu tomu tak bylo) k jejich značnému prohloubení.

Poznámka: I horní část virtuálního adresového prostoru, do které systém mapuje paměť jádra, je také rozdělena na stránky. Tento fakt jsem však v předchozím odstavci nezdůraznil z toho důvodu, že se tento text danou oblastí adresového prostoru nezabývá.

Celý obsah seriálu lze významově rozdělit na pět částí. V první se dozvíte, jak vypadá privátní část adresového prostoru z uživatelského režimu. Druhá část popisuje datové struktury, pomocí kterých si správce paměti rozložení adresového prostoru pamatuje. Ve třetí části si ukážeme, jaké konkrétní záznamy jádro používá pro reprezentaci bloků paměti alokované či rezervované pro konkrétní účely. Čtvrtá část krátce popisuje seznam programů použitých během psaní textu. V poslední naleznete definice potřebných datových struktur pro starší verze operačního systému. Celý text se totiž zaměřuje výhradně na Windows 7 (verze jádra 6.1.7600 a 6.1.7601).

Povrch adresového prostoru

Ke zkoumání stavu jednotlivých paměťových stránek privátní části virtuálního adresového prostoru slouží funkce rozhraní Windows API VirtualQuery. Její rozšířená varianta VirtualQueryEx dovoluje zkoumat povrch i jiných adresových prostorů než toho patřícího vlastnímu procesu. Rutina adresový prostor rozdělí do bloků. Každý blok lze definovat jako souvislý úsek paměti, jehož stránky mají stejné vlastnosti, mezi které patří stav, oprávnění a účel alokace.

Pro účely tohoto článku jsem vytvořil jednoduchou aplikaci Vq, která pomocí rutiny VirtualQueryEx prozkoumá povrch privátní části adresového prostoru procesu zadaného parametrem příkazové řádky. Svá zjištění v podobě seznamů bloků a jejich vlastností vypíše na standardní výstup. Ukázkový výstup, se kterým budu v této části seriálu dále pracovat, naleznete ve výpisu 1.

Výpis 1: Struktura povrchu adresového prostoru v podání utility Vq

0000000000000000 0000000000000000 65536	  Free        
0000000000010000 0000000000010000 65536	  Committed   Mapped  ---RW-- ---RW--
0000000000020000 0000000000020000 65536	  Committed   Mapped  ---RW-- ---RW--
0000000000030000 0000000000030000 16384   Committed   Mapped  ---R--- ---R---
0000000000000000 0000000000034000 49152	  Free        
0000000000040000 0000000000040000 4096    Committed   Mapped  ---R--- ---R---
0000000000000000 0000000000041000 61440	  Free        
0000000000050000 0000000000050000 4096	  Committed   Private ---RW-- ---RW--
0000000000000000 0000000000051000 61440	  Free        
0000000000060000 0000000000060000 1028096 Reserved    Private    
0000000000060000 000000000015B000 8192	  Committed   Private --GRW-- ---RW--
0000000000060000 000000000015D000 12288	  Committed   Private ---RW-- ---RW--
0000000000160000 0000000000160000 4096	  Committed   Mapped  ---R--- ---R---
0000000000000000 0000000000161000 61440	  Free        
0000000000170000 0000000000170000 8192	  Committed   Private ---R--- ---R---
0000000000000000 0000000000172000 57344	  Free        
0000000000180000 0000000000180000 176128  Committed   Private ---RW-- ---RW--
0000000000180000 00000000001AB000 872448  Reserved    Private    
0000000000280000 0000000000280000 421888  Committed   Mapped  ---R--- ---R---
0000000000000000 00000000002E7000 36864	  Free        
00000000002F0000 00000000002F0000 131072  Committed   Private ---RW-- ---RW--
00000000002F0000 0000000000310000 917504  Reserved    Private    
00000000003F0000 00000000003F0000 8192    Committed   Private ---RW-- ---RW--
0000000000000000 00000000003F2000 319488  Free        
0000000000440000 0000000000440000 32768   Committed   Private ---RW-- ---RW--
0000000000440000 0000000000448000 32768   Reserved    Private    
0000000000000000 0000000000450000 524288  Free        
00000000004D0000 00000000004D0000 40960   Committed   Private  --RW-- ---RW--
00000000004D0000 00000000004DA000 24576   Reserved    Private    
00000000004E0000 00000000004E0000 131072  Committed   Private ---RW-- ---RW--
00000000004E0000 0000000000500000 917504  Reserved    Private    
0000000000000000 00000000005E0000 1998782464	Free        
0000000077810000 0000000077810000 4096    Committed   Image   ---R--- ---RWEC
0000000077810000 0000000077811000 634880  Committed   Image   ---R-E- ---RWEC
0000000077810000 00000000778AC000 450560  Committed   Image   ---R--- ---RWEC
0000000077810000 000000007791A000 8192    Committed   Image   ---RW-- ---RWEC
0000000077810000 000000007791C000 77824   Committed   Image   ---R--- ---RWEC
0000000000000000 000000007792F000 1380352 Free        
0000000077A80000 0000000077A80000 4096    Committed   Image   ---R--- ---RWEC
0000000077A80000 0000000077A81000 1056768 Committed   Image   ---R-E- ---RWEC
0000000077A80000 0000000077B83000 192512  Committed   Image   ---R--- ---RWEC
0000000077A80000 0000000077BB2000 4096    Committed   Image   ---RW-- ---RWEC
0000000077A80000 0000000077BB3000 4096    Committed   Image   ---RW-C ---RWEC
0000000077A80000 0000000077BB4000 4096    Committed   Image   ---RW-- ---RWEC
0000000077A80000 0000000077BB5000 8192    Committed   Image   ---RW-C ---RWEC
0000000077A80000 0000000077BB7000 4096    Committed   Image   ---RW-- ---RWEC
0000000077A80000 0000000077BB8000 4096    Committed   Image   ---RW-C ---RWEC
0000000077A80000 0000000077BB9000 8192    Committed   Image   ---RW-- ---RWEC
0000000077A80000 0000000077BBB000 12288   Committed   Image   ---RW-C ---RWEC
0000000077A80000 0000000077BBE000 438272  Committed   Image   ---R--- ---RWEC
0000000000000000 0000000077C29000 121335808   Free        
000000007EFE0000 000000007EFE0000 20480   Committed   Mapped  ---R--- ---R---
000000007EFE0000 000000007EFE5000 1028096 Reserved    Mapped     
000000007F0E0000 000000007F0E0000 15728640    Reserved    Private    
000000007FFE0000 000000007FFE0000 4096    Committed   Private ---R--- ---R---
000000007FFE0000 000000007FFE1000 61440   Reserved    Private    
0000000000000000 000000007FFF0000 12288   Free        
000000007FFF3000 000000007FFF3000 4096    Committed   Private ---RW-- ---RW--
0000000000000000 000000007FFF4000 3215114240    Free        
000000013FA20000 000000013FA20000 4096    Committed   Image   ---R--- ---RWEC
000000013FA20000 000000013FA21000 36864   Committed   Image   ---R-E- ---RWEC
000000013FA20000 000000013FA2A000 12288   Committed   Image   ---R--- ---RWEC
000000013FA20000 000000013FA2D000 16384   Committed   Image   ---RW-- ---RWEC
000000013FA20000 000000013FA31000 12288   Committed   Image   ---R--- ---RWEC
0000000000000000 000000013FA34000 3143876608    Free        
000007FEFB070000 000007FEFB070000 4096    Committed   Image   ---R--- ---RWEC
000007FEFB070000 000007FEFB071000 20480   Committed   Image   ---R-E- ---RWEC
000007FEFB070000 000007FEFB076000 12288	  Committed   Image   ---R--- ---RWEC
000007FEFB070000 000007FEFB079000 12288	  Committed   Image   ---RW-- ---RWEC
000007FEFB070000 000007FEFB07C000 12288	  Committed   Image   ---R--- ---RWEC
0000000000000000 000007FEFB07F000 49352704    Free
000007FEFDF90000 000007FEFDF90000 4096    Committed   Image   ---R--- ---RWEC
000007FEFDF90000 000007FEFDF91000 303104  Committed   Image   ---R-E- ---RWEC
000007FEFDF90000 000007FEFDFDB000 90112   Committed   Image   ---R--- ---RWEC
000007FEFDF90000 000007FEFDFF1000 8192    Committed   Image   ---RW-- ---RWEC
000007FEFDF90000 000007FEFDFF3000 36864   Committed   Image   ---R--- ---RWEC
0000000000000000 000007FEFDFFC000 31080448    Free        
000007FEFFDA0000 000007FEFFDA0000 4096    Committed   Image   ---R--- ---RWEC
0000000000000000 000007FEFFDA1000 2158592    Free        
000007FFFFFB0000 000007FFFFFB0000 143360  Committed   Mapped  ---R--- ---R---
0000000000000000 000007FFFFFD3000 28672   Free        
000007FFFFFDA000 000007FFFFFDA000 4096    Committed   Private ---RW-- ---RW--
0000000000000000 000007FFFFFDB000 12288   Free        
000007FFFFFDE000 000007FFFFFDE000 8192    Committed   Private ---RW-- ---RW--
000007FFFFFE0000 000007FFFFFE0000 65536   Reserved    Private    

Druhý a třetí sloupec výpisu udávají adresu počátku a velikost bloku paměťových stránek, kterého se týkají další informace zapsané na daném řádku. Jedná se o největší souvislý blok stránek s danými vlastnostmi. První sloupec obsahuje adresu počátku bloku vrácenou při prvním volání funkcí VirtualAlloc (VirtualAllocEx) či MapViewOfFile (MapViewOfFileEx). Tato adresa se může od hodnoty v druhém sloupci lišit. V takovým případě byla funkce VirtualAlloc na daný blok použita víckrát: poprvé došlo například k rezervování celého bloku, následně byly některé stránky alokovány. Rozdělení jednoho bloku na více částí také může být způsobeno změnou oprávnění jednotlivých stránek. K tomuto účelu slouží rutiny VirtualProtect a VirtualProtectEx.

Z velikosti adres ve výpisu 1 lze odvodit, že aplikace Vq byla spuštěna na 64bitové platformě. Konkrétně se jedná o Windows 7 x64 Service Pack 1.

Čtvrtý sloupec ve výpisu 1 udává stav paměťových stránek v daném bloku. Možné hodnoty jsou následující:

  • Volná (Free) – blok stránek je volný. Může být alokován či rezervován voláním některé z alokačních rutin (VirtualAlloc, VirtualAllocEx, MapViewOfFile, MapViewOfFileEx).

  • Rezervovaná (Reserved) – blok stránek nelze (vyjma explicitního volání VirtualAlloc či VirtualAllocEx) alokovat či rezervovat. Nebyl ale namapován do fyzické paměti, ani mu nebyla přidělena oblast ve stránkovacím souboru (pokud stránkovací soubor existuje). Libovolný pokus o přístup do takové oblasti končí chybou.

  • Alokovaná (Committed) – bloku stránek je přiřazeno místo ve fyzické paměti či ve stránkovacím souboru. Přístup do takové oblasti obvykle nekončí chybou, pokud nedojde k porušení oprávnění stránek.

Pátý sloupec udává bližší informace o rezervovaných a alokovaných stránkách. Hodnota Private znamená stránky alokované pomocí funkce VirtualAlloc či VirtualAllocEx. Obsah takových stránek nelze namapovat do adresových prostorů více procesů. Hodnota Mapped indikuje, že daný blok reprezentuje soubor mapovaný do paměti (memory mapped file). Takové bloky vznikají použitím funkcí MapViewOfFile a MapViewOfFileEx. Obsah paměťově mapovaných souborů může být sdílen mezi více procesy. Hodnota Image indikuje, že do daného bloku paměti je namapován soubor formátu PE. Jedná se tedy buď o hlavní modul aplikace, nebo o některou z knihoven, které používá.

Poslední dva sloupce udávají oprávnění, kterými stránky v daném bloku disponují nebo disponovaly. Sedmý sloupec sděluje oprávnění zadané při první alokaci či rezervaci daného bloku. Šestý sloupec udává aktuální oprávnění.

Rozhodl jsem se zvolit formát zobrazování oprávnění podobný tomu, jaký používá například program ls na unixových operačních systémech. Kromě práva čtení, zápisu a spouštění (písmena R, W a E) však paměťová stránka může disponovat ještě dalšími:

  • Guard (G) – přístup na stránku s tímto oprávněním tento příznak vynuluje a vyvolá výjimku GUARD_PAGE_VIOLATION. Aplikace takové oprávnění může využít například k tomu, aby se dozvěděla, kdy dojde k prvnímu přístupu na určitá místa v paměti.

  • Copy on write (C) – operační systém na těchto stránkách provozuje optimalizaci copy on write, která šetří fyzickou paměť. Tuto optimalizaci lze použít pouze u fyzické paměti namapované do více adresových prostorů současně. Dokud budou všechny procesy takový blok pouze číst, data se ve fyzické paměti budou nacházet pouze v jednom exempláři, ač mohou být viditelná klidně v několika desítkách adresových prostorů a každý proces si může myslet, že má jejich separátní kopii. Pokud některý proces provede zápis, správce paměti mu vytvoří privátní kopii změněných stránek. Pro proces je tato událost transparentní.

  • No cache (N) – proces při alokaci daného bloku použil příznak PAGE_NOCACHE. Obsah daného bloku a jeho změny nejsou ukládány do vyrovnávací paměti procesoru, ale zapisují se přímo do fyzické paměti.

  • Write combined (O) – proces při alokace daného bloku použil příznak PAGE_WRITECOMBINED. Obsah stránek takové oblasti, stejně jako v případě příznaku PAGE_NOCACHE, neprochází přes vyrovnávací paměti procesoru (L1 a L2 cache), ale jeho změny se zapisují do speciální oblasti zvané write combined buffer (WC buffer). V tomto bufferu dochází k agregaci jednotlivých zápisů do jednoho požadavku, který je následně odeslán do operační paměti. WC buffer může navíc způsobit změnu pořadí jednotlivých operací [3].

Zajímavé útvary

Ještě než zamíříme pod povrch adresového prostoru, podíváme se na některá zajímavá uskupení, která lze na výpisu 1 najít. Prvním z nich je způsob, jakým se do adresového prostoru mapují soubory formátu PE (knihovny a hlavní soubor programu). Podívejme se například následující řádky z výpisu 1.

000007FEFDF90000 000007FEFDF90000 4096    Committed   Image   ---R--- ---RWEC
000007FEFDF90000 000007FEFDF91000 303104  Committed   Image   ---R-E- ---RWEC
000007FEFDF90000 000007FEFDFDB000 90112   Committed   Image   ---R--- ---RWEC
000007FEFDF90000 000007FEFDFF1000 8192    Committed   Image   ---RW-- ---RWEC
000007FEFDF90000 000007FEFDFF3000 36864   Committed   Image   ---R--- ---RWEC

Vidíme, že pro soubor byl alokován jeden blok paměti (první sloupec má vždy stejnou hodnotu), který byl následně rozdělen změnou oprávnění jednotlivých oblastí. Systém tak postupuje proto, že soubory PE se skládají z několika sekcí, každá obsahuje informace určitého typu, a tedy potřebuje nastavit jiná oprávnění, aby je bylo možné správně využít.

Například druhý blok stránek má aktuálně nastavena oprávnění pro čtení a spouštění. Bude se pravděpodobně jednat o sekci obsahující kód (instrukce procesoru). Čtvrtý blok disponuje oprávněním pro čtení a pro zápis, pravděpodobně půjde o mapování sekce s proměnnými nebo jinými daty.

Hodnoty oprávnění při alokaci také dávají tušit, že operační systém se bude snažit obsah jednotlivých sekcí sdílet mezi jednotlivými adresovými prostory, a tak šetřit fyzickou paměť. Tento postup je efektivní u základních knihoven DLL, které se obvykle nachází ve většině adresových prostorů. Vzhledem k vlastnostem formátu PE dochází k šetření paměti pouze v případě, že daná knihovna je namapována do adresových prostorů na stejných virtuálních adresách.

Dalším zajímavým místem je paměť sloužící jako zásobník vlákna. Na výpisu 1 se jedná o tyto tři řádky:

0000000000060000 0000000000060000 1028096 Reserved    Private
0000000000060000 000000000015B000 819     Committed   Private --GRW-- ---RW--
0000000000060000 000000000015D000 12288   Committed   Private ---RW-- ---RW--

Vidíme, že prostor pro zásobník vlákna byl nejprve celý rezervován. Následně došlo k alokaci posledních pěti stránek, přičemž dvě z nich mají nastavené oprávnění G, takže při prvním přístupu generují výjimku GUARD_PAGE_VIOLATION.

Jelikož zásobník v podání architektur x86 a x64 roste směrem k nižším adresám, alokace ne prvních, ale posledních stránek by nás neměla překvapovat. Vrchol zásobníku vlákna začne někde na konci poslední stránky a jak se bude entita zanořovat do stromu volání funkcí a procedur, bude postupně klesat. Proč však byly alokovány i stránky s oprávněním G?

Při hluboké rekurzi se může stát, že vláknu nebude stačit 12 KB prozatím alokovaných pro potřeby jeho zásobníku. Vrchol se dostane do stránky s nastaveným oprávněním G a dojde k výjimce. Ačkoliv výjimky obvykle znamenají něco zlého, k čemu by docházet nemělo, tato konkrétní slouží pro dobré účely. Díky ní systém pozná, že vlákno vyčerpalo zatím přidělený prostor pro zásobník a může pro něj alokovat více stránek z rezervované části.

Oblast s oprávněním G tedy slouží jako upozornění, že zásobník je třeba zvětšit. Systém tak nemusí alokovat celou paměť, kterou vláknu pro zásobník na počátku slíbil (standardně se jedná o 1 MB), ale pouze její velmi malou část. O zbytek požádá až v případě potřeby a oblast s oprávněním G postupně posouvá dolů. Dochází tak opět k šetření fyzické paměti.

Tím výčet z mého pohledu zajímavých míst na povrchu adresového prostoru končí. Nastal čas přestat zkoumat povrch a podívat se na to, jak jádro údaje o jednotlivých blocích paměťových stránek uchovává a co s nimi dělá při alokaci nových a dealokaci již nepotřebných oblastí. A tím se budeme zabývat v následujícím dílu seriálu.

Použité zdroje

[1] Russinovich Mark., Solomon David, Ionescu Alex: Windows Internals Covering Windows Server 2008 and Windows Vista, 5. vydání, Microsoft Press 2009
[3] Write Combining Memory Implementation Guidelines, Intel Corporation, 1998

Leave a Reply

Your email address will not be published. Required fields are marked *