Obsługa wiersza polecenia, a własciwie argumentów przekazywanych w ten sposób, to temat rzeka. Istnieje niezliczona liczba bibliotek i konwencji z których każda ma swoje wady i zalety. Pather, jako narzędzie wiersza polecenia, też musi sobie z tym poradzić. Zacząłem od określenia jak chciałbym, aby wyglądały polecenia:

  • pather load jekyll.paths
  • pather config-sys --part system -f system.paths

Nie będę ukrywał, że wzorowałem się na narzędziach takich jak Git czy Bundler, gdzie pierwszym argumentem jest polecenie po którym następują kolejne wartości w formie argumentów pozycjnych lub nazwanych. Te ostatnie występują w formie: -n wartość lub --name wartość.

Do implementacji mechanizmu postanowiłem wykorzystać bibliotekę CommandLineParser. W środowisku F# popularna jest biblioteka Argu, jednak nie wspiera ona funkcjonalności poleceń jako pierwszego argumentu. Całość postarałem napisać w taki sposób, aby nowe polecenia pojawiały się automatycznie po dodaniu odpowiedniego pliku źródłowego. Stanowi to wspaniałą okazję do ze sposobem w jaki elementy F# odwzorowywane są w struktury CLR.

Implementacja pojedynczego polecenia umieszczona jest w pliku w folderze Commands:

Argumenty opisuje rekord Args a za wykonanie odpowiada funkcja execute. Teraz wystarczy tylko zebrać wszystkie rekordy argumentów z całej aplikacji, sparsować wiersz polecenia i wykonać odpowiednią funkcję execute. Krótka zabawa z ILSpy pokazuje, że moduły są klasami oznaczonymi atrybutem CompilationMappingAttribute z ustawioną właściwością SourceConstructFlags na Module. W tej klasie musimy znaleźć klasę Args z atrybutem VerbAttribute:

Po dostarczeniu tablicy z typami argumentów CommandLineParser wykonuje swoją część roboty i dostajemy obiekt odpowiedniego typu. Teraz musimy znaleźć tylko odpowiednią funkcję execute, co jest dość proste:

Funkcja execute będzie składową typu, do którego należy typ argumentów.

Biblioteka CommandLineParser dobrze się sprawia, a odrobina refleksji pozwala elegancko rozdzielić implementację poszczególnych poleceń.

Aplikacje .NETowe często składają się z wielu plików - jeden plik wykonywalny (.exe) i kilka zestawów w formie bibliotek DLL. Nie jest to złe rozwiązanie, jednak utrudnia trochę rozpowszechanianie programu, gdyż nie można po prostu skopiować jednego pliku. Ponieważ chiałbym mieć łatwo dostępnego Pathera, na przykład jako jeden z plików na GitHub Pages czy do pobrania z GitHuba ropocząłem poszukiwania narzędzi do łączenia wielu plików wykonywalnych .NET jeden. Efektem było znalezienie dwóch narzędzi: ILMerge oraz ILRepack. Za pierwszym z nich przemawia gotowa integracja z FAKE, jednak ostatnia wersja została wydana końcem 2014 roku, a świat .NETowy często się zmienia i nie chciałbym zostać z narzędziem, które nie nadąża za zmianami. Druge - ILRepack - nazywa się alternatywą dla ILMerge’a. Co prawda nie ma gotowej integracji dla FAKE, ale udostępnianie jest w formie bibliteki do użycia we własnym kodzie, a skrypt budujący jest zwyczajnym skryptem F#, więc integracja nie powinna stanowić problemu.

Ostatecznie wybór padł na ILRepack.

ILRepack w FAKE

Pakowanie aplikacji dodałem jako koleny target. Wykorzystanie ILRepacka sprowadza się do określenia ścieżek do plików wejściowych i wyjściowego. Pierwszy z plików wejściowych będzie tym, do którego będą dokładane kolejne, czyli jego zależności. Cały target jest zaskakująco prosty:

Po zbudowaniu otrzymujemy jeden plik - Pather.exe - zawierający w sobie wszystkie zależności.

Gdy jednak spróbujemy zaktualizować zmienną PATH jakiegoś procesu staną się złe rzeczy…

Osadzanie natywnych bibliotek

