Development z Zapomnianej Strony

..:: Paweł Hofman .NET Portal ::..

Gdy w końcu przyjdzie pora, że podczas zabawy z QNX i SDK dla BlackBerry Playbook zostaniemy sam sam z plikiem .bar (lub nie daj Boże!) dostaniemy go od zaprzyjaźnionego dewelopera, to istnieje bardzo prosty sposób, aby ten plik umieścić na urządzeniu. Wystarczy wykonać polecenie:

blackberry-deploy -installApp -device <IP urządzenia> -package <ścieżka do pliku BAR> -password <hasło urządzenia>

A co najlepsze, wszystko to możemy zrobić bez fizycznego podłączania urządzenia do komputera, jeśli poprawnie skonfigurowaliśmy swoje konto dewelopera i mamy możliwość wgrywania zdalnie po WiFi.


Visual Studio 2008 jako narzędzie developerskie dla programistów Windows (czytaj .NET i nie tylko) plasuje się na pierwszej pozycji. Jednak pod względem rozszerzania i dostosowywania plasuje się daleko w tyle za swoim największym konkurentem Eclipse. Prawdopodobnie dlatego też Microsoft zrezygnował z opłat licencyjnych, wymaganych przy pisaniu dodatków jeszcze przy pierwszej wersji Visual Studio 2008 SDK.

