|
|
-
CPAU to całkiem popularne narzędzie typu runas. Nazwa programu pochodzi od nazwy funkcji systemowej CreateProcessAsUser, z której co prawda program nie korzysta, ale jest na tyle wymowna i czytelna, że świetnie nadaje się na krótką i treściwą nazwę. Z CPAU korzystają głównie administratorzy systemów, którzy chcą umożliwić zwykłym użytkownikom uruchomienie niektórych programów w kontekście administratora, rozwiązując w ten sposób wiele różnych problemów z pracą programu w trybie zwykłego użytkownika. Zdarza się również, że CPAU wykorzystywane jest po to, aby uruchomić jakąś aplikację jako ograniczony użytkownik, dzięki czemu można uniknąć problemów z aplikacjami bezstresowo modyfikującymi system, w momencie, gdy standardowo pracujemy na koncie administracyjnym.
Jedną z kluczowych funkcjonalności CPAU jest możliwość zapisania parametrów, a w tym nazwę użytkownika oraz hasło w osobnym, zakodowanym pliku. Algorytm zastosowany do zakodowania tych informacji został stworzony przez autora aplikacji i nie jest dostępny publicznie. Dzięki temu dane są bezpieczne i firmy takie jak Novell zachęcają do korzystania z CPAU oraz plików zadań z zapisanymi parametrami do wykonywania niektórych rutynowych czynności jak np. instalacja patchy, przez użytkowników systemu.
Od jakiegoś czasu obserwuję rosnącą popularność tego narzędzia w ramach serwisu wss i pomyślałem, że warto to i owo wyjaśnić. Czy rzeczywiście dane przechowywane w plikach zadań są bezpieczne?
Wyciąganie hasła bez łamania algorytmu dekodowania, czyli uruchamiamy WinDbg
CPAU w jakiś sposób tworzy nowy proces i przekazuje do niego parametry uruchomieniowe. Krótka zabawa z Dependency Walkerem pozwala nam obstawiać funkcję CreateProcessWithLogonW z biblioteki advapi32.dll. W momencie wywoływania tej funkcji parametry do niej przekazywane muszą znajdować się już w pamięci w postaci zdekodowanej i jest to dla nas najlepszy moment na przyjrzenie się im. Poniżej spisałem kroki, które należy wykonać, żeby dojść do opisywanego momentu i sprawdzić co widać. W ramach testu sięgnąłem do wspomnianego artykułu na stronach Novella i znajdującemu się tam przykładowemu plikowi .job
- Instalujemy WinDbg.
W tym kroku pobieramy WinDbg i instalujemy. Ustawiamy podstawowe parametry takie jak serwer symboli: >set _NT_SYMBOL_PATH=srv*c:\websymbols\*http://msdl.microsoft.com/download/symbols
-
Uruchamiamy nową sesję WinDbg i ładujemy CPAU. Zakładam, że cpau znajduje się w katalogu c:\utils, a plik zadań zapisany jest w c:\temp\novell.job. Wobec tego w oknie Command wpisujemy: .create "c:\Utils\cpau -dec -file c:\temp\novell.job"
Początek zabawy z WinDbg i CPAU
Wciskamy enter i przechodzimy do kolejnego kroku
-
Ładujemy cpau i ustawiamy breakpoint na wspominaną wcześniej funkcję systemową. W tym celu w oknie poleceń wpisujemy kolejno: g bp ADVAPI32!CreateProcessWithLogonW
Ustawienie pułapki na wywołanie funkcji tworzącej nowy process
-
Pozwalamy na dalsze wykonanie programu i czekamy na naszą pułapkę. Jeszcze raz w oknie poleceń wpisujemy g i chwilę później debugger oddaje nam sterowanie. Sprawdzamy zawartość pamięci wskazywanej przez rejestr eax otwierając okno 'Memory' i wpisując @eax w polu 'Virtual'.
Podglądamy obszar pamięci wskazywanej przez zawartość rejestru eaxNaszym oczom ukazuje się hasełko - w omawianym przypadku jest to 'support' (przykład na stronie Novella błędnie sugeruje, że hasło brzmi 'password'). Funkcja CreateProcessWithLogonW zakończona jest na literkę 'W', co wskazuje na to, że jest to wersja 'szeroka', pracująca na ciągach znaków zapisanych w unikodzie, zakończonych unikodowym nullem.
Doszliśmy do momentu, w którym widzimy zawartość pamięci tuż przed uruchomieniem funkcji tworzącej nowy proces. Obok hasła możemy znaleźć inne parametry - nazwę użytkownika oraz wykonywane polecenie. Cały proces mogliśmy nieco przyśpieszyć korzystając z faktu, iż polecenia dla WinDbg można oddzielać od siebie średnikiem i pisząc je w jednej linii.
Uwagi na koniec
OK, jednym słowem każdy może odczytać hasełko zakodowane w pliku .job. Czy to znaczy, że należy rozstać się z programem i więcej z niego nie korzystać? Nie! Zabezpieczenie w postaci nic nie mówiącego łańcucha cyfr jest wystarczającym straszakiem dla każdego przeciętnego użytkownika. A i opisana wyżej metoda nie jest dla każdego do powtórzenia, więc wobec braku jawnych dekoderów plików .job, każdy administrator może się czuć bezpieczny wobec znaczącej większości użytkowników.
Przestrzegam oczywiście przed nadmiernym wykorzystywaniem CPAU i raczej unikaniem kodowania haseł takich użytkowników jak administratorzy domenowi, etc., a raczej pozostaniu przy lokalnych kontach 'supportowych', które mają wyższe od standardowych uprawnienia. Zawsze przecież może się znaleźć w sieci ktoś, dla kogo wszelkiej maści tajemnice to osobiste wyzwania i dla kogo sięgniecie po WinDbg to dwa mlaśnięcia myszą...
|
-
Kontynuując myśl napoczętą w poprzednim wpisie przeniosę się ze swoimi rozważaniami na grunt grup offline. I już na wstępie wysunę śmiałą tezę, że obecna sytuacja gospodarcza na świecie sprzyja ruchom społecznościowym, w tym grupom offline.
Pomijając moje osobiste przemyślenia dotyczące 'kryzysu', czy też 'Kryzysu', spróbuję przejść od razu do namacalno-zauważalnych skutków.
W mediach pojawiają się informacje dotyczące zamrażania rekrutacji, a także likwidacji stanowisk pracy. Wszyscy czytali o cięciach Microsoftu, choć pojawiają się również głosy optymistyczne, wedle których sektor IT w ogóle nie zostanie dotknięty przez recesję. Jestem przekonany, że rzadko kto z branży uwierzy w to, że jego nic nie dotknie, tym bardziej, że na forach pojawiają się wątki, z których jasno wynika, że nie jesteśmy grupą specjalnie uprzywilejowaną, a może tylko w nieznacznie lepszej sytuacji.
I tak jak ponad dwa lata temu mówił Peter Schiff (Gutek, dzięki za link!), ja też jestem zdania, że trzeba się z obecną sytuacją pogodzić i ją po prostu przyjąć. Rynek pracy się zepsuje, co do tego nikt chyba nie ma najmniejszych wątpliwości. A w takiej sytuacji najlepsze, co możemy zrobić, to starać się o to, aby w potencjalnej rozmowie rekrutacyjnej móc się jak najlepiej zaprezentować. Firmy się reorganizują, restrukturyzują i następują wewnętrzne ruchy robaczkowe. To jest czas na inwestowanie w siebie, poszerzanie istniejących i zdobywanie nowych umiejętności. Zarówno tych technicznych, jak i miękkich - związanych z pracą w grupie, prowadzenia prezentacji, czy sprawnego formułowania myśli na piśmie. I wracając do mojej poprzedniej notki - gdzie szybciej, taniej i skuteczniej można to osiągnąć, jak nie właśnie w ramach spotkań grup offline??? Niektórzy już to dostrzegają i coraz cieplej patrzą na swój udział w spotkaniach grup, czy zaangażowaniu się w powstanie nowych.
Trzeba pamiętać również, że grupy offline i społeczności online to miejsca, gdzie ludzie kontaktują się ze sobą i zawierają bliskie znajomości. Bardzo cenne znajomości, bo dzięki nim grupowicze poszerzają swoją sieć potencjalnych pracowników / pracodawców / współpracowników, czyli miejsca, w którym przyjdzie im żyć i funkcjonować. "Liga rządzi, liga radzi, liga nigdy cię nie zdradzi!".
I właśnie te aspekty działania grup są obecnie bardzo atrakcyjne dla każdego z branży. Dla każdego, kto zastanawia się nad swoją przyszłością.
Jednak grupy to nie tylko sami ludzie. To także cele, jakie stawiają przed sobą grupowicze do realizacji, a tu nierzadko zaczynają pojawiać się pieniądze. I w tym miejscu dotykamy kolejnej sfery związanej z działalnością grup - sponsoring.
Nie będę zbyt odkrywczy, gdy stwierdzę, że sponsorzy zdecydowanie bardziej wolą wspierać grupy swoimi produktami, niż przekazując bezpośrednio pieniądze. W czasach kryzysowych prawda ta osiąga swoje drugie i trzecie dno - następuje całkowity odpływ gotówki ze strony sponsorów. I uważam, że to również można wykorzystać z powodzeniem dla rozwoju grupy! To jest właśnie ten moment, kiedy możemy postarać się o sponsoring produktowy, który w dłuższej perspektywie może zaowocować wsparciem finansowym dla grupy. Firmy dobrze wiedzą, że dołek kiedyś się skończy i wtedy lepiej być dobrze osadzonym w środowisku, niż zaczynać wszystko od zera. A licencje na soft można wykorzystać w czasie spotkań grupy, podnosząc w ten sposób wartość takich meetingów w oczach grupowiczów.
Zamiast zapraszać na spotkanie prezentera z innego kraju i opłacać jego T&E, można pokusić się o zorganizowanie sesji live meeting, a w przyszłości poprosić o wizytę. Sponsorzy mają swoje listy wybranych ze społeczności specjalistów, których chętnie oddadzą na pożarcie grupom offline. Oczywiście swoich pracowników również chętnie podeślą - w końcu to ich najcenniejszy nabytek, który na pewno wspomni o swoich produktach.
Idąc dalej tym tokiem rozumowania, grupy powinny odpowiadać na obecne potrzeby i dać szansę nowym grupowiczom na szybkie dołączenie do społeczności i zaoferować pomoc w realizacji swojej ścieżki rozwojowej. Zarówno udostępniając możliwości prowadzenia prezentacji, jak i angażując coraz to nowe osoby w rozmaite działania grupy. To czas na rozwój grup i budowanie silnego zaplecza prelegenckiego, z którym można próbować sił na wielu różnych imprezach tak krajowych jak i zagranicznych. To czas na budowanie silnie zintegrowanej społeczności, w której "it's all about people".
Szukasz swojej grupy? Ludzi podobnych do Ciebie, z którymi chcesz pogadać, wymienić się doświadczeniami, pośmiać i zaprzyjaźnić? W tej chwili najszybciej osiągniesz swój cel odwiedzając witrynę grup offline skupionych wokół technologii firmy Microsoft.
I nie, nie jest to płatna reklama grup. Sam mam wiele zastrzeżeń do sposobu działania grup oraz udziału firmy Microsoft w całej tej zabawie, ale jedno wiem na pewno: to właśnie dzięki grupom poznałem wiele wyjątkowych osób, z którymi naprawdę dobrze spędza się czas. I nikt mi tego nie odbierze. Ludzi i wiedzy tam zdobytej.
|
-
Czy zastanawialiście się kiedykolwiek nad tym, co popchnęło was do napisania pierwszego postu na forum, pierwszego komentarza na czyimś blogu, pierwszego udziału w spotkaniu grupy offline? A co was potem podkusiło, żeby pomóc komuś rozwiązać problem odpowiadając na pytanie na forum, lub co was skłoniło do napisania pierwszej notki na blogu, poprowadzenia prezentacji na spotkaniu grupy, albo zorganizowania pierwszego spotkania grupy offline w waszym mieście? Nie, nie zamierzam szukać odpowiedzi na te pytania. Zakładam, że wszystko (albo przynajmniej część, a reszta pozostaje kwestią czasu) to macie za sobą i zastanawiacie się nad swoją obecną sytuacją: warto, czy nie?
Oczywiste jest, że z punktu widzenia aktywisty jest on do przodu. No bo przecież: - prowadząc blog rozwija swoje umiejętności pisarsko - edytorskie. Pogłębia wiedzę, zawiera wirtualne znajomości, poszerza swoje horyzonty; - odpowiadając na forum znowu poszerza swoją wiedzę, gruntuje ją. Krąg jego e-znajomych jest coraz większy. Uczy się w klarowny sposób przekazywać istotę problemu i cierpliwie odpowiada na pytania, nierzadko zadawane przez osobę, która nie wie o co pyta; - organizując życie społeczności offline rozwija swoje umiejętności interpersonalne: pracuje w grupie, współpracuje ze sponsorami oraz rozwiązuje wewnętrzne problemy i konflikty. Dodatkowo zahacza o prowadzenie finansów, marketing grupy, reklamę w serwisach społecznościowych i innych miejscach publicznych. Nie wspominając o prowadzeniu strony internetowej, mailingów i całej reszty, którą aktywista musi sobie jakoś zorganizować; - prowadząc prezentację uczy się panować nad stresem, poszerza swoją wiedzę, doskonali umiejętności wyrażania własnych myśli oraz walczy z charakterystycznym dla osób o inklinacjach technicznych introwertyzmem. Otwiera się na innych, przełyka krytykę.
A to i tak tylko część 'bonusów'. Aktywista czasem coś wygra: a to dostanie myszkę z klawiaturą, a to książkę, a to MVP...
I wszystko pięknie, tylko od czasu do czasu pracodawca, lub partner życiowy spogląda przez ramię i pyta: a co to ma wspólnego z twoją pracą, lub odpowiednio - naszym życiem? Przecież czas jaki się poświęca na tego typu działalność nie bierze się z powietrza. Suma godzin podzielonych na jedzenie, sen, pracę, zabawę z dziećmi, sprawy rodzinne, sklepy.... oraz działalność 'pro publico bono.net' jest stała i = 24h na dobę ziemską. Oczywiście, zawsze można poszukać tu i ówdzie wolnych zasobów czasowych: a to snu trochę uszczknąć, a to dzieci przegonić, a to szybciej wygenerować kawałek aplikacji...
Zaraz, zaraz! Jak to 'szybciej wygenerować kawałek aplikacji'? Czyżby aktywista był wydajniejszy od nie-aktywisty? Lub też Wydajnosc(Aktywista(t-1 dzień)) < Wydajnosc(Aktywista(t)), dla każdego t? Być może. Ale jeśli nawet tak jest, to i tak 'zaoszczędzony' czas aktywista przeznacza na samorozwój. I czy nie jest tak, że leniwy, acz cwany i inteligentny programista szybciej znajdzie sposób na przyspieszenie sobie pracy, niż 'aktywista', który co i rusz będzie szukał 'lepszych i jeszcze wspanialszych' rozwiązań? Lenistwo motorem postępu?
W kategoriach ogólnoludzkich aktywista zawsze jest wygrany. Dzięki jego pomocy x osób rozwiązało swoje problemy. Dzięki zorganizowanym przez niego spotkaniom y osób wymieniło się doświadczeniami i być może rozpoczęło współpracę zawodową. Zmienił nieodwracalnie otaczający go świat. Zrobił duży krok w nieznane, gdzie przyszłość jest znacznie słabiej przewidywalna.
Spytacie: po co o tym piszę? Ano kategorie ogólnoludzkie nie wystarczają. Jeszcze pare miesięcy temu Gael Fraiteur próbował wespół z Mauricem de Beijer przekonać mnie, że organizowanie darmowych imprez nie ma większego sensu (Maurice pomaga przy organizacji SDE i SDC). Znam wiele osób, które koszta osobiste związane z działalnością pro publico bono musiały położyć na szali razem z potencjalnymi zyskami, czy to wynikającymi z potrzeb altruistycznych, czy to zimnej kalkulacji potencjalnych zysków i niestety... zakończyły, lub zamierzają zakończyć swoją działalność 'charytatywną'. 'Dlaczego?' - spytacie. Odpowiedź nie jest prosta, a wręcz bardzo złożona i często indywidualna. Wypalili się, dostali po plecach, odechciało im się, przenieśli aktywność na inny poziom, lub przedmiot zainteresowań... ale też zmienili swoje priorytety życiowe, oddali pole innym... co przypadek, to przyczyna. Statystycznie tylko 1% populacji jest aktywny. Reszta - nie. I bez współpracy aktywistów, studnia z nimi szybko wyschnie. Niepokoi mnie ten ruch odpływowy, choć nie wszystko rozumiem i wiem. Wiem jednak, że w światku aktywistów, który znam, brakuje 'superintegratora', osoby, za którą wszyscy pójdą i dzięki której cała działalność będzie miała większy wykop i sens. A może nie jest potrzebna taka osoba?
Zatem - być czy nie być aktywistą? Hę? Jak widzicie, ja wybrałem :)
|
-
W swoich potyczkach dosyć często mam do czynienia z COM-ami, przy tworzeniu instancji których równie często sięgam po Activator.CreateInstance. I pewnie nie byłoby w tym nic odkrywczego ani niezwykłego, gdyby nie pewna obserwacja.
Weźmy dla przykładu kawałek kodu, w którym tworzymy instancję klasy TestClass korzystając z wersji genericsowej i zwykłej:
class TestClass { public TestClass() { } }
class Program { const Int32 CYCLES = 100000; static void Main(string[] args) { CreateInstanceGenericTest(); CreateInstanceNonGenericTest(); }
private static void CreateInstanceNonGenericTest() { using (OperationTimer ot = new OperationTimer("CreateInstance NON Generic")) { TestClass t = null; for (int i = 0; i < CYCLES; i++) { t = (TestClass)Activator.CreateInstance(typeof(TestClass)); } } }
private static void CreateInstanceGenericTest() { using (OperationTimer ot = new OperationTimer("CreateInstance Generic")) { TestClass t = null; for (int i = 0; i < CYCLES; i++) { t = (TestClass)Activator.CreateInstance<TestClass>(); //jedyna różnica } } } }
po wykonaniu którego dostajemy czasy:
,375 seconds CreateInstance Generic ,574 seconds CreateInstance NON Generic
Czyli wszystko mniej-więcej zgodnie z oczekiwaniami. Wersja genericsowa nieznacznie sprawniejsza (powiedzmy dwukrotnie).
Dokonajmy jednak prostej modyfikacji. Zmieńmy mianowicie modyfikator dostępu dla klasy TestClass z internal na public, pozostawiając resztę bez zmian:
public class TestClass { public TestClass() { } }
a otrzymamy następujące czasy:
,372 seconds CreateInstance Generic ,035 seconds CreateInstance NON Generic
Jak widać powyższa zmiana zupełnie nie wpłynęła na wersję genericsową, natomiast spowodowała prawie 20x przyspieszenie wersji niegenericsowej, wobec czego wersja nie używająca generics jest teraz o rząd wielkości szybsza od odpowiednika genericsowego. Wow!
Ale o so chodzi?! :) Mam nadzieję, że ktoś wie jaka idea przyświecała takiemu rozróżnieniu i wytłumaczy mi to łopatologicznie :)
PS. Pomocnicza klasa OperationTimer mierząca czas wykonania kodu wewnątrz bloku using jest żywcem ściągnięta z książki 'CLR via C#' Jeffreya Richtera i opiera się na klasie Stopwatch z System.Diagnostics.
PS2. W przypadku zagnieżdżenia klasy TestClass niezależnie od wybranego modyfikatora dostępu wyniki są takie jak w przypadku pierwszym, czyli wersja genericsowa szybsza.
|
-
W tych ciężkich czasach każdy stara się liczyć każdy grosik, bo nigdy nie wiadomo, kiedy dopadną go czarne macki kryzysu. Jednak to również dobry czas, żeby inwestować we własną markę i myśleć o przyszłości, w której rozpoznawalność i pozytywna aura mogą mieć decydujące znaczenie.
Zachęcam wszystkie firmy oscylujące wokół technologii microsoftowych do wspierania inicjatyw typu konferencja C2C, na którą trafia wyjątkowa grupa pasjonatów i entuzjastów wspomnianych technologii. Niewielka - relatywnie - suma wydana na wsparcie cateringu, czy nadruku koszulek z logiem firmy i konferencji, lub pozwalająca na zarejestrowanie webcastu z sesji, to w miarę prosty sposób, żeby zaistnieć w umysłach wielu osób jako patron społeczności oraz dbający o rozwój i wiedzę pracodawca. Doświadczenia grup offline pokazują, że odwiedzający je członkowie nierzadko szukają nowej pracy poprzez znajomości zawarte na grupie, trafiając często do firm o rozpoznanej nazwie.
A zatem - jeśli chcesz, by logo Twojej firmy znajdowało się obok nazwisk tak znanych postaci, jak Udi Dahan, Julia Lerman, czy Ingo Rammer, które doskonale się indeksują i szybko prowadzą na stronę C2C, lub też chcesz, aby niemal cała .NETowa społeczność w Polsce nosiła koszulki z Twoim logo, a po niezwykle udanej konferencji uczestnicy zaśpiewali 'Niech żyje nam <Twoja firma>!!!', chwaląc pod niebiosa Twój wkład w to niezwykłe wydarzenie, to naprawdę nic prostszego!
Wystarczy jeden mały kroczek, a Aneta i Arek na pewno odpowiedzą na wszystkie pytania i rozwieją wszelkie wątpliwości! :)
Jeśli jeszcze się wahasz, to przeczytaj list intencyjny Microsoftu dla C2C. Potem zajrzyj na stale uzupełnianą stronę z sesjami oraz zapoznaj się z ubiegłoroczną edycją oraz echem, jakim konferencja odbiła się po polskich społecznościach microsoftowych.
Porozmawiaj ze swoimi kolegami z pracy, uderzcie potem wspólnie do swoich przełożonych - przecież Ty też możesz zrobić coś pozytywnego i pomóc organizatorom C2C, a nic nie ryzykujesz! Jeśli nie musisz do nikogo uderzać, bo sam podejmujesz istotne decyzje - zastanów się nad przyszłością swojej firmy, może ten ruch zaowocuje w sposób dla Ciebie nieprzewidywalny, a na pewno pozytywny, przy niewielkim koszcie.
|
-
Kilka rzeczy wydarzyło się w ostatnim czasie. Kolejne właśnie się dzieją, a jeszcze następne zbliżają się wielkimi krokami. Jedziemy! UPDATED!
- Początek stycznia. Nominacje i renominacje MVP.
Do programu dołączyli Szymon Kobalczyk, Damian Widera i Marcin Goł. Fajnie znać chłopaków osobiście :). Pierwszy to szwagier Marcina, blogera zinowego. Drugi - bez żadnych koligacji rodzinnych ze znanymi mi blogerami, po prostu sam jest blogerem na zinie :D. Trzeci - lider warszawskiej grupy PLSSUG, którą prowadzi po innym blogerze zinowym, Pawle ;). No i renominacje: razem z innymi chłopakami z zina (poza Mateuszem Kierepką, autorem tekstów do obu numerów zina): Procentem, Gutkiem i Kubą Binkowskim, dostaliśmy pozytywne cenzurki i jedziemy następny rok na tym samym wózku.
- Konferencje: VCL, 4Developers, C2C, TechFest.
VCL: to tak naprawdę pierwsza całodniowa konferencja organizowana siłami warszawskich grup ITPro, z dużym zaangażowaniem ze strony wielu osób (w tym m.in. Łukasza Foksa, z-ce rednacza WSS oraz lidera WGUiSW). Gdybym miał więcej wolnych dni, to na pewno bym się tam pojawił (impreza odbędzie się we wtorek, 17 lutego).
4Developers: organizowana w Krakowie przez fundację PROIDEA, z patronatem KGD, to miejsce, gdzie będzie można posłuchać o ciekawych rzeczach. A o ciekawych rzeczach mówią ciekawi ludzie. Polecam oczywiście prezentacje chłopaków z zina: Szymona i Marcina, a na dokładkę Mateusza. Przy okazji warto zajrzeć na prezentację Basi Fusińskiej, która dzielnie walczyła w konkursie SI w ramach ścieżki .NET C2C. Dodatkowo polecam prezentację (a przynajmniej zajrzenie na nią) niezwykle charyzmatycznego Jacka Laskowskiego, którego można złapać w czasie spotkań WJUG. Poza Jackiem swoją prezentację będzie miał również inny waleczny WJUG-owicz - Waldemar Kot.
C2C: organizowana siłami wielu grup offline. Niektórzy z nich, to blogerzy zina, z czego na główną uwagę zasługuje Arek, który zdaje się pociąga większość sznurków przy organizacji tej imprezy. W drugiej połowie stycznia zorganizowany został konkurs Speaker Idol, w którym w szrankach stanęli m.in. wspomnieni przy okazji 4Developers: Szymon i Basia, a który wygrał Piotrek Leszczyński - najnowsza postać na zinie. Poza Piotrkiem swoją prezentację będzie miał jeszcze jeden bloger zina: Marek, a to i tak tylko odnośnie ścieżki .NET. I pewnie pojawiłoby się na ścieżce .NET więcej prelegentów zina, ale przecież trzeba dać dojść do głosu gwiazdom zagranicznym, które w tym roku będą błyszczeć szczególnie jasnym blaskiem w czasie konferencji!!! W ramach ścieżki SQL pojawi się m.in. Marek, wymiatacz bazodanowy z zina, w niektórych momentach posiłkujący się językiem rosyjskim w stopniu dla mnie niepojętym. Poza nim zdaje się jeszcze jedna z postaci z zina, ale to chyba nic oficjalnego i lepiej nic więcej nie będę pisał.
TechFest: Michał Jagieła szykuje imprezę z ogromnym rozmachem i szczerze trzymam kciuki za powodzenie tego wydarzenia. Pojawi się tam plejada polskich prelegentów (w tym również blogerów z zina ;)), ale także zagraniczni goście, żeby wspomnieć tylko o sławnym i równie kontrowersyjnym Ayende Rahien.
- Krążą plotki, że w okolicach początku marca po Polsce będzie się tułał niejaki Raymond Lewallen. Zawita do kilku miast:
Kraków - sobota, 7 marca Katowice - poniedzialek Warszawa - wtorek Poznań - środa Toruń - czwartek Gdańsk - piątek gdzie na pewno opowie coś ciekawego. Co? A o tym możecie zdecydować, odwiedzając stronę swojej grupy na ms-groups.pl i głośno krzycząc o czym chcielibyście posłuchać. Ugośćcie strudzonego wędrowcę, a odpłaci Wam się pięknym za nadobne :). Nie jest wielką tajemnicą, że zine.net wzoruje się w dużej mierze na codebetter.com i IMO wizyta każdego z bloggerów tego wyjątkowego sajtu to niewątpliwe wydarzenie, na którym warto być.
- Ziemek Skowroński, barwna postać developers.pl oraz zina, po moich marudzeniach, a pewnie i nadejściu własnej potrzeby integracji z innymi podobnymi sobie, wziął się za grupy offline. I tak, jeśli będziecie 26 lutego w okolicach Gloucester (czyt. Gloster), to zajrzyjcie koło 18.00 w to przytulne miejsce, a będziecie świadkami inauguracji grupy GL.NET, prowadzonej właśnie przez Ziemka. Zapowiadają się ciekawe prezentacje, więc jak tylko możesz, to po prostu wpadnij.
- ITBlogs.pl
Niezwykle ciekawa i warta szerszego rozpropagowania inicjatywa, prowadzona przez Karola Stilgera oraz innych znanych itprosów. Trzymam kciuki i życzę sukcesu! Już jest tam parę fajnych blogów, w tym Bloggers Underground, blog imprezy około konferencyjnej :). Bracia i siostry itprosi, łączcie się na ITBlogs.pl!!! :)
- WG.NET
Wciągam się ponownie w Warszawską Grupę .NET. Liczę na to, że wspólnie z Arkiem oraz innymi chłopakami z zina (ale nie tylko!!!) zrobimy parę fajnych rzeczy i to nie wysiłkiem jednej osoby. To jednak temat na osobny post i być może szerszą dyskusję.
- Rok temu się nie udało, ale w tym roku wybieram się na MVP Global Summit, gdzie mam nadzieję zahaczę o
 i liczę na kilka pogaduch z różnymi ludźmi. Być może uda się załatwić jakąś fajną sesję LM dla wg.net, albo prelegenta na jakąś konferencję? Zobaczymy... Jak macie jakieś pytania, lub sugestie, które chcielibyście przekazać któremuś z MVP, którzy licznie przybywają na Summit, to chętnie przekażę :)