Pather wykorzystuje dwie natywne biblioteki DLL do modyfikowania zmiennej PATH w procesach. Niestety, aby mogłe one być załadowane przez inny proces muszą istnieć jako pliki na dysku, co przeczy idei jednoplikowej aplikacji. Postanowiłem rozwiązać ten problem poprzez osadzenie ich jako zasóbów w Pather.exe, a następnie zapisaniu ich na dysku w momencie, kiedy będą potrzebne.

Na początek zmuszenie MSBuilda do umieszczenia binarek jako zasoby poprzez dodanie do Pather.fsproj:

Tworzymy nowy target (_AddInjections), który wywoła się przed kompilacją (CallTarget w BeforeBuild). W tym targecie dodajemy do grupy Injections dwa elementy określające jakie “wstrzyknięcia” chcemy osadzić jako zasoby. Następnie korzystając z magii MSBuilda definiujemy nowe zasoby (Resource) dla plików .dll i .pdb dla każdej biblioteki DLL. Nie jest to najpiękniejszy kawałek kodu, jednak lepsze to niż ręczne dodawanie plików wyjściowych jednego projektu jako plików źródłowych drugiego.

Mając wszystko co trzeba jako zasoby, możemy przystąpić do zapisania bibliotek na dysku tuż przed wstrzyknięciem ich od innego procesu:

Do wyciągnięcia zasobów wykorzystany został ResourceManager dający łatwy dostęp do poszczególnych strumieni, a zapisanie ich na dysku jest prostą sprawą. Nazwy zasobów odczytałem przy pomocy ILSpy - jeszcze nigdy nie udało mi się trafić :)

Tak oto Pather stał się jednoplikową, łatwo kopiowalną aplikacją.

W świecie .NETowym bardzo wiele rozwiązań pochodzi z Microsoftu. Część z nich naprawdę dobra, część ma pewne wady. Na szczęście społeczność open-source przychodzi z pomocą. W tym poście mam zamiar opowiedzieć o dwóch narzędziach, które starają się poprawić to co w “oficjalnych” nie jest idealne - Paket oraz FAKE

Paket

NuGet jest fantastycznym rozwiązaniem, w którym kilka elementów nie wyszło. Któż z nas nie spędził długich chwil patrząc na aktualizację pakietów czy wyliczanie zależności albo walce ze zmianą platformy na której ma działać aplikacja? Wersja 3.0 wprowadziła kilka kolejnych “udogodnień”: brak pakietów tylko z zawartością (to już pojawiło się ponownie w 3.3) czy pakietów solution-only.

Paket jest narzędziem napisanym w F#, który ma na celu poprawić te braki. Nie zastępuje on całej infrastruktury NuGeta, a jedynie część kliencką. Z najbardziej interesujących funkcji warto wymienić:

  • Szybsze rozwiązywanie zależności
  • Zależności opisane w prostych plikach tekstowych
  • Całe drzewo zależności zapisane w oddzielnym pliku (a nie w packages.config)
  • Działa bez Visual Studio
  • W ścieżkach do pakietów nie ma wersji
  • Zamiana platformy docelowej projektu nie wymaga przeinstalowania paczek
  • Szybsze rozwiązywanie zależności
  • Zależnością mogą być paczki NuGeta, pliki, Gisty, repozytoria gitowe

Dwukrotne wymienienie szybkości nie jest przypadkiem, nawet przy prostych operacjach z niewielką liczbą zależności Paket jest zdecydowanie szybszy niż NuGet. Do opisu zależności wykorzystywane są dwa pliki: paket.dependencies oraz paket.references. Pierwszy z nich jest znajduje się w folderze głównym solucji i opisuje skąd brać paczki oraz jakie są potrzebne. Drugi znajduje się w każdym projekcie i określa, które zależności powinny być przypisane jako referencje (w przypadku binarek). Po wprowadzeniu zmian wystarczy wydać polecenie paket install a całość osiągnie pożądany stan.

Na chwilę obecną pliki te dla Pathera wyglądają następująco:

Jak widać są one dość proste, a osoby znające Bundlera zauważą podobieństwo między Gemfile a paket.dependencies. Jeżeli ktoś nie przepada za konsolą, to jest dodatek do Visual Studio (https://github.com/fsprojects/Paket.VisualStudio) oraz do Atoma (https://atom.io/packages/paket).

FAKE

Drugim narzędziem o którym chciałbym wspomnieć jest Fake pozwalający na pisanie skryptów budujących jako skryptów F#. Każdy kto pisał kiedyś rozbudowane skrypty w MSBuildzie, (N)Antcie doceni swobodę pełnego języka i zwartego zapisu. Dodatkową zaletą jest fakt, że FAKE dystrybuowany jest w formie paczki NuGetowej i łatwo go zainstalować (np. Paketem). Warto też wspomnieć o rozbudowanej bibliotece standardowej dającej takie możliwości jak:

  • Generowanie AssemblyInfo
  • Uruchamianie MSBuilda
  • Uruchamianie testów xUnit, NUnit
  • Badanie pokrycia kodu OpenCoverem
  • Pobieranie paczek NuGetowych i uruchamianie Paketa
  • Zipowanie
  • Operowanie na repozytorium Gita
  • Budowanie paczek NuGeta
  • Konfigurowanie IISa
  • … i wiele, wiele innych

Sam skrypt budujący składa się z targetów będących w zasadzie zwykłymi funkcjami F#. Kolejność wywołania określona jest poprzez zdefinowanie zależności miedzy targetami (podobnie jak w innych narzędziach). Jeżeli skrypt nazwiemy build.fsx nie trzeba będzie podawać jego nazwy przy uruchamianiu Fake-a. Skrypt budujący Pathera wygląda następująco:

Najbardziej interesujący jest target WatchTests - obserwuje on zmiany binarki z testami i automatycznie je uruchamia. Linie 39 - 42 określają kolejność wykonania (najpierw Build potem RunTests) oraz domyślny target RunTests.

Ten post dał zgrubny pogląd dwa narzędzia wykorzystywane w Patherze. W następnym poście postaram się pokazać trochę bardziej rozbudowany skrypt budujący.

W ostanim poście pominąłem temat uzyskania adresu funkcji LoadLibrary w przestrzeni adresowej innego procesu.

Zacznijmy od odrobiny informacji teoretycznych. Adres funkcji w pamięci jest sumą dwóch wartości: adresu bazowego (ładowania) modułu (pliku wykonywalnego DLL lub EXE) oraz względnego adresu wirtualnego (RVA - Relative Virtual Address). Pierwsza z tych liczb zależna jest od systemu operacyjnego i określa pod jakim adresem zaczyna się w pamięci biblioteka DLL. Można go uzyskać poprzez standardowe funkcji WinAPI a nawet .NET. Co więcej, niektóre biblioteki systemowe (np. kernel32.dll) w każdym procesie znajdują się pod tym samym adresem. Druga potrzeba wartość - RVA - oznacza jak daleko od początku modułu znajduje się funkcja. RVA można znaleźć w nagłówku pliku wykonywalnego.

Naszym zadaniem jest znalezienie adresu funkcji LoadLibrary z kernel32.dll w pamięci jakiegoś procesu. Ponieważ kernel32.dll znajduje się zawsze pod tym samym adresem, również LoadLibrary będzie w tym samym miejscu, więc proste wywołanie GetProcAddress powinno zwrócić adres prawidłowy w dowolnym procesie. Czemu więc oddzielny post na tak proste zagadnienie?

Zadanie to byłoby proste gdybyśmy mówili o procesach działających w tej architekturze (Pather x86 - program x86 albo Pather x64 - program x64). W przypadku niezgodności sytuacja robi się zdecydowanie bardziej skomplikowana, a wynika ona z faktu, że w systemie Windows (64-bitowym) znajdują się dwie wersje kernel32.dll - \Windows\System32\kernel32.dll oraz \Windows\SysWOW64\kernel32.dll. Ta druga wykorzystywana jest w procesach 32-bitowych i nawet jeżeli znalazłby się one pod tym samym adresem ładowania to RVA funkcji LoadLibrary będzie inne. Na szczęście aplikacje .NET są niezależne od platformy, więc mogą działać z natywną architekturą, co znacząco ułatawia sprawę, gdyż procesy 32-bitowe w 64-bitowym systemie są “oszukiwane” i z ich punktu widzenia wszystko inne nadal jest 32-bitowe.

Do zdobycia RVA funkcji możemy wykorzystać bibliotekę PeNet, która pozwala na odczytanie nagłówków z plików wykonywalnych - w tym eksportowanych funkcji. Zdobycie ścieżki do pliku napotyka na pewne trudności, gdyż .NET-owa klasa Process zwróci \Windows\system32\kernel32.dll zarówno dla procesów 64- i 32-bitowych. Na ratunek przychodzi nam ToolHelp API, które to zwraca realną scieżkę do modułów procesu. Implementacja potrzebnego (dość prostego) fragmentu znajduje się na GitHubie. Przy pomocy tego API można także odczytać adres bazowy modułu, dzięki czemu mamy wszystkie fragmenty układanki. Finalny kod funkcji fundFunction przedstawia się następująco:

Uff… to był długi wstęp (5 postów!), ale dzięki niemu mam pewność, że Pather może (prawie) osiągnąć to czego od niego oczekuję. Drugim dużym zagadenieniem, któremu poświęcę kilka postów będzie parsowanie plików ze ścieżkami, ale to dopiero za jakiś czas. Następne posty będą nieco konkretniejsze.

Poprzedni post skończył sie napisaniem (nieprzechodzących) testów. Teraz nadszedł czas na implementację mechanizmu wstrzykiwania biblioteki DLL do innego procesu.

Na początek stwórzmy bibliotekę, która będzie wstrzykiwana w docelowy proces. Niestety nie możemy jej napisać w F# ponieważ musi być ona natywna (i zgodna z architekturą docelowego procesu), tak więc wykorzystamy C++. Po załadowaniu do procesu wywołana zostanie funkcja DllMain:

Zwrócenie wartości FALSE powoduje natychmiastowe wyładowanie biblioteki z pamięci. Dzięki temu nie będzie zaśmiecać docelowego procesu. Komunikacja z Patherem odbywać będzie się przy pomocy named pipe, a cała pętla obsługi znajduje się w funkcji injection:

Nazwa potoku budowana jest według wzoru: \\.\pipe\pather\<id-procesu>, a na na protokół składają sie trzy operacje: echo (kod 45), ustawianie (01) i odczytywanie (02) zmiennej środowiskowej. Na razie zostawy ich implementację i przyjrzyjmy się jak to wygląda stronie Pathera, która jest zdecydowanie bardziej złożona.

Na początek trzeba zaimportować kilka funkcji WinApi:

Wstrzykiwanie biblioteki DLL do jakiegoś procesu opiera się na utworzeniu w nim wątku (z pomocą CreateRemoteThread) z funkcją LoadLibrary jako entrypointem i ścieżką do biblioteki jako parametrem w postaci wskaźnika na łańcuch znaków zakończony znakiem 0. Wymaga to uprzedniego umieszczenia go w pamięci procesu co umożliwia para VirtualAllocEx i WriteProcessMemory. Całość rozbudowana o wybranie odpowiedniej wersji biblioteki DLL (x86 albo x64) przedstawia się następująco:

Funkcja findFunction pozwala na określenie adresu zadanej funkcji w przestrzeni adresowej docelowego procesu i będzie tematem następnego postu.

Mając mechanizm wstrzykiwania możemy obudować go w obsługę named-pipe:

Mając podstawową obsługę komunikacji z obu stron możemy zaimplementować trzy operacje:

ze strony Pathera:

oraz biblioteki DLL:

Wyjaśnienia wymagają funkcje C++ readString, readStringLength, writeString oraz writeStringLength - są to funkcje implementujące ten sam sposób kodowania łańcuchów znaków co BinaryReader i BinaryWriter. Ich implementacja jest bliźniaczo podobna do ich odpowiedników w .NET (BinaryReader.ReadString, BinaryWriter.WriteString).

Świadomie nie skupiałem się na problemach związanych z aplikacjami 32-bitowymi na 64-bitowym systemie - jedynie funkcja injectLibrary wybiera odpowiednią bibliotekę na podstawie architektury docelowego procesu. Problemy związane z WoW64 zostaną opisane w następnym poście.