Technologiczna bieda kompilatora PureBasic

assembler x 16kompilator x 4optymalizacja x 3PureBasic x 1x86 x 4

Nawiązując do artykułu o kompilatorze Go, nie mogę ominąć kompilatora PureBasic w wersji od 4.xx do 5.30. Jest to kompilator dla kolejnej odmiany języka BASIC, której developerzy są głusi na wszelkie maile dotyczące nieprawidłowości w kodzie i w ogóle “nie rozumieją o co chodzi”.

Więc o co tym razem mi chodzi? Analizowałem plik kompilatora PureBasic.exe w wersji 5.30. Może zacznijmy od optymalizacji.

1. Optymalizacja

Optymalizacja to słowo, które jest obce twórcom kompilatora PureBasic. W poprzednim artykule ktoś sugerował, że biedna optymalizacja kompilatora Go być może ma jakiś “sens”, na pewno jakiś gimbus, który nie rozróżnia assemblera od akumulatora, no więc być może i tutaj znajdziesz jakiś sens (czekam na komentarz):

Nie wiem jak można by i to zaklasyfikować? push ebp + pop ebp = zero efektu, wyrównanie kolejnych instrukcji też nie.

Kolejny genialny fragment kodu:

Widać ktoś nie doczytał, że instrukcją mov można również odczytać dane z pamięci, pomijając dodatkowo stos.

Teraz trochę dłuższy fragment, w którym być może zauważycie pewien powtarzający się wzorzec:

Kolejno ustawiana jest wartość rejestru EBP, nie zważając na to, że z 20 razy jest ta sama operacja wykonywana, być może twórcy PureBasic-a  doczytali o loop unrollingu i wdrożyli go w życie z dodatkowymi benefitami…

2. Techniki czyszczenia stosu dla cdecl

Konwencja wywoływania funkcji cdecl bazuje na tym, że parametry są kolejno wrzucane na stos serią instrukcji push, po czym następuje wywołanie funkcji instrukcją call, po której wskaźnik stosu np. ESP jest korygowany o rozmiar wrzuconych wcześniej parametrów np. przez add esp,8 (dla dwóch 4 bajtowych parametrów etc.).

Różne kompilatory różnie radzą sobie z korektą stosu, jedne po każdym wywołaniu funkcji w konwencji cdecl korygują stos, inne potrafią inteligentnie obliczyć rozmiar parametrów i skorygować stos po całej serii wywołań wielu funkcji w konwencji cdecl (np. LCC), a jeszcze inne kompilatory robią to tak:

Mówiąc w skrócie po wykonaniu call-a (2 parametry, czyli 8 bajtów na stosie zostało do tego wykorzystanych) stos korygowany jest przez instrukcję pop eax (która działa jak add esp,4), po czym stos korygowany jest ponownie instrukcją add esp,4, a po niej wartość rejestru EAX, który jak można by się było domyśleć miał być do czegoś wykorzystany – jest zerowany! PureBasic – PureMagic :)

3. Sprawdzanie flag

W sumie można by to zaliczyć do optymalizacji, gdyby w ogóle kompilator jakieś stosował:

Jak widać kompilator PureBasic w zależności od humoru stosuje 3 metody sprawdzania czy rejestr jest wyzerowany (z czego 2 nie są rekomendowane, gdyż to operacje zapisu czyli and i or, instrukcja test jedynie ustawia flagi).

4. Konwencja “zerocall”

W czasach DOS-u, częstą praktyką była konwencja wywoływania funkcji, w której ustawiano flagę carry (przeniesienia) instrukcją STC lub ją zerowano instrukcją CLC w zależności od wartości zwracanej przez funkcję (true / false). Przypadki użycia takich technik widziałem również w kilku programach na Windows, jednak to taka rzadkość, że można przypuszczać, że to twór programistów mających korzenie programistyczne w DOS-ie.

Wykorzystanie flagi carry miało swoje plusy, gdyż po wywołaniu funkcji nie trzeba było sprawdzać wyniku funkcji np. przez test eax,eax, a jedynie wykonać instrukcję skoku JC lub JNC.

PureBasic stosuje unikalną konwencję, którą nazwałem zerocall:

Czyli funkcja sama w sobie sprawdza wynik i ustawia flagę zerową, można powiedzieć, że jest to technologia unikalna dla PB, gdyż nigdy wcześniej nie spotkałem się z taką innowacją.