To tyle z 'nowości', zdecydowanie wartych moim zdaniem dostrzeżenia. Na pewno coś pominąłem, ale liczę na to, że znajdzie się dobra dusza, która mi to wskaże ;)
|
-
Tekst ten dedykuję Grzesiowi Tworkowi i Pauli Januszkiewicz, których tegoroczna sesja na MTS natchnęła mnie do tego, żeby opisać parę poruszonych tam tematów na blogu.
Na pewno wielokrotnie zastanawialiście się nad tym, w jaki sposób zapisać jakąś tajemną informację tak, żeby nikt niepowołany nie mógł się do niej dobrać. Weźmy dla przykładu automatyczne logowanie do systemu.
Winlogon jakiego nie znamy?
W ramach swojego wsparcia technicznego, Microsoft proponuje rozwiązanie polegające na edycji zawartości klucza rejestru, a mianowicie HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon i ustawieniu:
- DefaultUserName - nazwa użytkownika, dla którego ma zachodzić automatyczne logowanie;
- DefaultPassword - hasełko użytkownika wpisanego w DefaultUserName;
- AutoAdminLogon - 1 oznacza, że ma zachodzić automatyczne logowanie, 0 - nie.
I wszystko w porządku, gdyby nie to, że hasełko w DefaultPassword jest wpisane jawnym tekstem i dostępne bez większego problemu dla każdego, kto może odczytać zawartość tego klucza. Bezpieczeństwo takiego rozwiązania pozostawię każdemu do samodzielnej oceny. Moment! - ktoś może powiedzieć - przecież jest bezpieczniejsze rozwiązanie!
A i owszem!
Podczas swojego startu, proces Winlogon sprawdza zawartość wspomnianego wyżej klucza i na jego podstawie podejmuje decyzję o automatycznym logowaniu. Jednak chwilę wcześniej Winlogon sięga do LSA po DefaultPassword i jeśli coś dostanie w odpowiedzi, to nie omieszka z tego skorzystać, zamiast wpisu w rejestrze. Prawdę powiedziawszy, Microsoft zaleca tę właśnie metodę jako sposób ochrony hasła DefaultPassword.
LsaStorePrivateData - odsłona pierwsza
Jednak co to znaczy, że Winlogon sięga do LSA po DefaultPassword? Otóż upraszczając, wywoływana jest funkcja LsaRetrievePrivateData z biblioteki advapi32.dll z próbą odczytania wartości Sekretu dla klucza 'DefaultPassword'.
Sekretu? A cóż to takiego???
Podsystem Lsass udostępnia mechanizm umożliwiający zapisywanie wrażliwych danych do 'zasobnika LSA' w postaci zaszyfrowanej oraz na żądanie dostęp do tych danych. I tak: żeby zapisać interesujące nas dane, wystarczy użyć funkcji LsaStorePrivateData, gdzie w postaci parametrów przekazujemy parę klucz - wartość, czyli np. 'DefaultPassword' i 'Pa$$w0rd', natomiast do wyłuskania zapisanych danych należy skorzystać ze wspomnianej wcześniej funkcji LsaRetrievePrivateData z parametrem określającym klucz. Microsoft podzielił dane, które możemy w ten sposób zapisywać na 4 grupy:
- dane lokalne - nazwy z tej grupy rozpoczynają się prefiksem 'L$'. Dane lokalne można odczytać tylko na komputerze, na którym te dane są przechowywane. Dodatkowo wpadają tu dane, których nazwy zaczynają się od '$machine.acc', 'SAC', 'SAI', 'SANSC', żeby wymienić tylko kilka;
- dane globalne - tu nazwy rozpoczynają się od prefiksu 'G$'. Dane globalne utworzone na kontrolerze domeny są replikowane na pozostałe kontrolery domeny;
- dane komputera - ich nazwy rozpoczynają się od 'M$'. Zgodnie z dokumentacją dostęp do nich ma wyłącznie system operacyjny. Do tej grupy trafiają również nazwy zaczynające się od 'NL$', lub '_SC_';
- dane prywatne - te nie wymagają żadnego przedrostka. Są dostępne z zewnętrznych komputerów, ale nie są replikowane w domenie.
Warto zwrócić uwagę na dane komputera (machine data), które możemy zapisywać korzystając z LsaStorePrivateData, natomiast nie możemy odczytywać przy wykorzystaniu LsaRetrievePrivateData.
Poniższe fragmenty kodu pokazują, jak korzystać z obu funkcji przy użyciu P/Invoke. Jest to właściwie przeniesienie przykładowego kodu do zmiany DefaultPassword do środowiska .NET. Można również skorzystać z przykładowego kodu do funkcji LsaRetrievePrivateData w ramach pinvoke.net.
public static string RetrieveData(string secretName) { string secretValue = ""; long retcode = 0; IntPtr zero = Marshal.AllocHGlobal(0); Win32.LSA_UNICODE_STRING systemName = new Win32.LSA_UNICODE_STRING(); IntPtr policyHandle = IntPtr.Zero; Win32.LSA_OBJECT_ATTRIBUTES objectAttributes = new Win32.LSA_OBJECT_ATTRIBUTES(); objectAttributes.Length = 0; objectAttributes.RootDirectory = IntPtr.Zero; objectAttributes.Attributes = 0; objectAttributes.SecurityDescriptor = IntPtr.Zero; objectAttributes.SecurityQualityOfService = IntPtr.Zero; retcode = Win32.LsaNtStatusToWinError(Win32.LsaOpenPolicy(ref systemName, ref objectAttributes, (int) Win32.LSA_AccessPolicy.POLICY_CREATE_SECRET, out policyHandle)); if (retcode == 0) { IntPtr secretData; Win32.LSA_UNICODE_STRING[] lsa_unicode_stringArray = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() }; lsa_unicode_stringArray[0].Buffer = Marshal.StringToHGlobalUni(secretName); lsa_unicode_stringArray[0].Length = (ushort)(secretName.Length * 2); lsa_unicode_stringArray[0].MaximumLength = (ushort)((secretName.Length + 1) * 2); Win32.LsaRetrievePrivateData(policyHandle, lsa_unicode_stringArray, out secretData); if (secretData != IntPtr.Zero) { Win32.LSA_UNICODE_STRING lsa_unicode_string2 = (Win32.LSA_UNICODE_STRING)Marshal.PtrToStructure(secretData, typeof(Win32.LSA_UNICODE_STRING)); secretValue = Marshal.PtrToStringAuto(lsa_unicode_string2.Buffer); } Win32.LsaClose(policyHandle); } Win32.FreeSid(zero); return secretValue; }
public static long StoreData(string secretName, string secretData) { long retcode = 0; IntPtr zero = Marshal.AllocHGlobal(0); Win32.LSA_UNICODE_STRING systemName = new Win32.LSA_UNICODE_STRING(); IntPtr policyHandle = IntPtr.Zero; Win32.LSA_OBJECT_ATTRIBUTES objectAttributes = new Win32.LSA_OBJECT_ATTRIBUTES(); objectAttributes.Length = 0; objectAttributes.RootDirectory = IntPtr.Zero; objectAttributes.Attributes = 0; objectAttributes.SecurityDescriptor = IntPtr.Zero; objectAttributes.SecurityQualityOfService = IntPtr.Zero; retcode = Win32.LsaNtStatusToWinError(Win32.LsaOpenPolicy(ref systemName, ref objectAttributes, (int) Win32.LSA_AccessPolicy.POLICY_CREATE_SECRET, out policyHandle)); if (retcode == 0) { Win32.LSA_UNICODE_STRING[] lsa_unicode_stringArray = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() }; lsa_unicode_stringArray[0].Buffer = Marshal.StringToHGlobalUni(secretName); lsa_unicode_stringArray[0].Length = (ushort)(secretName.Length * 2); lsa_unicode_stringArray[0].MaximumLength = (ushort)((secretName.Length + 1) * 2); Win32.LSA_UNICODE_STRING[] privateData = new Win32.LSA_UNICODE_STRING[] { new Win32.LSA_UNICODE_STRING() }; privateData[0].Buffer = Marshal.StringToHGlobalUni(secretData); privateData[0].Length = (ushort)(secretData.Length * 2); privateData[0].MaximumLength = (ushort)((secretData.Length + 1) * 2); Win32.LsaStorePrivateData(policyHandle, lsa_unicode_stringArray, privateData); Win32.LsaClose(policyHandle); } Win32.FreeSid(zero); return retcode; }
Do tego niezbędne importy na potrzeby P/Invoke
internal static class Win32 { [DllImport("advapi32")] public static extern IntPtr FreeSid(IntPtr pSid); [DllImport("advapi32.dll")] public static extern uint LsaClose(IntPtr ObjectHandle); [DllImport("advapi32.dll")] public static extern uint LsaNtStatusToWinError(uint status); [DllImport("advapi32.dll")] public static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, int DesiredAccess, out IntPtr PolicyHandle); [DllImport("advapi32.dll")] public static extern uint LsaRetrievePrivateData(IntPtr PolicyHandle, LSA_UNICODE_STRING[] KeyName, out IntPtr PrivateData); [DllImport("advapi32.dll", SetLastError = true)] public static extern uint LsaStorePrivateData(IntPtr PolicyHandle, LSA_UNICODE_STRING[] KeyName, LSA_UNICODE_STRING[] PrivateData);
public enum LSA_AccessPolicy : long { POLICY_AUDIT_LOG_ADMIN = 0x200L, POLICY_CREATE_ACCOUNT = 0x10L, POLICY_CREATE_PRIVILEGE = 0x40L, POLICY_CREATE_SECRET = 0x20L, POLICY_GET_PRIVATE_INFORMATION = 4L, POLICY_LOOKUP_NAMES = 0x800L, POLICY_NOTIFICATION = 0x1000L, POLICY_SERVER_ADMIN = 0x400L, POLICY_SET_AUDIT_REQUIREMENTS = 0x100L, POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x80L, POLICY_TRUST_ADMIN = 8L, POLICY_VIEW_AUDIT_INFORMATION = 2L, POLICY_VIEW_LOCAL_INFORMATION = 1L }
[StructLayout(LayoutKind.Sequential)] public struct LSA_OBJECT_ATTRIBUTES { public int Length; public IntPtr RootDirectory; public Win32.LSA_UNICODE_STRING ObjectName; public uint Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; }
[StructLayout(LayoutKind.Sequential)] public struct LSA_UNICODE_STRING { public ushort Length; public ushort MaximumLength; public IntPtr Buffer; } }
I przykładowe użycie może wyglądać następująco:
static void Main(string[] args) { StoreData("alamakota", "ala ma kota"); string s = RetrieveData("alamakota"); Console.WriteLine(s); }
Uzbrojeni w maszynkę do odczytywania i zapisywania sekretów możemy już swobodnie ustawiać sobie DefaultPassword i usunąć odpowiedni element w kluczu Winlogon w rejestrze. Nareszcie bezpieczni!
Lsa(Store | Retrieve)PrivateData internals
Po pierwszej euforii nadchodzi chwila refleksji: 'A gdzie właściwie zapisywane są te dane?'. W dokumentacji do obu funkcji, Microsoft twierdzi, że:
"The data stored by the LsaStorePrivateData function is not absolutely protected. However, the data is encrypted before being stored, and the key has a DACL that allows only the creator and administrators to read the data."
Z jednej strony mamy zaufanie do systemu, że zadba o nasze dane, z drugiej - okazuje się, że wcale nie jest tak różowo. Otóż okazuje się, że dane przekazane przez LSASS zapisywane są w rejestrze, w gałęzi HKLM\Security\Policy\Secrets. Standardowo tylko system ma dostęp do tego klucza, więc jeśli chcecie się do niego dobrać, to musicie to zrobić w kontekście konta systemowego. W systemach przed Vistą najprościej skorzystać z polecenia systemowego at:
>at 10:23 /interactive regedit.exe
albo, już niezależnie od systemu i jeszcze prościej, z sysinternalsowego psexec:
>psexec -i -s regedit.exe
Przykładowa zawartość sekretu zapisanego wcześniej opisanymi metodami wygląda tak, jak to przedstawia Rysunek 1.

