Optymalizacja programow pod katem wielkosci kodu
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1.Zerowanie rejestrow
2.Przenoszenie do rejestrow 32bitowych wartosci z zakresu 0-255
3.Sprawdzanie wartosci zwracanych przez funkcje API
4.Wymienianie wartosci rejestrow
Programisci piszacy w asemblerze mysla, ze jesli pisze sie w asemblerze
ich
kod jest juz max zoptymalizowany(w koncu to asembler! ;), ale jak sie
przekonalem
wiele drog wiedzie do tego samego celu.
1.Zerowanie rejestrow
~~~~~~~~~~~~~~~~~~~~~
a) mov eax,0 5 bajtow ; B0,00,00,00,00
b) xor eax,eax 2 bajty ; 33,C0
c) sub eax,eax 2 bajty ; 2B,C0
d) and eax,0 3 bajty ; 83,E0,00
jak sie okazuje nawet najprostsza operacja moze zajac nawet 5 bajtow,
ale
jesli zastosujemy np. xor-a ta sama operacja zajmie 2 bajty w wynikowym
kodzie
programu.Czesto wykorzystuje sie wartosc 0 jako paremtr dla jakichs tam
funkcji api
a) standardowe rozwiazanie
push offset szSansSerif ; lpFace ; 5bajtow
push 0 ; pitch and family ; 2b
push 0 ; output quality ; 2b
push 0 ; clipping precision ; 2b
push 0 ; output precision ; 2b
push 1 ; char set identifier ; 2b
push 0 ; strikeout attribute flag ; 2b
push 1 ; underline attribute flag ; 2b
push 0 ; italic attribute flag ; 2b
push 400 ; font weight(normal) ; 5b
push 0 ; base-line orientation angle ; 2b
push 0 ; angle of escapement ; 2b
push 0 ; logical average character ; 2b
push 0Dh ; logical height of font ; 2b
call CreateFontA
laczna ilosc bajtow instrukcji potrzebnych do zapamietania paramertow
wywolania procki CreateFontA zajmie w tym przypadku 34 bajty
b) zoptymalizownae rozwiazanie
sub eax,eax ; 2b
push offset szSansSerif ; lpFace ; 5b
push eax ; pitch and family ; 1b
push eax ; output quality ; 1b
push eax ; clipping precision ; 1b
push eax ; output precision ; 1b
push 1 ; char set identifier ; 2b
push eax ; strikeout attribute flag ; 1b
push 1 ; underline attribute flag ; 2b
push eax ; italic attribute flag ; 1b
push 400 ; font weight(normal) ; 5b
push eax ; base-line orientation angle ; 1b
push eax ; angle of escapement ; 1b
push eax ; logical average character ; 1b
push 0Dh ; logical height of font ; 2b
call CreateFontA
tym razem 27bajtow, w sumie maly zysk w porownaniu z poprzednia
procedura
ale czasami te pare bajtow moze sie przydac do czegos innego.
c) serie
Gdy w "serii" musimy zapamietac na stosie zera zamiast
push 0 ; 2bajty
push 0 ; 2bajty
push 0 ; 2bajty
push 0 ; 2bajty
push 0 ; 2bajty
push 0 ; 2bajty
push 0 ; 2bajty
================================
+ 14bajtow
oraz
sub eax,eax ; 2bajty
push eax ; 1bajt
push eax ; 1bajt
push eax ; 1bajt
push eax ; 1bajt
push eax ; 1bajt
push eax ; 1bajt
push eax ; 1bajt
===============================
+ 9bajtow
mozna wykonac to tak
sub eax,eax ; 2bajty
push 7 ; 2bajty
pop ecx ; 1bajt
@save_args:
push eax ; 1bajt
loop @save_args ; 2bajty
================================
+ 8bajtow
d) jesli zamierzamy wyzerowac rejestr edx normalnie robimy to przez np
xor edx,edx
ale mozna zrobic to jeszcze prosciej korzystajac z instrukcji
cdq(ConvertDoubletoQuad)
Instrukcja cdq powoduje,ze rejestr edx jest wypelniany bitem znaku z
eax(31bit) wiec jesli
wiemy ze w eax mamy np 1 to wykonanie instrukcji cdq spwoduje wyzerowanie
edx,nalezy
tylko uwazac stosujac cdq, czy bit znaku w eax jest ustawiony czy nie,bo
jesli w eax
mielibysmy liczbe np
eax=80000001h=10000000000000000000000000000001b
^bit znaku
to wykonanie cdq spowoduje,ze edx zostanie zapelniony bitem znaku eax
czyli 1,
wiec w edx znajdzie sie 0FFFFFFFFh.Instrukcja cdq zajmuje tylko jeden
bajt.
2.Przenoszenie do rejestrow 32bitowych wartosci z zakresu 0-255
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
a) mov eax,7Fh 5 bajtow ; B0,FF,00,00,00
b) sub eax,eax 4 bajty ; 2B,C0
mov al,7Fh ; B0,FF
c) push 7Fh 3 bajty ; 6A,FF
pop eax ; 58
Czesto potrzebne jest przeniesienie wartosci z zakresu od 0-FF do
rejestru 32bitowego
Rejestr 32bitowy przyjmuje dane w porcjach 32bitowych(no chyba stad ta
nazwa ;)
a wiec jesli wykorzystamy w kodzie programu cos takiego
mov eax,4
instrukcja zajmie 5 bajtow ; B0,04,00,00,00
4-ka jest traktowana jako 32bitowa wartosc gdy korzystamy z 32bitowych
rejestrow.
Najbardziej zoptymalizowanym rozwiazaniem wydaje sie byc wykorzystanie
stosu do
prznoszenia wartosci 0-127 do 32bitowych rejestrow
push 4 ; 6A,04
pop eax ; 58
wow 3 bajty, mimo, ze zajmuje to wiecej miejsca w notepadzie po
skompilowaniu kodu
zajmuje mniej bajtow na dysku!Nalezy w tym miejscu wspomniec,ze kompilator
zapisze
skrocona forme push-a jesli wartosc bedzie miescic sie w granicach
0-127,jesli przekroczymy
ta wartosc i wymusimy na kompilatorze zapisanie skroconej formy push-a dla
dowolnej
wartosci bajtu np definiujac makro:
pushb macro byteval
db 06Ah,byteval
endm
pushb 080h ; zapamietaj na stos 128
pop eax
po wykonaniu tych instrukcji w eax znajdzie sie wartosc
0FFFFFF80h(-80h)ale dlaczego nie
00000080h?Heh liczby z zakresu 128-255 traktowane sa jako liczby ujemne ,jesli
uzyjemny skroconej formy push-a dla takiej liczby,na pozostale miejsca
dword-a
zostanie skopiowany znak bajtu(1 binarna oznacza minus,0 plus)czyli w
wypadku
wartosci 80h(128dec) bit 1 i tak zostanie zapisany dword na stosie jako
rozszerzony
o znak
00000000000000000000000010000000 = 00000080h
^bit znaku
11111111111111111111111110000000 = FFFFFF80h
^^^^^^^^^^^^^^^^^^^^^^^^bity oznaczajace znak
heh, zycie jest jak pudelko czekoladek...;).
Ale jest jeszcze inne rozwiazanie,zamiast "marnowac" 1 bajt
uzywajac np
mov eax,255 ; 5bajtow
mozna w zastepstwie tego uzyc
xor eax,eax ; 2bajty
mov al,255 ; 2bajty
3.Sprawdzanie wartosci zwracanych przez funkcje API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
W czasach VC++ i delphy chyba nikt juz nie przejmuje sie wartosciami
zwracanymi przez
funkcje api, a czesto wlasnie sprawdzanie wartosci moze skrocic czas
debugowania
programu.Ok ale to nie na temat, tak wiec funkcje jak to funkcje zwracaja
jakies wartosci,
w przypadku funkcji WinAPI zwracana wartosc zawsze znajduje sie w
rejestrze eax.W zaleznosci
od funkcji w eax zwracane sa wartosci np 0,-1,uchwyt pliku itp.Np funkcja
CreateFileA
zwraca w eax wartosc -1 gdy nie mamy dostepu do pliku, ktory akurat
chcielismy otworzyc.
Ale juz np.funkcja CreateIcon zwraca w eax 0 jesli wystapil blad.W kazdym
razie bez winapi.hlp
programista piszacy pod win nie ma latwego zycia ;).Jak sprawdzic czy w
eax jakas funkcja
nie zwrocila przypadkiem wartosci informujacej o tym ze wystapil jakis
blad
push ...
call LoadBitmapA
w helpie czytamy, ze
"If the function succeeds, the return value is the handle of the
specified bitmap.
If the function fails, the return value is NULL. "
push ..
call LoadBitmapA
cmp eax,0 ; 83,F0,00
jz @wystapil_blad
ale chwilka instrukcja cmp eax,0 zajmuje 3 bajty.Czy nie da sie tego
zrobic troszke
inaczej?Zamiast cmp mozemy zastosowac operacje logiczne takie jak or i
test
call LoadBitmapA
or eax,eax ; 0B,C0
jz @wystapil_blad
lub
call LoadBitmapA
test eax,eax ; 85,C0
jz @wystapil_blad
obie instrukcje ustawia nam flage zerowa jesli eax==0 a wiec daja one
taki sam
efekt jak cmp eax,0 ale zajmuja o 1 bajt mniej.Mozna jeszcze ten kod
zoptymalizowac
call LoadBitmapA
xchg eax,ecx ; 1bajt
jecxz @wystapil_blad ; instrukcja jecxz zajmuje 2 bajty(tyle samo co skoki
jxx short)
Nalezy uwazac korzystajac z jecxz poniewaz jest to skok warunkowy o
zakresie -127 do 128
i jezeli przekroczymy ten zakres przy wlaczonej dyrektywie jumps tasm
przetlumaczy jecxz na
call LoadBitmapA
xchg eax,ecx
jecxz @dummy
jmp @next
@dummy:
jmp @wystapil_blad
@next:
Funkcje api czasami jako kod bledu zwracaja w eax wartosc
-1(0FFFFFFFFh), jak zatem
sprawdzic czy taka wlasnie wartosc zwrocila jakas funkcja
np.CreateFileA?Najprostszy
rozwiazaniem jest oczywiscie
call CreateFileA
cmp eax,-1 ; 83,F0,00
je @wystapil_blad
a czy nie ten sam efekt da
call CreateFileA
inc eax ; jesli w eax bylaby wartosc -1 po zwiekszeniu o 1 zostalaby
ustawiona
je @wystapil_blad ; flaga zerowa
dec ; jesli nie wystapil blad zmniejszamy wartosc w eax o 1 tak aby
; przywrocic oryginalna wartosc zwrocona przez funkcje
w tym przypadku kod wynikowy bedzie o 1bajt mniejszy
4.Wymienianie wartosci rejestrow
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Np mamy w eax 4 w edx 98 i chcemy, zeby w eax znalazla sie liczba 98 a
w edx 4 czyli
zeby rejestry zamienily sie wartosciami.Jak to zrobic
push eax
push edx
pop eax
pop edx
rozwiazanie to zajmuje 4 bajty,mozna jeszcze tak
mov ebx,eax
mov eax,edx
mov edx,ebx
"jedynie" 6 bajtow.Ale istnieje instrukcja xchg(eXCHange -
wymien,zamien)
ktorej rozmiar wynosi 1 bajt jesli jednym z rejestrow ktorych wartosci
wymieniamy
jest rejestr eax np
xchg eax,edx ; 92h
ale juz
xchg edx,esi ; 87h,0D6h
Jak sie okazuje wiele instrukcji jest zoptymalizowane pod wzgledem
wielkosci jesli
rejestrem na ktorym operujemy jest eax np.
add edi,400000h ; 81h,0C7h,00h,00h,40h,00h 6bajtow
add eax,400000h ; 05h,00h,00h,40h,00h 5bajtow
jak widac instrukcja korzystajaca z edi ma rozmiar o 1 bajt wiekszy niz
ta sama
instrukcja korzystajaca z eax
Ostatnie slowo
~~~~~~~~~~~~~~
Moze zdawac sie wam ze na cholere ta cala optymalizacja, ale jesli
przypadkiem zamiast
nopowania jakiegos fragmentu kodu bedziecie potrzebowac jeszcze jakichs
dodatkowych instrukcji
a przestrzen do wykorzystania bedzie bardzo skromna, wiedza o
optymalizacji moze sie
baaaardzo przydac.
bart^CrackPl
cryogen@free.net.pl
|