Co ciekawe miałoby to jedynie sens w przypadku optymalizacji pod względem rozmiaru i w przypadku zastosowanie tej techniki do wszystkich wygenerowanych funkcji, a tak niestety nie jest.

5. Zerowanie pustej pamięci

W sumie ten kod zainspirował mnie do kolejnego artykułu po kompilatorze języka Go. Generował go kompilator PB w wersji od 4.xx do 5.30.

Krótki opis. Zerowana jest pamięć od zmiennej hHeap, która znajduje się w sekcji danych “.data”. Zerowany bufor pamięci ma rozmiar 0x1C20 bajtów.

Spoglądając na deadlisting można zauważyć, że ten obszar jest pusty! A do tego jego większa część znajduje się w zakresie sekcji “.data”, która jest niezainicjalizowana.

Gwoli wyjaśnienia sekcja “.data” posiada ustawiony rozmiar fizyczny oraz rozmiar wirtualny, który przekracza rozmiar fizyczny co pozwala na zaoszczędzenie miejsca na dysku plikom, które chcą mieć zaalokowane duże obszary pamięci.

System Windows niezainicjalizowane obszary pamięci w sekcjach zawsze wypełnia zerami.

Zerowany obszar w plikach PureBasic jest zatem wypełniony 0×00 od samego początku i nie ma potrzeby dodatkowo go zerować, całej sytuacji smaczku dodaje jeszcze fakt, że obszar ten wykorzystywany jest do trzymania zmiennych globalnych i kompilator zamiast zapisać wartości tych zmiennych od razu do pliku, najpierw generuje kod, który zeruje cały ten obszar, a później dodatkowo generuje instrukcje, które ustawiają te wartości, czyli 2 razy marnowany jest czas procesora po uruchomieniu takiej aplikacji, podczas gdy cały ten proces można zrealizować na etapie kompilacji.

Konkluzje

PureBasic to całkiem ciekawe narzędzie, ale jakość generowanego kodu pozostawia wiele do życzenia i nie zdecydowałbym się na jego używanie do wysokowydajnych aplikacji, poza tym fundamentalne braki w wiedzy twórców kompilatora, w tak podstawowych kwestiach jak alokacja pamięci czy poprawne wykorzystanie instrukcji procesora x86, sprawiają, że można mieć wątpliwości co do samej jakości kodu jak i innych technologii użytych w tym narzędziu.

Reversing na Tweeterze

Technologiczna bieda kompilatora Go

assembler x 16Go x 1Google x 1kompilator x 4optymalizacja x 3

Lubię babrać się w low levelu, to zarówno moja pasja jak i praca. Jestem w trakcie testowania nowego exe protectora i jak to zwykle bywa – testuje nim co popadnie, wliczając wszelkie nietypowe pliki w formacie PE, w tym pliki z różnych kompilatorów i środowisk programistycznych jak np. DMD, Lazarus / FPC, PureBasic, a nawet takie antyki jak WATCOM.

Różne kompilatory stosują całą mase magicznych zabiegów, aby czasami nieświadomie uniemożliwić zabezpieczenie wygenerowanego pliku PE EXE, a ja staram się to ładnie obsłużyć, ominąć, hookować etc.

Dzisiaj przysiadłem sobie do kompilatora języka wprost od Google, czyli – Go.

…i zdębiałem. Ściągnąłem paczkę v1.3.1 dla Windows w wersji 32 bitowej.

O ile protector poradził sobie bez zająknięcia z testowym zabezpieczeniem pliku kompilatora – go.exe, o tyle nie byłbym sobą jeśli bym nie zajrzał do środka, jak strukturalnie zbudowany jest plik z Go (biorąc pod uwagę, że kompilator Go zbudowany jest ze źródeł w C oraz samym języku Go).

A w środku szalone lata 90 :), spójrzmy na to razem:

1. Mania używania CLD

CLD to instrukcja procesora ustawiająca flagę kierunku wykonywania operacji takich jak rep stosb, rep movsd, scasb, jej zresetowanie powoduje, że operacje te wykonują się bajt po bajcie w kolejności rosnącej, jej ustawienie powoduje, że operacje te wykonują się odwrotnie (czyli od końca do przodu).

Problem w tym, że na systemach Windows ta flaga jest domyślnie już wyzerowana, a funkcje WinApi nigdy jej nie ustawiają, rzadkością są w ogóle przypadki, że kiedykolwiek jest ona ustawiana instrukcją STD, chyba jedynie w jakichś egzotycznych przykładach napisanych w assemblerze.