Rysunek 1. Przykładowy sekret :)
Przeglądając zawartość klucza HKLM\SECURITY\Policy\Secrets możemy znaleźć wiele podkluczy, których nazwy zaczynają się od L$, M$, ale także w formacie SCM:{GUID}, o których nie będę się rozpisywał. Naszą uwagę przykują jednak klucze o nazwach zaczynających się na _SC_.
Sekrety usług
Jeśli postanowimy, żeby któraś z usług uruchamiana była z konta innego niż systemowe, to zostaniemy poproszeni o wprowadzenie nazwy użytkownika i hasła, w ramach którego usługa ma startować. Jest to jednorazowa czynność i musimy tylko pamiętać o tym, że gdy zmienimy hasło dla tego użytkownika, to musimy ponownie odwiedzić zakładkę 'Logowanie', gdzie ponownie będziemy musieli wprowadzić nasze dane. Zapewne zastanawialiście się kiedyś, gdzie przechowywane są te informacje i czy można się do nich jakoś dobrać. No właśnie, ciekawe, nie? :)
Weźmy dla przykładu usługę Telnet (jeśli jeszcze ją mamy w systemie, a w przypadku braku, możemy wybrać sobie inną ofiarę, której zepsucie nic nam nie zaszkodzi ;)) i ustawmy logowanie z dowolnego konta.

