Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

RtlCreateProcessReflection, czyli widelec(), a zrzuty pamięci

Śmiem twierdzić, że za sprawą Windows 7 systemy 64-bitowe na dobre upowszechniły się. A jak 64 bity, to i więcej pamięci, z której chętnie korzystają aplikacje 64-bitowe. Ten kij ma jednak drugi koniec - zrzut pamięci na dysk dla procesu, który wykorzystuje grubo ponad 4GB nie jest natychmiastowy i potrafi trochę potrwać, blokując skutecznie zrzucany proces. Czy można coś na to poradzić? Wygląda na to, że istnieje pewne nowe rozwiązanie systemowe i spróbuję je w tym tekście przedstawić. Planowałem co prawda zademonstrować je na styczniowym spotkaniu WG.NET, podobnie zresztą jak i kilka innych niespisanych jeszcze tematów, ale spotkanie nie doszło do skutku i na razie musi wystarczyć tekst :)

Idea

Załóżmy, że mamy bardzo pamięciożerny proces A. Teraz wyobraźmy sobie, że mamy mechanizm, który na chwilkę zamraża proces A, tworzy jego kopię (proces B), której udostępnia całą pamięć procesu A, po czym wznawia działanie procesu A. Zaraz, zaraz, czy to przypadkiem nie brzmi jak fork(), znany z wszelkiej maści Linuxów i Unixów?
pid_t pid = fork();
if(pid < 0)
// błąd
else if(pid == 0)
// potomny
else
// macierzysty

I wszystko super, ale w Windowsach nie ma fork’a! Mamy do dyspozycji wyłącznie CreateProcess*(), który tworzy nowe procesy nie mające za wiele wspólnego z procesem macierzystym.

RtlCreateProcessReflection

W tym miejscu do gry wchodzi zespół odpowiedzialny za wydajność systemu, za którego sprawą w bibliotece ntdll.dll pojawiła się tytułowa funkcja, z pomocą której możemy rozwiązać problem opisany we wstępie. Nie jest to co prawda fork, ale w ramach ‘podobieństwa’ możemy nazwać ją roboczo widelec(), tudzież ‘zdalny_widelec()’, a dlaczego tak, to za chwilę się wyjaśni.
Funkcja jest bez jakiejkolwiek dokumentacji i poza WER jedynym znanym mi przypadkiem jej użycia jest procdump Sysinternalsów, za sprawą zresztą którego zainteresowałem się tym mechanizmem. W helpie do procdumpa znajdziemy bowiem coś takiego:

>procdump /?
ProcDump v4.0 - Writes process dump files
Copyright (C) 2009-2011 Mark Russinovich
Sysinternals - www.sysinternals.com
Monitors a process and writes a dump file when the process exceeds the
specified CPU usage.
[..]
   -r      Reflect (clone) the process for the dump to minimize the time
           the process is suspended (Windows 7 and higher only).
[..]

Gdy skorzystamy z tej opcji, procdump utworzy na chwilkę klon procesu, zrobi full dump klona, minidump procesu macierzystego (wątki + handle) i do tego utworzy plik .ini wiążący oba pliki, przy czym jego format przedstawia się mniej-więcej tak:

[UserModeDump]
default=parent.dmp
memory=clone.dmp
module=parent.dmp

Teraz wystarczy załadować do debuggera plik .ini, a ten pięknie połączy nam oba .dmp i w rezultacie dostaniemy coś, co będzie wyglądało jak pełny zrzut procesu macierzystego.

Zajrzyjmy głębiej

Dokumentacji co prawda nie ma, ale RtlCreateProcessReflection jest jedną z eksportowanych funkcji ntdll.dll i po skorzystaniu z pliku z symbolami możemy to i owo wywnioskować. Korzystając z tej wiedzy przygotowałem sobie prosty tool tworzący klony i postanowiłem sprawdzić co też dzieje się z pamięcią procesu po utworzeniu klonu.
Uruchomiłem nieśmiertelny notatnik, napisałem "Ala ma kota" i zajrzałem do pamięci procesu (Rys 1)

Rys 1. Pamięć wirtualna notatnika

Postanowiłem poszukać miejsca, w którym znajduje się nasza "Ala ma kota"