Dlatego też chciałem dzisiaj pokazać kilka przykładów, jak Visual Studio możemy dostosowywać do swoich potrzeb (z użyciem lub bez SDK):

  1. Korzystając z dostępnych w sieci narzędzi, zmieniamy aktualny motyw (theme) tak, aby nie raził i nie męczył wzroku. Milutki generator znajdziemy tutaj

    Visual Studio Generated Template

     

  2. Grzebiąc w rejestrach systemu możemy włączyć wskaźnik po prawej stronie tekstu tak, aby linie tekstu nie były zbyt długie. Wyróżniona 21-a kolumna zapewni nas, że pisząc tak krótkie linie, otworzymy pliki z kodem źródłowym nawet na najstarszej DOS-owej maszynie.

    Uaktualniamy rejestr systemowy dodając następujący wpis typu tekstowego (string):

    [HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\Text Editor]
    Guides = RGB(128,0,0) 20


    (dowolnie dobieramy kolor oraz po przecinku za liczbą 20 wpisujemy dodatkowe wartości, jeśli jeden przewodnik to za mało)
    Visual Studio - przewodnik
  3. Piszemy własny wizualizator danych.

    Wizualizatory to to, co tygrysy lubią bardzo. Bardziej lubią chyba tylko pakiety (VS Packages), ale o tym za chwilę. Idea wizualizatorów polega na tym, iż pisząc własne programy często tworzymy własne struktury danych, które łatwiej przestawiałyby się graficznie. Podczas debugowania Visual Studio udostępnia nam zatem kopię wskazanego obiektu, który możemy własnoręcznie przetworzyć i pokazać na ekranie. Poniżej znajduje się przykład wizualizatora obiektów System.__ComObject z wyszczególnieniem interfejsów i metod implementowanych przez tego CRW (Callable Runtime Wrapper).

    Visual Studio - wizualizator
    Oznaczone odpowiednimi atrybutami assembly wgrywamy do katalogu:

    %MyDocuments%\Visual Studio 2008\Visualizers

  4. Kolejnym ciekawym rozszerzeniem jest możliwość dodawania własnych słów kluczowych, które zostaną rozpoznane w edytorze tekstu dla VisualC++. Dotyczy się to przede wszystkim plików z rozszerzeniami .h, .c, .cpp, jednak bardzo dobrze sprawdza się również po przypisaniu edytora Microsoft Visual C++ do rozszerzeń takich jak .asm, .cu, czy tym podobnych (Tools->Options->Text Editor->File Extensions).

    W poniższym pliku umieszczamy nowe słowa kluczowe w kolejnych linijkach, a po ponownym uruchomieniu Visual Studio od razu zauważymy efekty:

    c:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\usertype.dat

  5. Na sam koniec deser. Gdy wymienione powyżej przykłady nie spełniają naszych wymagań, to zwracamy się w stronę pisania własnych rozszerzeń z w pełni funkcjonalną logiką. Co chcielibyśmy tym uzyskać, ot np.: aby z poziomu Visual Studio uaktualniały się wpisy w narzędziach do zarządzania błędami (ticket'y w wybranym trackerze) lub po prostu potrzebujemy dokujące okienko narzędziowe, które ściąga nowinki ze świata, przekonując pracodawcę, że jednak dzielnie kodujemy. Piszemy wtedy własny dodatek add-in lub VS Package wykorzystując w pełni możliwości oferowane przez SDK.

    Należą do nich m. in.:
    - własne edytory (lub zmiana istniejących)
    - okna narzędziowe (tool windows)
    - pliki pomocy
    - nowe języki programowania (wraz ze schematami budowania nowych projektów)
    - nowe rodzaje projektów (bądź dodawanie funkcji do istniejących, tj.: .csproj)
    - szablony i kreatorzy (templates & wizards)
    - dostawców usług (dostawcy listy błędów, zadań)


    Więcej informacji o pisaniu dodatków do Visual Studio zamieściłem w Samouczku.
    Jako działający przykład posłużyć może autorski projekt TytanNET.

 

Wytrwałych odsyłam do bloga Sary Ford, która do niedawna jeszcze żywo publikowała nowinki w stylu Tips&Tricks o Visual Studio oraz była autorką kilku książek o podobnie brzmiącym tytule.

Jako książkę z tej tematyki polecam (czym prawdopodobnie zadziwię niektórych) Inside Visual Studio .NET 2003 (tak 2003!), gdyż ona najdokładniej omawia sposób pisania add-inów oraz interakcję z tego poziomu z edytorami, plikami projektu i kodem źródłowym.

 

Wesołego kodowania!



Mało kto zdaje sobie tak naprawdę sprawę, że oprócz wersjonowania kodu oraz oficjalnych/inżynierskich releasów wysyłanych do klientów wypada również wersjonować pliki PDB z nimi związane. Dlaczego? Odpowiedź jest bardzo prosta. Wyobraźmy sobie, iż nasza aplikacja się po prostu wysypuje. Zaraz ktoś mi powie – “hola hola, ale przecież mamy logi, w pięknym tekstowym formacie i wszystko w nich widać”. Może ktoś nawet słyszał o plikach MAP :) Super! No ale nie wszystkie nasze moduły muszą być przecież napisane w .NET. Te napisane w językach natywnych (C/C++) nie podadzą nam tak łatwo miejsca wystąpienia błędu (czytaj: pełny stos wywołań, call-stack). Jedyne na co możemy wtedy liczyć, bez wspomnianej informacji debugowej, to nazwa modułu i adres, pod którym spotkany został błąd. Ponadto nie zawsze mamy pod ręką Visual Studio. Jeśli chcielibyśmy przeprowadzić debugowanie na nieco niższym poziomie przy pomocy WinDBG, aby wykryć zakleszczenia (dead-locks) oraz wysokie zużycie pamięci, czy po prostu różne inne wycieki pamięci natywnej (nawet w kodzie .NET) to okazują się one wręcz niezbędne. W dodatku na różnych maszynach, ten sam błąd może wystąpić pod różnym adresem, co związane jest z niedeterministyczną realokacją modułów lub po prostu dysponujemy zrzutem pamięci (memory dump file), który pozwala nam w pełni cieszyć się chwilą awarii naszej aplikacji w trybie “pośmietnym” (określanym jako post-mortem debugging).

A tak niewiele trzeba, aby całe zadanie w pełni zautomatyzować. Potrzebujemy zrobić około trzech rzeczy:

  • założyć lokalne repozytorium plików PDB oraz EXE/DLL dla naszych produktów (dajmy na to w katalogu D:\SymbolStore\Projects)
  • dla jeszcze większej wygody założyć lokalne repozytorium na pliki PDB dla bibliotek systemu operacyjnego, które będziemy pobierać z publicznych serwerów Microsoftu (co pomoże rozszyfrować nazwy funkcji oraz przekazywane parametry do systemowych wywołań WinAPI – kernel32.dll, user32.dll…)
  • zdefiniować zmienną systemową _NT_SYMBOL_PATH, która będzie pokazywała na te repozytoria:

_NT_SYMBOL_PATH=srv*D:\SymbolStore\Microsoft\* http://msdl.microsoft.com/download/symbols;srv* D:\SymbolStore\Projects\

 