Rysunek 2. Zakładka logowanie dla usługi Telnet.
Zajrzyjmy teraz na chwilę do książki Marka Russinovicha i Davida Solomona o internalsach windowsów, a dokładniej do rozdziału poświęconemu uruchamianiu usług. Interesuje nas w szczególności fragment:
"SCM [Service Control Manager] zapisuje dane o usługach uruchomionych na koncie innym niż systemowe poprzez wywoływanie funkcji Lsass LsaLogonUser. Funkcja LsaLogonUser standardowo wymaga hasła, ale SCM sygnalizuje dla Lsass, że hasło jest przechowywane 'sekretnie' w kluczu HKLM\SECURITY\Policy\Secrets w rejestrze. [..] Kiedy SCM wywołuje LsaLogonUser, ukreśla typ logowania jako logowanie usługi, tak by Lsass szukał hasła w podkluczu Secrets o nazwie _SC_<nazwa usługi>. SCM poleca Lsass, by ten przechowywał hasło logowania w sekrecie, podczas gdy SCP konfiguruje informacje logowania usługi." [2]
Aha! Tu Cię mamy!
Mr Jingle podpowiada mi w tym momencie, że czuje już zapach hasła.
Po modyfikacji ustawień logowania dla usługi Telnet, w sekretach w rejestrze pojawił się dodatkowy podklucz, _SC_TlntSvr. To by potwierdzało słowa Wielkich Autorów, więc nie czekając długo na dalsze zachęty, łapię szybko za kod do wydłubywania sekretów i dopisuje w mainie krótką linijkę, dla testu:
static void Main(string[] args) { string s = RetrieveData("_SC_TlntSvr"); Console.WriteLine(s); }
Kompiluję, uruchamiam i... nic :(. Po prostu pusta linia. No tak, przecież sekrety zaczynające się od _SC_ należą do kategorii systemowych, a te można tylko zapisywać, odczytywanie nie działa. Upewniam się jeszcze, czy sam mogę utworzyć sekret systemowy i później go odczytać:
static void Main(string[] args) { StoreData("M$alamakota", "ala ma kota"); string s = RetrieveData("M$alamakota"); Console.WriteLine(s); }
i oczywiście znowu puściutko. Dla pewności sprawdzam jeszcze wcześniejszy kod i przy zapisie do 'alamakota', odczyt działa prawidłowo. A więc tak, jest bezpiecznie!
W tym momencie Mr Jingle trąca mnie nosem. 'Nie, to nie może być takie proste!' - odpowiadam. Sięgam jednak po otwarty regedit, eksportuję do pliku zawartość klucza HKLM\SECURITY\Policy\Secrets\_SC_TlntSvr\CurrVal, zmieniam '_SC_TlntSvr' na 'alamakota' i importuję. Potem uruchamiam wcześniej przygotowany kod i ... na konsoli wypisuje się hasełko, wklepane wcześniej sumiennie w oknie ustawień logowania usługi Telnet.
Uwagi końcowe
Podsystem LSA umożliwia zapisanie zaledwie 4096 sekretów, z czego połowa zarezerwowana jest dla systemu operacyjnego. Przechowywanie sekretów z udziałem LSA dostępne jest od Windows NT 4.0 i obecnie Microsoft nie zaleca korzystania z opisanych metod do zapisywania swoich sekretów. Począwszy od Windows 2000 dostępny jest interfejs ochrony danych DPAPI z metodami CryptProtectData i CryptUnprotectData, jednak w tym przypadku to programista musi zadbać o przechowywanie wrażliwych danych, co - jak widać - może być jednak lepszym rozwiązaniem. Trzeba również pamiętać, że większość z opisanych metod wymaga odpowiednich przywilejów, dostępnych praktycznie rzecz biorąc wyłącznie dla administratorów systemów.
Z oczywistych względów nie udostępniam gotowego narzędzia, a jedynie opisuję technikę.
[Edit: Postanowiłem jednak udostępnić prostą wersję programu, ktory wykonuje opisane w tekście czynności. Wszystko oczywiście wyłącznie do celów 'edukacyjnych'. W przyszłości zamierzam rozwijać to narzędzie o kolejne elementy, które postaram się sumiennie opisywać na blogu. Program wymaga zainstalowanego .NET Framework w wersji 2.0 oraz uruchamianie z konta systemowego.]
Od dawien dawna dostępne jest narzędzie, które wyłuskiwało sekrety systemowe korzystając z dll injection, jednak od pewnego czasu ta technika zastosowana do Lsass powoduje załamanie tego procesu i wymusza reset komputera.
Sprawdziłem opisaną metodę na Windows 2000 Server, Windows XP oraz Windows Vista, wszędzie z tym samym rezultatem.
Źródła (podaję polskie tłumaczenia, dostępne jeszcze na rynku - są tańsze od oryginałów i znośnie przetłumaczone):
1. 'Bezpieczny kod. Tworzenie i zastosowanie' - Michael Howard i David LeBlanc. (Ech, czas kupić kolejne wydanie tej rewelacyjnej książki!) 2. 'Microsoft Windows 2000 od środka' - David A. Solomon, Mark E. Russinovich. (Tu też już jest kolejne wydanie i naprawdę nie wiem, czemu go jeszcze nie mam w swojej biblioteczce!)
|
-
Tak, to już dziś! Spotkanie ostatnich sprawiedliwych w jaskini zła, a właściwie w knajpie wszelkiego dobra, czyli... spytajcie Karola gdzie :P.
A kogóż tam licho zagna? Ano będą chłopcy stojący po czarnej stronie mocy (pijący na codzień mocną czarną cavę), będzie siedmiu wspaniałych z zina, będą też MS-ITProsi i czort wie, kto jeszcze! Sporo luda z grup offline, małżeństwo graczy w 'Go, yoshi, go!', więc miałoby tam zabraknąć mnie? Ależ skąd! No więc 'wepchłem' się na krzywy ryj i ja!
Do zobaczenia na leżakach ;)
PS. Tsiii... wezmę 'ukrytą kamerę', to zobaczycie jak wyglądają rycerze Jedi po kilku głębszych... prezentacjach ;)
|
-
Zainspirowany małą pogaduchą z Pawłem opartą na cytatach z 'Matrixa', pomyślałem, że fajnie będzie rzucić parę z nich na blog - a nuż komuś jeszcze przyjdzie coś fajnego do łebka ;)
Morpheus: .NET is a system, Neo. That system is our enemy. But when you're inside, you look around, what do you see? Businessmen, teachers, lawyers, carpenters. The very minds of the people we are trying to save. But until we do, these people are still a part of that system and that makes them our enemy. You have to understand, most of these people are not ready to be unplugged. And many of them are so inert, so hopelessly dependent on the system, that they will fight to protect it.
Neo: Why do SQL CLR hurt? Morpheus: You've never used them before.
Neo: What are you trying to tell me? That I can write .NET code? Morpheus: No, Neo. I'm trying to tell you that when you're ready, you won't have to.
Morpheus: Unfortunately, no one can be told what the .NET is. You have to see it for yourself.
Żeby nie psuć zabawy, zostawiam na razie tyle. Może ktoś dorzuci coś od siebie w komentarzach? ;)
Cytaty na podstawie strony miłośników filmu.
|
-
Poniższe zdjęcie powinno być wystarczającą rekomendacją dla wszystkich tych, którzy do dziś uważali, że to strata czasu i pieniędzy.