Rys 2. Wynik poszukiwania ciągu "Ala ma kota"

A także podejrzeć zawartość

Rys 3. Podgląd zawartości pamięci notatnika

Zauważmy na Rys 1, że nasz adres (0x329760) wpada w prywatny (Private Commit) obszar pamięci rozpoczynający się na adresie 0x2d0000 o całkowitym rozmiarze 388kB (97 stron po 4kB) oznaczony jako RW (do odczytu i zapisu).

System pracuje w trybie debugowania w trybie jądra, więc przechodzę do podłączonego WinDbg i sprawdzam zawartość pamięci dla notatnika. Zaczynam od znalezienia procesu

1: kd> !process 0n1456 0
Searching for Process with Cid == 5b0
Cid handle table at fffff8a005df0000 with 536 entries in use

PROCESS fffffa8002e0fb30
    SessionId: 1  Cid: 05b0    Peb: 7fffffd7000  ParentCid: 0778
    DirBase: 4d893000  ObjectTable: fffff8a0025f8980  HandleCount:  56.
    Image: notepad.exe

W następnym kroku ustawiam bieżący kontekst na znaleziony proces

1: kd> .process /p fffffa8002e0fb30
Implicit process is now fffffa80`02e0fb30
.cache forcedecodeuser done

Upewniam się, że adresy wirtualne procesu (adres biorę z Rys 2.) są takie same w bieżącym kontekście i wskazują na tę samą zawartość

1: kd> du 329760
00000000`00329760 "Ala ma kota"
1: kd> dd 329760
00000000`00329760 006c0041 00200061 0061006d 006b0020
00000000`00329770 0074006f 00000061 00000000 00000000
00000000`00329780 00000000 00000000 00000000 00000000
00000000`00329790 00000000 00000000 00000000 00000000
00000000`003297a0 00000000 00000000 020d0008 00000000
00000000`003297b0 00000000 00000000 4e3c11c9 10001368
00000000`003297c0 fc63d4e0 000007fe 001000d4 00000000
00000000`003297d0 00000000 00000000 00000000 00000000

po czym szukam strony na której znajduje się nasza kochana Ala ze swoim kotem. Jako leniwiec korzystam przy tym z rozszerzenia !pte (page table entries), choć pewnie mógłbym skorzystać z mapy stron (L4) wskazywanej przez DirBase: 4d893000 (wzięte z pierwszego zrzutu dotyczącego procesu) i na paluszkach to jakoś poprzeliczać.

1: kd> !pte 329760
                                           VA 0000000000329760
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001948
contains 03A000004BB9F867  contains 3E9000004C322867  contains 2B6000004F635867  contains D2D000004F646867
pfn 4bb9f     ---DA--UWEV  pfn 4c322     ---DA--UWEV  pfn 4f635     ---DA--UWEV  pfn 4f646     ---DA--UW-V

Przy czym: D-dirty, A-accessed, U-user mode, W-writable, V-valid (za dokumentacją WinDbg). Mając adres strony mogę teraz zajrzeć do pamięci fizycznej i porównać jej zawartość z tym, co mam pod adresem wirtualnym. Mnemotechnicznie - biorę 3 ostatnie cyfry adresu wirtualnego (760) i doklejam je do numeru strony (4f646), ale oczywiście stoi za tym odpowiednia arytmetyka związana z tłumaczeniem adresów wirtualnych na fizyczne i być może kiedyś to opiszę dokładniej, a póki co odsyłam do Russinovicha i jego książki o internalsach Windows.

1: kd> !du 4f646760
#4f646760 "Ala ma kota"

A więc to samo, co w zrzucie pamięci wirtualnej! Na wszelki wypadek robię jeszcze zrzut surowy

1: kd> !dd 4f646760
#4f646760 006c0041 00200061 0061006d 006b0020
#4f646770 0074006f 00000061 00000000 00000000
#4f646780 00000000 00000000 00000000 00000000
#4f646790 00000000 00000000 00000000 00000000
#4f6467a0 00000000 00000000 020d0008 00000000
#4f6467b0 00000000 00000000 4e3c11c9 10001368
#4f6467c0 fc63d4e0 000007fe 001000d4 00000000
#4f6467d0 00000000 00000000 00000000 00000000