Od tej pory z każdą nową wersją naszej aplikacji wykonamy poniższe dwie linijki skryptu (rozszerzone samemu już też o pliki EXE/DLL lub inne cenne dla nas dane):

set MODPATH=D:\SymbolStore\Projects\
set ProjectPath=D:\Projects\MyProject
symstore add /r /f "%ProjectPath%\*.pdb" /s %MODPATH% /t "MyProject" /v "Build v2.0.1" /c "15/12/2009 Daily Build"

Narzędzie symstore służące do zarządzania lokalnym repozytorium plików PDB dostępne jest za darmo po zainstalowaniu pakietu Debugging Tools for Windows (czyli po prostu z WinDBG). Dodatkowo zmienną tę poprawnie rozpoznaje większość Microsoftowych debuggerów i automatycznie pobiera odpowiednie pliki PDB związane z modułami EXE/DLL, które aktualnie analizujemy.

Nie ma róży bez kolców:

  • w ciągu kilku pierwszych uruchomień, większość tych plików PDB dla naszego OSa musi zostać pobrana z Internetu, co może trochę potrwać (300MB się jednak trochę pobiera…)
  • zajmują one też coraz więcej miejsca na dysku ;)
  • no i w końcu sam czas włączania debugera, zwłaszcza Visual Studio, znacząco się wydłuża, bo są one ładowane do pamięci, aby rozwiązywać nazwy funkcji; WinDBG jest tu o tyle inteligentniejszy, że ładuje te pliki na żądanie lub kiedy podejrzewa, że będą potrzebne nie obciążając maszyny aż tak bardzo.

Miłego debugowania.



Z różnych powodów, czasem nie chcemy, aby ktoś przyglądał się naszej aplikacji w trakcie jej działania. Pomińmy jednak na chwilę te powody i skupmy się na samym wykrywaniu debuggera…

Najprostszą metodą sprawdzenia w systemie Windows, czy nasza aplikacja jest aktualnie uruchomiona w jego kontekście lub jest on podłączony można wykonać poprzez wywołanie funkcji IsDebuggerPresent() z WinAPI. Jednakże sprytny debugger może ją przesłonić i zawsze zwracać, że nie.

Informację tę zatem wyciągniemy samemu, “ręcznie” z bloku informacji o wątku (Thread Information Block – TIB). Pod indeksem 0x30 znajduje się wskaźnik na strukturę opisującą debugger. Drugi bajt tej struktury opisuje status tego, czego szukamy. Ponadto wartość ta jest dynamicznie uaktualniana, gdy tylko coś dzieje się z naszym procesem. Przykład implementacji funkcji w C, która pobierze dla nas tę wartość przestawiam poniżej.


int IsSuperDebuggerPresent ()
{
    int result;

    __asm
    {
        /* get the Thread Information Block (TIB) pointer */
        mov eax, fs:[18h]

        /* 0x30 byte points to the debugger structure: */
        mov eax, dword ptr [eax + 0x30]

        /* then second word indicates if the process is under debugging: */
        movzx eax, byte ptr [eax + 2]
        mov result, eax
    }

    return result;
}


Inną metodą pozwalającą sprawdzić, czy nic nie dzieje się wokół naszej aplikacji jest również sprawdzanie licznika instrukcji RTDSC (Time Stamp Counter). W swoim kodzie możemy oszacować ilość ticków procesora, która mija przy wykonywaniu pewnych, zabezpieczanych bloków instrukcji. Następnie porównywać je  ze statystykami prowadzonymi wśród całego kodu.  Tutaj jednak pojawiają się pytania – jak zachowywać się i w środowiskach wielowątkowych i wieloprocesorowych, gdzie wielkości te nie dają się łatwo przewidzieć i mogą zmieniać się w czasie…



 