Asia zachwycona balonem na ciepłe powietrze, czyli workiem na śmieci, nadmuchanym suszarką do włosów i unoszącym się wiele metrów popod kopułę Wydziału Fizyki PW :)
Festiwal odbył się w Warszawie między 19 a 28 września, na terenie wielu uczelni warszawskich, i nie tylko tam. Ja załapałem się z moim najstarszym dzieckiem na ostatni weekend festiwalu i zajrzeliśmy tylko na Wydział Fizyki PW. Z początku myślałem, że nic z tego nie będzie. Fizyka dla trzylatka? Sam od lat interesuję się naukami przyrodniczymi, ale trzylatek to prawdziwe wyzwanie! Uspokoiłem się jednak już po paru minutach. Toczenie koła, malowanie rąk, ‘lewitujący’ pojazd na torze eliptycznym, kula ‘z błyskawicami’, przelewanie wody z kubeczka do kuwety, latające piłeczki do ping-ponga - było wszystko to, co musiało być i znacznie więcej. Po dwóch godzinach przechodzenia po tunelach z lustrami, wycinaniu własnej lupy, graniu na flecie, etc. wróciliśmy do domu z zupełnie nowymi doświadczeniami. I nawet nie chodziło o to, żeby czegokolwiek się nauczyć, czy zapamiętać. Na to jeszcze będzie czas. Ciekawość świata, zabawa ‘naukowa’ z rówieśnikami, przyzwyczajenie się do istnienia ‘świątyń nauki’ - to było to, na czym przede wszystkim mi zależało. Pierwszy krok już został wykonany, czas robić następne.
- Czy takie festiwale są potrzebne? Też pytanie!
- Czy odwiedzimy w przyszłym roku kolejną edycję? Oczywiście!
|
-
Co należy zrobić, żeby zostać MVP? Należy blogować na zine.net.pl!!! ;) Żarty na bok, a teraz wielki szacun dla jednego z największych stachanowców grup offline – Marcina Celeja!!! Tak, Marcin po kilku latach pracy organicznej w grupie KGD oraz aktywnemu wsparciu dla grup offline w całej Polsce, nareszcie został doceniony przez korporację i dołączył do grona polskich MVP.
GRATULUJĘ!!!
Kim jest Marcin? To jeden z najsympatyczniejszych i najbardziej otwartych ludzi, z jakimi przyszło mi kiedykolwiek kontaktować się. Swoim rozbrajającym uśmiechem przypomina mi mojego najmłodszego synka, któremu niczego nie mogę odmówić :). Marcin potrafi jak rzadko kto zjednywać sobie ludzi, do tego jest entuzjastą, który zaraża swoją pasją wszystkich wokół. Gdy dowiedziałem się, że Marcin będzie reprezentował KGD na C2C, to byłem spokojny o sesję i jej odbiór. Marcin bawi się technologią, widać wyraźnie, że tworzenie sprawia Mu radość. I przekazuje tę radość innym.
Marcin prowadzi(ł) swój blog na geekswithblogs. Jednak od pewnego czasu można czytać teksty Marcina również na Jego blogu na zine, gdzie zdradza tajnikich nowych technologii. Tam również możecie Go znaleźć - zawsze chętnie odpowiada na każdy komentarz.
Czuję się wyróżniony, że tylu wybitnych ludzi zgodziło się współtworzyć zine.net. Dla mnie wszyscy jesteście MVP i wiem, że to kwestia czasu, kiedy stanie się to faktem!
Marcinie, jeszcze raz gratuluję i życzę Ci dalszych, jeszcze większych sukcesów!