Tworzymy klony

Puszczam dalej system i tworzę klon notatnika. Na liście procesów pojawia się potomny notatnik

Rys 4. Procexp - notepad ze swoim potomkiem

Zauważmy, że lista uchwytów (ctrl+h w procexpie) jest pusta i na dzień dobry jest w stanie suspended (szare tło). Ciekawie prezentuje się też lista wątków oraz stos jedynego

Rys 5. Stos dla wątku klona

w porównaniu do listy wątków i stosu wywołań procesu macierzystego

Rys 6. Stos dla wątku notatnika - rodzica

Gołym okiem widać, że jedyny wątek procesu potomnego nie ma wiele wspólnego z notatnikiem. Klon nie ma okna, ani nawet nie jest wykonywany żaden kod z notepad.exe.

Sprawdzam zawartość pamięci notatnika

Rys 7. Pamięć notatnika macierzystego - po sklonowaniu

i porównuję go z zawartością pamięci klona

Rys 8. Pamięć klona

Gdy zestawimy Rys 7. z Rys 1. to okazuje się, że obszar zaczynający się od adresu 0x2d0000 został podzielony na mniejsze, a interesujący nas adres (0x329760) wpada do obszaru rozpoczynające się na adresie 0x315000, o rozmiarze 112 kB. Najciekawsze jednak jest zabezpieczenie tego obszaru - WC, co oznacza 'copy on write', ale o tym trochę dalej.

Sprawdzam, czy "Ala ma kota" znajduje się pod tym samym adresem wirtualnym w procesie klonie

Rys 9. Adres Ali w klonie

oraz zawartość pamięci klona w interesującym nas adresie

Rys 10. Zrzut pamięci klona - "Ala ma kota"

Jak widać, adres wirtualny "Ali" w przypadku notatnika-klona i notatnika-rodzica są takie same. Ale co z pamięcią fizyczną?

Copy on Write

Przechodzę do debuggera i robię zrzuty pamięci obu procesów. Zaczynam od procesu-rodzica.

0: kd> !process 0n1456 0
Searching for Process with Cid == 5b0
Cid handle table at fffff8a005df0000 with 536 entries in use

PROCESS fffffa8002e0fb30
    SessionId: 1  Cid: 05b0    Peb: 7fffffd7000  ParentCid: 0778
    DirBase: 4d893000  ObjectTable: fffff8a0025f8980  HandleCount:  56.
    Image: notepad.exe

0: kd> .process /p fffffa8002e0fb30
Implicit process is now fffffa80`02e0fb30
.cache forcedecodeuser done

Sprawdzam PTE dla adresu wirtualnego "Ali"

0: kd> !pte 329760
                                           VA 0000000000329760
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001948
contains 03A000004BB9F867  contains 3E9000004C322867  contains 2B6000004F635867  contains D2D000004F646225
pfn 4bb9f     ---DA--UWEV  pfn 4c322     ---DA--UWEV  pfn 4f635     ---DA--UWEV  pfn 4f646     C---A--UR-V

Kątem oka widzę, że pfn (4f646) jest ten sam, co przed sklonowaniem. Zmieniło się zabezpieczenie strony, zamiast W mamy teraz R-readable, a więc bez możliwości zapisu. Ale pojawiło się jeszcze C, czyli nasz Copy-on-Write.
Robimy zrzut pamięci wirtualnej

0: kd> du 329760
00000000`00329760 "Ala ma kota"

oraz fizycznej

0: kd> !du 4f646760
#4f646760 "Ala ma kota"

Przełączam się na proces potomny i wykonuję te same czynności, co w przypadku macierzystego.

0: kd> !process 0n2152 0
Searching for Process with Cid == 868
Cid handle table at fffff8a005df0000 with 543 entries in use

PROCESS fffffa80022bf620
    SessionId: 1  Cid: 0868    Peb: 7fffffd7000  ParentCid: 05b0
    DirBase: 4b20d000  ObjectTable: fffff8a01083f010  HandleCount:   0.
    Image: notepad.exe