Watch Window Debugowanie i podglądanie wartości zmiennych w oknie Watch, często byłoby dużo wygodniejsze, gdyby dane można rzutować na inne typy oraz poddawać je innym manipulacjom. Na szczęście prócz wymuszenia zmiany typów, okno to posiada wbudowany zestaw predefinowanych formatów, które mogą pobierać dane ze znanych typów wyliczeniowych (Windows Messages lub Code Errors) lub po prostu odczytywać napisy ANSI / UTF-16, bądź traktować dane jako tablice o zadanej wielkości, podając wartości wszystkich elementów. Wpisujemy je po przecinku na końcu wyrażenia, np.:

  • lpszFileName, su --> spowoduje wyświetlenie zawartości zmiennej jako tekstu UTF-16
  • dwMessage, wm  --> wyświetli zadaną liczbę jako kod Winows Message (WM_CREATE, …)
  • index, x  –-> wyświetli liczbę w trybie hexadecymalnym
  • (char *)lpszDeviceName, 10  --> wyświetli 10 pierwszych znaków napisu

Dla C++ ten zestaw formatów przedstawia się następująco:

Nazwa formatu Opis działania Przykładowe wyrażenie Wyświetlane
d, i signed decimal integer 0x1122FF, d  
u unsigned decimal integer 0x11, u  
o unsigned octal integer    
x, X hexadecimal integer    
l, h przedrostek long/short dla: d, i, u, o, x, X    
f signed floating point    
e signed scientific notation    
g signed floating point lub signed scientific notation w zależności od tego, który jest krótszy    
c single character    
s string    
su Unicode string 0x023571FA, su  
hr HRESULT 0x00000000L, hr S_OK
wc flagi Windows Class 0x00000040, wc WC_DEFAULTCHAR
wm numery komunikatów Windows Message 0x0001, wm WM_CREATE
! raw format, usuwa wszystkie informacje o typie    

 

Nazwa formatu Opis działania Przykładowe wyrażenie
ma 64 ASCII characters lpData, ma
m 16 bytes in hexadecimal, followed by 16 ASCII characters lpData, m
mb 16 bytes in hexadecimal, followed by 16 ASCII characters lpData, mb
mw 8 words lpData, mw
md 4 doublewords lpData, md
mq 2 quadwords lpData, mq
mu 2-byte characters (Unicode) lpData, mu

 

Tak samo i w programach w języku C# formaty przestawiają się:

Nazwa formatu Opis działania Przykładowe wyrażenie Wyświetlane
d decimal integer 0x64 100
h hexadecimal integer 11 0xB
nq string without quotes “Sample Text” Sample Text
private Wyświetla prywatne zmienne obiektu.    
raw Wyświetla dane własne obiektu. Poprawne tylko dla typów pośredniczących (proxy).    
Ac Wymusza wyliczanie wartości wyrażeń (przydatne gdy wyliczanie wartości i właściwości jest wyłączone).    

 

Jako ostatnie chciałem przestawić pseudo-zmienne, które pokazują charakterystyki uruchamianej aplikacji:

Pseudo-zmienna Opis działania
$handles Wyświetla liczbę zaalokowanych uchwytów (HANDLE) aplikacji.
$vframe Wyświetla adres aktualnej ramki stosu (stack frame).
$TID Wyświetla ID aktualnego wątku.
$ENV Wyświetla rozmiar środowiska przekazanego podczas uruchamiania aplikacji.

Próba edycji tej wartości wyświetli wszystkie zmienne środowiskowe w oknie Output, bez wykonania jakiejkolwiek zmiany.
$CMDLINE Wyświetla rozmiar linii poleceń, którą uruchomiono program.

Próba edycji wartości wyświetli wartość tekstową tej linii w oknie Output, bez jakiejkolwiek zmiany.
$registername
        or