Marcin podczas zanurzenia :)
|
-
Kilka dni temu dostałem maila tej treści:
------------ W związku z pracami konserwacyjnymi prowadzonymi przez jednego z naszych dostawców (Datacenter Energis) w nocy z soboty (2008-08-30) poczynając od godziny 23:30, na niedzielę do godziny 06:00 mogą wystąpić przerwy w działaniu serwerów HostedWindows.pl.
Przepraszamy za ewentualne utrudnienia. ------------
Jeśli zatem chcecie coś skomentować, wrzucić nowy post na blog - uwzględnijcie w swoich planach ten short break.
|
-
MTS - call to action :)
No i wybrałem. Chwila zastanowienia, konsultacje z chłopakami z grup oraz zina i padło na następujące sesje:
Środa
| 07:30 - 09:30 |
Rejestracja |
|
| 09:30 - 11:00 |
Sesja generalna (otwierająca) |
|
| 11:00 - 11:20 |
Przerwa kawowa |
|
| 11:20 - 12:35 |
Wydajne aplikacje ASP.NET w świecie Web 2.0 |
Tymoteusz Chmielewski |
| 12:35 - 13:35 |
Lunch |
|
| 13:35 - 14:50 |
The World of Expression |
Michael Köster |
| 14:50 - 15:10 |
Przerwa kawowa |
|
| 15:10 - 16:25 |
.NET bez wizardów – sposoby tworzenia i dynamicznego aktywowania komponentów w aplikacjach |
Bartosz Pampuch |
| 16:25 - 16:45 |
Przerwa kawowa |
|
| 16:45 - 18:00 |
Dynamics CRM 4.0 jako platforma dla aplikacji biznesowych |
Daniel Biesiada |
Czwartek
| 07:30 - 09:00 |
Rejestracja |
|
| 09:00 - 10:15 |
Bezpieczeństwo serwisów WWW – praktyczne uwagi o implementacji zaleceń DBTI ABW w ASP.NET |
Zbigniew Łapiński |
| 10:15 - 10:35 |
Przerwa kawowa |
|
| 10:35 - 11:50 |
Zasadzki systemowe w Windows Server 2008 |
Grzegorz Tworek |
| 11:50 - 12:50 |
Lunch |
|
| 12:50 - 14:05 |
Programowanie .NET na platformie SQL Server |
Krzysztof Kozielczyk |
| 14:05 - 14:25 |
Przerwa kawowa |
|
| 14:25 - 15:40 |
Testy w Microsoft Visual Studio Team System 2008 |
Artur Jedynak |
| 15:40 - 16:00 |
Sesja generalna (zamykająca) |
|
Czemu tak? Część sesji i tak zobaczę w ramach spotkań grup offline, część mam nadzieję obejrzeć po konferencji (będą materiały??), więc nie ma sensu wybierać ich na mtsie. Grzesia Tworka chętnie poznam osobiście, a sesja o internalsach w2k8 już mnie rozgrzewa :)
Jakoś wcześnie w tym roku rozchodzimy się do domków... Ale pewnie tylko po to, żeby uczesać kudełki i zabrać się na wieczorną imprezę :)
Zatem - do zobaczenia!
Ciekawe jak inni wybrali...
|
-
Biblijna wieża Babel w Visual Studio? No cóż, jeśli spojrzeć na Visual Studio jako kombajn, który dostarcza podstawową funkcjonalność dla wielu różnych języków, to czemu nie moglibyśmy wprowadzić się z naszym własnym i spróbować wykorzystać możliwości tegoż potwora do naszych celów?
Kontynuując zatem temat usług językowych Visual Studio, napoczęty tekstami o MPLex i MPPG, tym razem skupimy się na tym, jak można wykorzystać oba te narzędzia do "pokolorowania sobie plików" opisanych naszym lekserem i parserem.
Tym razem jednak, hołdując zasadzie, że jeden obraz znaczy więcej niż tysiąc słów, postanowiłem przygotować niespełna półgodzinny sceencast, w którym pokazałem krok po kroku jak przygotować projekt usługi językowej do pokolorowania składni przykładowego języka opisanego w poprzednich dwóch odcinkach. Sugeruję oglądanie w trybie HD z wyłączonym skalowaniem.