W binarce go.exe znajdziemy tego całą masę:

W 1 bloku kodu (a co, kto bogatemu zabroni):

Jeszcze tutaj:

I w całej masie kodu wszędzie CLD. Co ciekawe nie znalazłem w binarce ani jednej instrukcji STD, która sprawiałaby, że trzeba użyć CLD ;)

2. Undefined behaviour

W assemblerze znajdziemy instrukcje, których znaczenia nie zna nawet taki wymiatacz assemblera jak Rysio z Klanu. Wśród nich znajdziemy tajemniczą instrukcję UD2, lata temu stosowałem ją w metodach antydebug ze względu na to, że deasemblery w popularnych debuggerach nie wiedziały co z nią robić. Obecnie jest udokumentowana jako celowo nieprawidłowa instrukcja do testowania (sprytnie).

Jej zastosowanie w kodzie generowanym w nowoczesnych kompilatorach praktycznie nie istnieje. Na co komuś instrukcja, która niczego nie robi (oprócz linuxiarzy)? W kodzie kompilatora Go znalazła zastosowanie:

Jest ona tu zastosowana jako bariera, upewniająca, że po runtime_panicindex nic nie zostanie wykonane. Tak dla picu i marnotrastwa miejsca. To tak jakby po ExitProcess() dać dla bajery int 3.

3. Optymalizacyjne potworki

Są takie instrukcje, których manuale zarówno Intela jak i AMD rekomendują omijać szerokim łukiem. Dlaczego? Bo to archaiczne twory z początków procesorów 386, które przetrwały na liście instrukcji, jednak nikt ich już od tamtych czasów nie optymalizował w silikonie i nikt ich nie używa, oprócz paru szalonych programistów assemblera, którzy optymalizują kod pod względem rozmiaru.

Jedną z takich instrukcji jest xchg, która wymienia zawartość 2 rejestrów, od czasów MS-DOS nie widziałem, żeby jakikolwiek kompilator generował tą instrukcję w kodzie (oprócz ręcznych wstawek w assemblerze). A tutaj mamy tego całą masę:

Na pohybel  manualom :P, już nawet nie mówię, że ten kod wygląda tak biednie, że mam ochotę dać darowiznę, nawet niewprawiony programista assemblera lepiej by to napisał.

Kolejny:

Podpowiem tym razem: jmp runtime_call16 i jesteście 2 bajty do przodu, niczego nie tracąc, a wręcz zyskując, jmp w tym wypadku to relatywny skok, więc wynikowy plik PE EXE będzie jeszcze mniejszy o rozmiar relokacji używany w instrukcji mov eax,adres_w_pamieci.

I znowu:

Każdy lamer zaczynający programować w assemblerze później czy prędzej dowie się, że instrukcje takie jak and, or, xor ustawiają również flagi w zależności od wyniku wykonanej operacji, nie trzeba dodatkowo stosować porównania do sprawdzenia flagi.

Go lubi nadmiarowy kod:

Komentarz? 3 instrukcje do sprawdzenia czy rejestr EAX zawiera wartość 0. Na całym świecie kompilatorowym załatwiają to 2 instrukcje test eax,eax lub cmp eax,0 + skok.

4. Na pohybel Windowsowi

Nikt o zdrowych zmysłach nie korzysta w produkcyjnym kodzie ze struktur takich jak np. PEB czy TEB, gdyż wskutek ewolucji systemów operacyjnych, ich pola zawsze mogą się zmienić. Nikt oprócz kompilatora go.exe, który radośnie wykorzystuje go do śledzenia stosu i w razie potrzeby zwiększania jego rozmiaru:

Kto o zdrowych zmysłach i wbrew dokumentacji, która wyraźnie zaznacza, że struktury te mogą się zmienić w kolejnych wersjach systemu operacyjnego decyduje się na korzystanie z nich? Chyba ktoś, kto nigdy swojego oprogramowania nie testował na 2 różnych wersjach systemu Windows, albo nie zakłada, że będzie on musiał działać na innym OS.

5. Mamy swój standard

W świecie Windows istnieje określony sposób wywoływania funkcji WinApi – określany mianem stdcall, który zakłada, że kolejne parametry są wrzucane na stos i następuje wywołanie zadanej funkcji. Seria instrukcji push zakończona instrukcją call. Proste? Za proste! Skomplikujmy to. Oto jak wygląda wywoływanie funkcji WinApi w go.exe:

Czyli istnieje sobie wrapper o sweetaśnej nazwie runtime_stdcall(), któremu przekazywana jest funkcja jako wskaźnik, parametry, a wrapper sam wywołuje instrukcję call. Po co? Kto bogatemu zabroni!

Wnioski

Pierwszy jest taki, że jestem cholernie zawiedziony jakością kodu Go, jak widać można mieć górę pieniędzy, a i tak być technologicznie zacofanym do epoki kamienia łupanego.

Wiem, że w dobie nowych procesorów Intel Core i7 mało kto zwraca uwagę na takie detale, jednak nie wiem czy chciałbym aby moje oprogramowanie i algorytmy były kompilowane do takiej formy jaką obecnie oferuje najnowszy kompilator Go. To się po prostu nie dodaje jak mówi Mariusz Max Kolonko…

Na koniec jeszcze dump z programu Hello World w Go:

Jeśli i wy doceniacie piękno assemblera i gardzicie lipnym kodem wpiszcie w komentarzach waszą wersję tej funkcji, najlepszy wpis czeka nagrodapierwszy plakat formatu PE (bo już nie mam go gdzie powiesić). Nie spodziewam się rewelacji z waszej strony, ale kto wie ;)

Willy – Monospaced Font

ascii-art x 1font x 1monospaced x 1willy x 3

Dawno dawno temu, mój przyjaciel z demosceny Willy (aka Czarny Jobacz) - stworzył font, który idealnie nadawał się do wyświetlania ascii artów.

Problem w tym, że ów font był w formie bitmapy i tak przeleżał dobrych kilka(naście) lat na moim dysku, do niedawna, gdy znalazłem mały skrypt, napisany przez autora Putty, czyli Simona Tathama, do konwersji danych, zapisanych w postaci skryptu tekstowego do wyjściowego pliku .fon.

W efekcie prezentuję wam font o nazwie Willy, a wyglada on tak:

Willy-Monospaced-FontFont, skrypt generujący oraz oryginalną bitmapę możecie ściągnąć tutaj:

Willy-Monospaced-Font

Historia bezstratnych algorytmów kompresji

DEFLATE x 1Huffman x 1kompresja x 2LZ77 x 1lzma x 2LZS x 1LZW x 1Shannon-Fano x 1zip x 2

packageZnakomity artykuł, prowadzący po historii bezstratnych algorytmów kompresji:

http://ieeeghn.org/wiki/index.php/History_of_Lossless_Data_Compression_Algorithms

Jakby kogoś ciekawił temat kompresji to polecam również popularne forum:

http://encode.ru

Gdzie możecie porozmawiać z takimi gwiazdami w świecie kompresji jak – Matt Mahoney, oprócz tego znajdziecie tam źródła wielu eksperymentalnych algorytmów kompresji.

TreeStructInfo – nowy sposób przechowywania ustawień konfiguracyjnych

FPC x 1ini x 2konfiguracja x 1Pascal x 1

Dzisiaj natrafiłem na projekt przechowywania danych konfiguracyjnych stworzonych przez jednego z moderatorów forum 4programmers .

tree_struct_info

Opis:

TreeStructInfo to projekt uniwersalnego formatu tekstowych i binarnych plików konfiguracyjnych, przeznaczonych do przechowywania ustawień aplikacji i gier w formie drzew danych. Umożliwia tworzenie zarówno prostych, jednoplikowych konfiguracji, jak i złożonych systemów konfiguracyjnych, składających się z wielu powiązanych ze sobą plików.

Format ten zaprojektowany został tak, aby był przyjazny dla człowieka, ale także jak najbardziej funkcjonalny i prosty do przetwarzania. Forma tekstowa daje możliwość tworzenia i edytowania plików w dowolnych edytorach, forma binarna zaś sprzyjać ma szybkości przetwarzania plików.

Przykład przechowywania danych tekstowych, liczbowych, binarnych oraz w atrybutach:

Przykładowy plik konfiguracyjny:
http://treestruct.info/pl/format/1.0.htm#idSampleFile

Przykłady przechowywania danych tekstowych, liczbowych i binarnych:
http://treestruct.info/pl/format/1.0.htm#idAttrValuesDataTypes

Opis plików konfiguracyjnych w formie tekstowej:
http://treestruct.info/pl/format/1.0.htm#idTextForm

Opis plików konfiguracyjnych w formie binarnej:
http://treestruct.info/pl/format/1.0.htm#idBinaryForm

Strona projektu -  http://treestruct.info