@registername
Wyświetli zawartość rejestru o zadanej nazwie.
$clk Wyświetli aktualny czas w cyklach.
$user Wyświetli informacje o koncie użytkownika, z którego uruchomiono aplikację.
$exception (C#, VB.NET, F#) Wyświetli informacje o ostatnim wyjątku.

 

Więcej informacji można znaleźć w MSDN (o C++ tutaj, o C# tutaj).



Sposób wywoływania funkcji w programach pisanych w C/C++, Pascalu, assemblerze (rozumianych szeroko jako języki i kod niezarządzany) odgrywa niebagatelne znaczenie. Aby dwie funkcje - wywołująca i wywoływana - mogły się ze sobą dogadać, muszą wcześniej uzgodnić kilka drobnych acz istotnych szczegółów. Należą do nich:

  • sposób, w jaki przekazywane będą argumenty (np. poprzez stos, rejestry lub mieszając oba)
  • sposób, w jaki argumenty są numerowane (od prawej, czy od lewej strony)
  • sposób udekorowania nazwy funkcji (poprzez dodanie przyrostków, przedrostków lub całkowite wygenerowanie nowej nazwy, gwarantujące unikatowość, gdy funkcja jest wielkorotnie przeładowana)
  • czy występuje stała, czy zmienna liczba argumentów (a tym samym, która ze stron będzie odpowiedzialna za posprzątanie)
  • czy występuje wywołanie funkcji globalnej (statycznej) lub metody klasy (C++).

Mimo iż kombinacji wymienionych czynników jest sporo, stosuje się głównie 3 konwencje: fastcall, stdcall, thiscall, cdecl.
Poniżej krótkie omówienie, ze wskazaniem najważniejszych cech oraz zastosowania.

 

Konwencja stdcall (__stdcall)

  • argumenty odkładane na stosie od prawej do lewej (od końca)
  • funkcja wywoływana odpowiedzialna jest za usunięcie argumentów ze stosu (jako, że ich rozmiar jest znany)
  • nazwa funkcji zostaje udekorowana  - otrzymuje znak podkreślenia jako przedrostek  a na końcu po znaku małpy, rozmiar argumentów w bajtach (np. void fun(int arg) -> _fun@4)
  • konwencja ta nadaje się wszędzie tam, gdzie występuje znana liczba argumentów oraz występują restrykcje co do rozmiaru kodu binarnego (który mniejszy jest, gdyż to funkcja wywoływana sprząta stos)

 

Konwencja cdecl (__cdecl)

  • argumenty odkładane na stosie również od prawej do lewej (od końca)
  • funkcja wywołująca odpowiedzialna jest za usunięcie argumentów ze stosu (bo zna ich rozmiar), a to pozwala na stosowanie zmiennej liczby argumetów (np.: printf)
  • nazwa funkcji zostaje poprzedzona podkreślnikiem (np. void fun(int arg) -> _fun)
  • generuje dłuższy kod binarny od stdcall (jako, że sprzątanie stosu jest wstawiane po każdym wywołaniu), ale pozwala na wywołania funkcji ze zmienną listą argumentów

Ciekawe porównanie znaleźć można na codeguru.com.

 

Konwencja fastcall (__fastcall)

  • główną zaletą jest fakt, iż argumenty przekazywane są nie przez stos, a rejestry
  • niemniej jednak wymaga to pewnych założeń
    • rejestry te muszą być w chwili wywołania nieużywane (inaczej ich stan będzie odłożony na stos i zyski z takiego przekazywania argumentów zmaleją)
    • używane są tylko niektóre rejestry (i jeśli funcja ma za dużo argumentów lub ich rozmiar jest za dużo, to i tak będą przekazywane przez stos)
    • kolejność użycia rejestrów zależy wyłącznie od użytego kompilatora (i niektóre pozwalają na własne zdefiniowanie tej kolejności)
    • np. Microsoft C++ Compiler używa ECX i EDX, w których umieszcza dwa pierwsze argumenty (bez upakowania jeśli nie są 32-bitowe), a pozostałe odkłada na stos w kolejności od prawej do lewej (od końca)
  • nazwa funkcji jest poprzedzona znakiem małpy i tak samo jak przy stdcall, na końcu dodany jest również znak małpy oraz rozmiar argumentów w bajtach (np. void fun(int a) -> @fun@4).

 

Konwencja thiscall

  • jest to konwencja stosowana najczęściej przy wywołaniu funkcji danej klasy (w C++)
  • w funkcjach takich zawsze jako pierwszy parametr występuje niejawnie wskaźnik na obiekt klasy (this) wywołujący zadaną metodę
  • funkcja o stałej liczbie argumentów:
    • sama jest odpowiedzialna za posprzątanie stosu
    • przy Microsoft C++ Compiler, wskaźnik na obiekt klasy (this) jest przekazywany przez rejestr ECX
  • funkcja o zmiennej liczbie argumentów:
    • to wywołujący jest odpowiedzialny za posprzątanie stosu
    • w tym miejscu zachowuje się dokładnie jak cdecl - wszystkie argumenty są odkładane i przekazywane przez stos, razem ze wskaźnikiem na obiekt klasy (this)
  • nazwy funkcji nie są zmieniane