Babel w akcji
Z góry przepraszam za błędy, potknięcia, etc. - jest to mój pierwszy publiczny screencast :) Jeszcze nie wiem, dlaczego obciętych jest kilka pierwszych zdań, ale nie są one niezbędne do zrozumienia całości (nie ma tam trzęsienia ziemi w stylu Hitchcocka ;)). (Edit: Video jest już w całości). Czekam zatem na komentarze, bo zastanawiam się, czy ta forma prezentacji w moim wykonaniu ma sens. Jeśli tak - to pewnie jeszcze po nią sięgnę w przyszłości.
Przy okazji chciałbym zachęcić do obejrzenia innych screencastów poświęconych Visual Studio Extensibility, dostępnych na MSDN.
|
-
Intro
W poprzednim odcinku tej małej serii przedstawiłem w paru przykładach mplex - generator skanerów w C#, dedykowany do współpracy z Visual Studio. Dziś skupimy się na kolejnym z 'narzędzi językowych' dostępnych w SDK dla Visual Studio 2005, czyli MPPG. MPPG, czyli Managed Package Parser Generator, to odpowiednik unixowego YACC’a , czyli 'Yet Another Compiler Compiler' - kompilatora kompilatorów. Podstawą MPPG jest projekt GPPG, który jest wspierany przez Microsoft w ramach prac nad Ruby.NET (GPPG stanowi jego część). MPPG generuje kod w C# i współpracuje z MPLexem przy tworzeniu analizatorów składni. O tym, gdzie można znaleźć narzędzia językowe oraz jak przygotować sobie podstawowe środowisko pracy można przeczytać w poprzednim odcinku, więc przechodzimy od razu do przykładów.
Przykłady
Zacznijmy od skanera.
A. ex4.lex. Wracamy do naszego przykładu z poprzedniego odcinka i nieco go modyfikujemy na potrzeby parsera.
/* SEKCJA 1: DEFINICJE */ %using System.Collections; %using Babel; %using Babel.Parser; %namespace Babel.Lexer %{ const int LOOKUP = 0; int stan; Hashtable words = new Hashtable(); internal void add_word(int s, string text) { if(!words.Contains(text)) words.Add(text, s); } internal int lookup_word(string text) { if(words.Contains(text)) return (int) words[text]; else return LOOKUP; } %} %% /* SEKCJA 2: REGULY */ \n { stan = LOOKUP;} \. {stan = LOOKUP; return (int)Tokens.KROPKA;} ^rzeczownik {stan = (int)Tokens.RZECZOWNIK;} ^czasownik {stan = (int)Tokens.CZASOWNIK;} ^przymiotnik {stan = (int)Tokens.PRZYMIOTNIK;} [a-zA-Z]+ { if(stan != LOOKUP) { add_word((int)stan, yytext);} else { switch(lookup_word(yytext)) { case (int)Tokens.RZECZOWNIK: return((int)Tokens.RZECZOWNIK); case (int)Tokens.CZASOWNIK: return((int)Tokens.CZASOWNIK); case (int)Tokens.PRZYMIOTNIK: return((int)Tokens.PRZYMIOTNIK); default: {return (int)Tokens.LEX_ERROR;} } } } %% /* SEKCJA 3: KOD UZYTKOWNIKA */
Typ wyliczeniowy Locals z ex3.lex zastępujemy nieznanym nam jeszcze typem Tokens. Rezygnujemy z wypisywania informacji o zdefiniowanych częściach mowy, zamiast tego zwracamy liczbę określającą token powiązany z daną częścią mowy. Z sekcji kodu użytkownika, w której uprzednio mieliśmy zdefiniowany Main, nie pozostało już nic. W sekcji reguł z przyzwoitości zdefiniowaliśmy kropkę :). Reszta właściwie bez zmian.
Czas na wprowadzenie parsera.
1. ex41.y
/* SEKCJA 1: DEFINICJE */ %namespace Babel.Parser %partial %token RZECZOWNIK CZASOWNIK PRZYMIOTNIK %token KROPKA %token LEX_ERROR %token maxParseToken %% /* SEKCJA 2: REGULY */ zdanie: RZECZOWNIK CZASOWNIK KROPKA {Console.WriteLine("Zdanie prawidlowe!");} ; %% /* SEKCJA 3: KOD UZYTKOWNIKA */ public static void Main(string[] args) { Babel.Lexer.Scanner scnr = new Babel.Lexer.Scanner(); Parser p = new Parser(); p.scanner = scnr; //p.Trace = true; string line = Console.ReadLine(); do { scnr.SetSource(line, 0); p.Parse(); } while((line = Console.ReadLine()) != null); }
O co w tym chodzi???
Sprawa jest prostsza, niż się wydaje :). Po pierwsze, widać gołym okiem, że pliki mppg, podobnie jak pliki mplex, składają się z 3 części: Definicji, Reguł składniowych i Kodu użytkownika. I tak:
- W sekcji Definicji, korzystając ze specjalnego symbolu %namespace, ustalamy przestrzeń, w jakiej znajdzie się klasa Parser wygenerowanego analizatora składniowego. Dzięki symbolowi %partial ustalamy, że klasa Parser będzie klasą częściową, dzięki czemu możemy podzielić definicję klasy pomiędzy pliki (my jednak z tego nie korzystamy). Kolejny symbol specjalny to %token, dzięki któremu definiujemy symbole, których oczekujemy od leksera. Tak zdefiniowane symbole trafią do typu wyliczeniowego Tokens, który widzieliśmy już w naszym pliku leksera.
- W sekcji Reguł ustalamy reguły gramatyczne, dzięki którym określamy, czy dana konstrukcja jest prawidłowa składniowo, czy też nie. W naszym przypadku definiujemy regułę, która mówi, że zdanie jest prawidłowe, jeśli składa się z rzeczownika, czasownika i zakończone jest kropką. Dla przykładu, oczekujemy, że zdanie:
Ala je. jest prawidłowe, o ile 'Ala' to rzeczownik, a 'je' to czasownik, natomiast spodziewamy się, że zdanie: Ala je obiad. jest nieprawidłowe, bez względu na definicję poszczególnych słów, ponieważ nasze zdanie pozwala tylko na konstrukcje składające się z dwóch słów, w ustalonej kolejności, po których jest kropka. A zatem komunikat 'Zdanie prawidłowe!' powinien pojawić się wyłącznie w przypadku zdań takich, jak to z pierwszego przykładu.
- W sekcji kodu użytkownika dołączamy metodę Main, dzięki której możemy sprawdzić działanie naszego analizatora. Tworzymy obiekty klasy Scanner i Parser, wiążemy je ze sobą i przechodzimy w pętli do parsowania. Dla ciekawskich, którzy chcieliby zobaczyć jak odbywają się poszczególne redukcje oraz przesunięcia w toku działania parsera, wystarczy odkomentować linijkę 'p.Trace = true;'.
Wszystko w porządku, ale jak to teraz skompilować?
Jak już wcześniej pisałem, oba narzędzia - mplex oraz mppg wykorzystywane są przez usługi językowe Visual Studio, przez co wygenerowane klasy muszą implementować określone interfejsy oraz dziedziczyć po określonych klasach. W przypadku samego lexera wystarczyło dodać plik dummy.cs z odpowiednimi definicjami i sprawa była załatwiona. Tym razem jednak jest nieco trudniej. A zatem - krok po kroku:
1. Lexer. Tak, jak poprzednio, uruchamiamy:
2. Parser. W wierszu poleceń wykonujemy:
mppg wyrzuca wygenerowaną zawartość na wyjście standardowe, więc musimy przekierować wyjście do pliku, żeby móc z tego później skorzystać.
3. Z katalogu "%VS2K5SDK%\2007.02\VisualStudioIntegration\Common\Source\CSharp\" kopiujemy katalog Babel do katalogu, w którym znajdują się nasze pliki .lex i .y. Ponadto modyfikujemy nieco nasz plik dummy.cs:
//dummy.cs using System; namespace Babel.Parser { public interface IColorScan { void SetSource(string source, int offset); int GetNext(ref int state, out int start, out int end); } public interface IErrorHandler { int ErrNum { get; } int WrnNum { get; } void AddError(string msg, int lin, int col, int len, int severity); } }
4. Podczas kompilacji musimy dołączyć kilka referencji do bibliotek z katalogu "%VS2K5SDK%\2007.02\VisualStudioIntegration\Common\Assemblies\", a mianowicie:
- Microsoft.VisualStudio.TextManager.Interop.dll;
- Microsoft.VisualStudio.OLE.Interop.dll;
- Microsoft.VisualStudio.Package.LanguageService.dll;
- Microsoft.VisualStudio.Shell.dll;
- Microsoft.VisualStudio.Shell.Interop.dll;
- Microsoft.VisualStudio.Shell.Interop.8.0.dll;
- Microsoft.VisualStudio.TextManager.Interop.8.0.dll.
Mając tę wiedzę, możemy wreszcie skompilować nasz projekt (przy standardowej instalacji SDK tak to wygląda):
>set VS2K5SDK=C:\Program Files\Visual Studio 2005 SDK >set REFDIR=%VS2K5SDK%\2007.02\VisualStudioIntegration\Common\Assemblies >csc /out:ex41.exe /r:"%REFDIR%\Microsoft.VisualStudio.TextManager.Interop.dll" /r:"%REFDIR%\Microsoft.VisualStudio.OLE.Interop.dll" /r:"%REFDIR%\Microsoft.VisualStudio.Package.LanguageService.dll" /r:"%REFDIR%\Microsoft.VisualStudio.Shell.dll" /r:"%REFDIR%\Microsoft.VisualStudio.Shell.Interop.dll" /r:"%REFDIR%\Microsoft.VisualStudio.Shell.Interop.8.0.dll" /r:"%REFDIR%\Microsoft.VisualStudio.TextManager.Interop.8.0.dll" dummy.cs babel\IScanner.cs babel\ShiftReduceParser.cs babel\State.cs babel\ParserStack.cs babel\Rule.cs ex41.cs ex4.cs
UFF!!!
Oczywiście, nie życzę nikomu takiej zabawy na dłuższą metę i sugeruję przygotowanie sobie czy to pliku .bat, czy też odpowiedniego pliku dla msbuild. W tym drugim przypadku można skorzystać z przygotowanych tasków MPLexCompile oraz MPPGCompile, do których jeszcze wrócimy przy omawianiu usług językowych.
Po wykonaniu tych kroków mamy wreszcie nasz program, który wreszcie możemy uruchomić i potestować:
>ex41.exe mgrzeg je. rzeczownik mgrzeg czasownik je mgrzeg je. Zdanie prawidłowe!
Tak, "mgrzeg je." i jest to zgodne z naszą gramatyką :)
Pobawmy się przez chwilę specjalnym symbolem 'error', dzięki któremu możemy powiedzieć, że coś jest nie do końca tak, jak powinno być. Zmodyfikujmy w tym celu sekcję reguł, pozostawiając resztę kodu niezmienną.
2. ex42.y
/* SEKCJA 2: REGULY */ zdanie: RZECZOWNIK CZASOWNIK KROPKA {Console.WriteLine("Zdanie prawidlowe!");} | RZECZOWNIK CZASOWNIK error {Console.WriteLine("Brakuje kropki!");} | RZECZOWNIK error {Console.WriteLine("Brakuje czasownika!");} | error CZASOWNIK {Console.WriteLine("Brakuje rzeczownika!");} ; %%
Teraz po uruchomieniu programu nasza sesja może wyglądać następująco:
>ex42.exe ala je. rzeczownik ala czasownik je ala je. Zdanie prawidlowe! ala. Brakuje czasownika! je. Brakuje rzeczownika! ala je Brakuje kropki!
A zatem potrafimy już wychodzić (a przynajmniej informować użytkownika o tym) z sytuacji błędnych, czas na nieco bardziej złożoną gramatykę. Zmodyfikujmy zatem nieco naszą definicję zdania, dodajmy części zdania. Zmieńmy zatem po raz kolejny sekcję Reguł, resztę kodu pozostawiając bez zmian.
3. ex43.y
zdanie: podmiot orzeczenie dopelnienie KROPKA {Console.WriteLine("Zdanie prawidlowe!");} | podmiot orzeczenie dopelnienie error {Console.WriteLine("Brakuje kropki");} | podmiot error {Console.WriteLine("Brakuje orzeczenia");} | error orzeczenie {Console.WriteLine("Brakuje podmiotu");} ; podmiot: RZECZOWNIK ; orzeczenie: CZASOWNIK ; dopelnienie: /* pusto! */ | PRZYMIOTNIK RZECZOWNIK | PRZYMIOTNIK error {Console.WriteLine("Brakuje rzeczownika w dopelnieniu!");} | RZECZOWNIK ;
Teraz przykładowa sesja z programem może wyglądać następująco:
>ex43.exe rzeczownik ala kota czasownik ma przymiotnik czarnego ala ma czarnego kota. Zdanie prawidlowe! ala ma kota. Zdanie prawidlowe! ala ma. Zdanie prawidlowe! ala kota. Brakuje orzeczenia ma czarnego kota. Brakuje podmiotu ala ma kota Brakuje kropki
Teraz nasza definicja zdania zakłada, że składa się ono z podmiotu, orzeczenia, dopełnienia i zakończone jest kropką. Reszta definicji jest oczywista, może dodatkowego komentarza wymaga dopełnienie, które może być puste, lub składać się z przymiotnika i rzeczownika, lub samego rzeczownika. Wszystkie trzy przypadki zostały przez nas sprawdzone w przykładowej sesji.
W ostatniej zabawie ze zdaniami możemy pokusić się o regułę rekurencyjną
4. ex44.y
zdanie : zdanie_proste KROPKA {Console.WriteLine("Zdanie proste!");} | zdanie_zlozone KROPKA {Console.WriteLine("Zdanie zlozone!");} ; zdanie_proste : podmiot orzeczenie dopelnienie | podmiot error {Console.WriteLine("Brakuje orzeczenia");} | error orzeczenie {Console.WriteLine("Brakuje podmiotu");} ; zdanie_zlozone : zdanie_proste SPOJNIK zdanie_proste | zdanie_proste error zdanie_proste {Console.WriteLine("Brak spojnika");} | zdanie_zlozone SPOJNIK zdanie_proste ; ...reszta reguł,
Zdefiniowanie dodatkowego tokena oraz pozostałą zabawę pozostawiam jako samodzielne ćwiczenie.
Na sam koniec przykładów baaardzo prosty przykład kalkulatora (w końcu Gutek też pisał kalkulator :P), który potrafi tylko dodawać i odejmować liczby całkowite :D.
B. ex6.lex
/* SEKCJA 1: DEFINICJE */ %using Babel; %using Babel.Parser; %namespace Babel.Lexer %% /* SEKCJA 2: REGULY */ [0-9]+ { yylval.value = int.Parse(yytext); return (int)Tokens.NUMBER;} [ \t] ; \n|\r\n return 0; . return yytext[0]; %% /* SEKCJA 3: KOD UZYTKOWNIKA */
Do tego dorzucamy plik parsera.
1. ex61.y
/* SEKCJA 1: DEFINICJE */ %namespace Babel.Parser %partial %union { public int value; } %start statement %token NAME NUMBER %token maxParseToken %% /* SEKCJA 2: REGULY */ statement: expression '.' {Console.WriteLine("={0}", $1.value);} ; expression: expression '+' NUMBER {$$.value = $1.value + $3.value;} | expression '-' NUMBER {$$.value = $1.value - $3.value;} | NUMBER {$$.value = $1.value;} ; %% /* SEKCJA 3: KOD UZYTKOWNIKA */ public static void Main(string[] args) { Babel.Lexer.Scanner scnr = new Babel.Lexer.Scanner(); Parser p = new Parser(); p.Initialize(); p.scanner = scnr; //p.Trace = true; string line = Console.ReadLine(); do { scnr.SetSource(line, 0); p.Parse(); } while((line = Console.ReadLine()) != null); }
Warto zwrócić uwagę na wykorzystanie symbolu specjalnego %union, który definiuje nam strukturę, obiekt której widoczny jest od strony scannera jako yylval, a od strony parsera możemy operować na nim wykorzystując symbole $$ oraz $i, gdzie i oznacza pozycję terminala w danej regule, dla przykładu:
expression: expression '+' NUMBER {$$.value = $1.value + $3.value;} $$.value - wartość pola value symbolu nieterminalnego $$; $1.value - wartość terminala expression z prawej strony reguły '+' - odpowiada $2, $3.value - wartość terminala NUMBER.
Przykładowe działanie programu:
Ostatki
Na koniec pare informacji o debuggowaniu. Jakkolwiek MPLex nie pomaga nam w tym za bardzo, to MPPG emituje do wygenerowanego pliku z klasą Parsera informację o numerach wierszy pliku źródłowego. Tym samym wystarczy podpiąć się z debuggerem gdziekolwiek w kodzie i możemy na bieżąco śledzić bieżące wartości pól skanera i parsera. Dla przykładu, żeby wskoczyć z debuggerem do pliku .y i sprawdzić jakie mamy bieżące wartości odpowiednich terminali w regule dodawania, wystarczy zapisać ją:
expression: expression '+' NUMBER { System.Diagnostics.Debugger.Break(); $$.value = $1.value + $3.value; }
Sugeruję umieszczanie kolejnych instrukcji w osobnych wierszach - debugger na podstawie wygenerowanego pliku ma tylko informację o numerze linii, nie wie nic o kolumnie :(. Oczywiście, należy też pamiętać o zmuszeniu kompilatora do wygenerowania informacji dla debuggera, czyli dodaniu opcji /debug+ w wywołaniu csc.
|
|
|
|