0: kd> .process /p fffffa80022bf620
Implicit process is now fffffa80`022bf620
.cache forcedecodeuser done

Czas na PTE dla "Ali" w przypadku klona

0: kd> !pte 329760
                                           VA 0000000000329760
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001948
contains 000000004EE92867  contains 000000004A993867  contains 0130000049715867  contains 907000004F646225
pfn 4ee92     ---DA--UWEV  pfn 4a993     ---DA--UWEV  pfn 49715     ---DA--UWEV  pfn 4f646     C---A--UR-V

Wow! Ta sama strona pamięci, co w przypadku macierzystego! Czyli oba procesy - potomny oraz macierzysty współdzielą ten sam obszar pamięci fizycznej.

Puszczam system i sprawdzam co się stanie, gdy w notatniku zmodyfikuję tekst i zamiast "Ala" wpiszę "Ola"

Rys 11. Pamięć procesu rodzica po zmianie "Ala" na "Ola"

Adres wirtualny, w którym znajduje się "Ola" jest ten sam, co poprzednio. Zauważmy jednak, że tym razem trafił do obszaru rozpoczynającego się na adresie 0x327000 o rozmiarze 10 stron, jednak oznaczonego jako RW, czyli tak, jak miało to miejsce przed sklonowaniem.

Natomiast w przypadku klona

Rys 12. Pamięć klona po zmianie "Ala" na "Ola" w rodzicu

praktycznie nic się nie zmieniło w stosunku do tego, co było przed zamianą Ali na Olę w procesie macierzystym. A więc ten sam adres wirtualny, ta sama ochrona pamięci, ba, nawet obszar ten sam!

Pozostaje nam sprawdzić co się wydarzyło z pamięcią fizyczną. Zaczynamy od procesu rodzica.

0: kd> !process 0n1456 0
Searching for Process with Cid == 5b0
Cid handle table at fffff8a005df0000 with 545 entries in use

PROCESS fffffa8002e0fb30
    SessionId: 1  Cid: 05b0    Peb: 7fffffd7000  ParentCid: 0778
    DirBase: 4d893000  ObjectTable: fffff8a0025f8980  HandleCount:  57.
    Image: notepad.exe

0: kd> .process /p fffffa8002e0fb30
Implicit process is now fffffa80`02e0fb30
.cache forcedecodeuser done

0: kd> !pte 329760
                                           VA 0000000000329760
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001948
contains 03A000004BB9F867  contains 3E9000004C322867  contains 2B6000004F635867  contains D2D0000049F19867
pfn 4bb9f     ---DA--UWEV  pfn 4c322     ---DA--UWEV  pfn 4f635     ---DA--UWEV  pfn 49f19     ---DA--UW-V

Jak widać - nowy pfn z nowymi ustawieniami strony (identycznymi z tymi przed pojawieniem się klona). A zawartość pamięci?

0: kd> du 329760
00000000`00329760  "Ola ma kota"
0: kd> dd 329760
00000000`00329760  006c004f 00200061 0061006d 006b0020
00000000`00329770  0074006f 00000061 00000000 00000000
00000000`00329780  00000000 00000000 00000000 00000000
00000000`00329790  00000000 00000000 00000000 00000000
00000000`003297a0  00000000 00000000 020d0008 00000000
00000000`003297b0  00000000 00000000 4e3c11c9 10001368
00000000`003297c0  fc63d4e0 000007fe 001000d4 00000000
00000000`003297d0  00000000 00000000 00000000 00000000

I jeszcze sprawdzamy zawartość strony, na żywca

0: kd> !du 49f19760
#49f19760 "Ola ma kota"
0: kd> !dd 49f19760
#49f19760 006c004f 00200061 0061006d 006b0020
#49f19770 0074006f 00000061 00000000 00000000
#49f19780 00000000 00000000 00000000 00000000
#49f19790 00000000 00000000 00000000 00000000
#49f197a0 00000000 00000000 020d0008 00000000
#49f197b0 00000000 00000000 4e3c11c9 10001368
#49f197c0 fc63d4e0 000007fe 001000d4 00000000
#49f197d0 00000000 00000000 00000000 00000000

Czyli wszystko się zgadza. W przypadku klona mamy natomiast

0: kd> !process 0n2152 0
Searching for Process with Cid == 868
Cid handle table at fffff8a005df0000 with 545 entries in use

PROCESS fffffa80022bf620
    SessionId: 1  Cid: 0868    Peb: 7fffffd7000  ParentCid: 05b0
    DirBase: 4b20d000  ObjectTable: fffff8a01083f010  HandleCount:   0.
    Image: notepad.exe

0: kd> .process /p fffffa80022bf620
Implicit process is now fffffa80`022bf620
.cache forcedecodeuser done
0: kd> !pte 329760
                                           VA 0000000000329760
PXE at FFFFF6FB7DBED000    PPE at FFFFF6FB7DA00000    PDE at FFFFF6FB40000008    PTE at FFFFF68000001948
contains 000000004EE92867  contains 000000004A993867  contains 0130000049715867  contains 907000004F646225
pfn 4ee92     ---DA--UWEV  pfn 4a993     ---DA--UWEV  pfn 49715     ---DA--UWEV  pfn 4f646     C---A--UR-V

0: kd> du 329760
00000000`00329760  "Ala ma kota"
0: kd> dd 329760
00000000`00329760  006c0041 00200061 0061006d 006b0020
00000000`00329770  0074006f 00000061 00000000 00000000
00000000`00329780  00000000 00000000 00000000 00000000
00000000`00329790  00000000 00000000 00000000 00000000
00000000`003297a0  00000000 00000000 020d0008 00000000
00000000`003297b0  00000000 00000000 4e3c11c9 10001368
00000000`003297c0  fc63d4e0 000007fe 001000d4 00000000
00000000`003297d0  00000000 00000000 00000000 00000000
0: kd> !du 4f646760
#4f646760 "Ala ma kota"

czyli dokładną kopię tego, co mieliśmy poprzednio, przed zamianą Ali na Olę.

Podsumowanie

Po utworzeniu klona część stron procesu macierzystego została oznaczona jako C, czyli copy on write i udostępniona procesowi klona. Jest to podejście 'leniwe' - po co bowiem tworzyć pełną kopię strony, skoro oba procesy mogą współdzielić tę, która jest i dopiero w przypadku, gdy któryś z nich będzie chciał coś do niej zapisać, to zostanie utworzona prywatna kopia tej strony i tam zostanie przepisana zawartość tej, wraz z modyfikacją. Próba zapisu kończy się wyjątkiem błędu zapisu, jednak zarządca pamięci tylko na niego czeka i w tym momencie tworzy odpowiednią kopię.
Po zmianie "Ali" na "Olę" proces macierzysty dostał nową, prywatną stronę, a stara zawartość pozostała niezmieniona i klon ciągle z niej może skorzystać.

Po wznowieniu procesu klona (w końcu jest w stanie suspended), ten kończy swoje działanie.

W tym miejscu chyba nikogo już nie dziwi ukuta wcześniej nazwa 'zdalny_widelec()', bo może i fork jest, ale jakiś taki nie do końca, no i w zdalnym procesie. Siebie też możemy 'nadziać', podobnie zresztą jak procesy systemowe :).

I już na sam koniec - w ramach zabaw polecam sprawdzenie co się stanie, gdy jako ostatni parametr do RtlCreateProcessReflection przekażemy 0. Normalnie w tym miejscu dostalibyśmy PID klona i TID jedynego jego wątku, a tak... :) Proponuję przetestować to na różnych procesach, w tym csrss, tudzież lsass...

Opublikowane 16 lutego 2012 02:55 przez mgrzeg

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

Sebastian said:

Świetny artykuł - zawsze się zastanawiałem jak działa ten klon w procdumpie :) Moim skromnym zdaniem to sam ten wpis mógłby się stać tematem prezentacji.

lutego 16, 2012 06:24
 

Karol Stilger said:

Nie kijem go to widelcem:)

lutego 16, 2012 06:26
 

mgrzeg said:

Sebastian, dzięki :) Opis może jest nieco przydługawy, ale 'na żywca' to zajmuje max 10-15 min, więc jako drobna dygresja do procdumpa mogłoby się nadawać :)

Karol: widelec.pl? :)

lutego 17, 2012 19:20

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij

Subskrypcje

W oparciu o Community Server (Personal Edition), Telligent Systems