| Corso 11 UIC Un po' di sana ottimizzazione |
||
| Data |
by "Winebag" |
|
| 31/05/2001 |
Published by Quequero |
|
|
|
Mi complimento con te perché il tutorial è dettagliatissimo e sei stato davvero bravo, ma avevo specificato anche in mailinglist che non potevi togliere le routine ma solo ottimizzarle…Perché? Perché se ognuno toglie la sua istruzione allora si riscrive il programma e il mio intento non era quello di farvi scrivere un programma ma quello di ottimizzarne uno… |
Certo che il cinese è proprio una lingua del cazzo! Flavio Oreglio |
| .... |
E-mail: W1n3b4g@usa.net |
.... |
| Difficoltà |
(X)NewBies ( )Intermedio ( )Avanzato ( )Master |
|
In questo corso dovremo fingerci programmatori,ma lo faremo a modo nostro: il nostro compito infatti è quello di allenarci a scrivere funzioni nel minor spazio possibile. Per una volta il nostro migliore amico sarà il TASM, ma non dimentichiamo certo il debugger.
Un po' di sana ottimizzazione
(Coding)
Written by Winebag
| Introduzione |
Ciao a tutti, questo è il mio primo tute per l' UIC, pertanto il livello sarà abbastanza basso e comprensibile. Probabilmente vi sembrerà disordinato, infatti non segue necessariamente un ordine logico e compaiono in più punti le stesse parti di codice; questo perché spesso prima di togliere alcune istruzioni è necessario toglierne altre, ma lo capirete leggendo. Le istruzioni che andrò di volta in volta a togliere dovrebbero essere in rosso.
| Tools usati |
| URL o FTP del programma |
Il programma e i sorgenti li trovate al sito della UIC.
| Notizie sul programma |
Il programma.....Non fa assolutamente nulla, si preoccupa solo di generare un numero quasi-random utilizzando il più possibile i cicli di clock (parole di Quequero)…Rendiamo grazie a Quequero :) NdQue.
| Essay |
Allora ,la prima cosa che facciamo è eseguire il programma src.exe e scoprire
che non fa davvero un cazzo : dopo aver fatto alcune seghe alla nostra CPU salta
fuori con una messagebox che ha il solo scopo di prenderci per il culo seguita
da una seconda che ci saluta con un numero non troppo random , ma lo mettiamo
a posto noi :-). Incominciamo a dare uno sguardo ai sorgenti, il programma legge
il tempo dall' avvio di win genera alcuni numeri a caso che mette in un buffer
che provvederà a sodomizzare con vari cicli e infine a mostrarcelo dopo averci
detto il tempo che ha impiegato il programma.
Poiché ci saranno da compilare cose e cose ci scriviamo un BAT che ci velocizzerà
il lavoro
----------------------------compile.bat----------------------------------
@echo off
cd \tasm
tasm32 -ml -m5 -q c:\windows\desktop\uic11\%1.asm
tlink32 -Tpe -aa -x -c %1.obj ,,, import32
copy %1.exe c:\windows\desktop\uic11\%1.exe
del %1.exe
del %1.obj
--------------------------------------------------------------------------
Uso: compile nome(ovviamente in c:\windows\desktop\uic11). Il BAT va messo in
windows\command; se siete troppo legati alla riga di comando potete evitare
tutto ciò, io l'ho messo perché questo in fondo è un tutorial sull'ottimizzazione.
Quando si ottimizza qualcosa si cerca di limitare l'uso della CPU accelerando i tempi, di ridurre le dimensioni dell'eseguibile e, ovviamente di non alterare il funzionamento. Noi baderemo soprattutto agli ultimi 2 punti, ma si pone un problema:in 5 passaggi vengono inseriti valori in eax che variano di volta in volta, per cui non possiamo verificare se effettivamente non abbiamo alterato il funzionamento. Ma noi, mica scemi, possiamo modificare queste istruzioni in modo da renderle costanti. Io le ho sostituite nell'ordine così:
mov eax,00112233h;call GetTickCount
mov eax ,12345678h;db 0fh, 31h
mov eax ,87654321h;db 0fh, 31h
mov eax, 12345678h;db 0fh, 31h
mov eax,00112234h;call GetTickCount
I byte 0F 31 indicano l' istruzione RDTSC che copia nella qword EDX:EAX il valore
contenuto in un registro di 64 bit che si occupa di contare i cicli di clock.Avrei
perciò dovuto inserire dei valori in edx, ma ho visto che era superfluo. Il
2°RDTSC è all'interno di un loop ma lo vedremo quando sarà il caso.
Adesso possiamo fare 2 copie: 1 la chiamiamo test.asm e la compiliamo(compile
test), sull'altra lavoreremo controllando sempre che i numeri generati coincidano.
Alla fine ripristineremo le istruzioni originali che ho lasciato di commento.
Start:
call GetTickCount ; Iniziamo il conteggio del tempo
push eax <---salva il valore nello stack
mov ecx, eax <---inutile
imul eax, 256<---------moltiplica il n° di cicli per 256 (100h)
mov ecx, eax<--- e lo copia in ecx
step1:
imul eax, 4096<---- moltiplica eax x 4096(1000h)
push edx<-----salva edx nello stack
mov edx,ecx<---lo sovrascrive
pop edx<---lo ripristina
loop step1
Questa routine moltiplica eax x 4096 un casino di volte. Ciò corrisponde a uno shift a dx di 12. Quindi eax , sia esso positivo o negativo dopo al massimo 3 cicli varrà 0.Per cui possiamo sostituire al ciclo, per adesso, xor eax,eax:
xor eax,eax<-- step 1
mov eax, 0<--vale già 0!
sub edx, edx<--azzera edx
mov eax, ecx<--sovrascrive eax
mov eax,12345678h;db 0fh, 31h<----sovrascrive
eax e edx
and eax, 0000FFFFh<--azzera la word alta
di eax
mov ecx, eax<--lo mette in ecx
È facile capire che le prime 4 istruzioni sono inutili quindi via tutte.
Questo è solo il 1° paragrafo dedicato allo step 2. Spiego dopo perchè non faccio tutto adesso.
push vari
step2:
.......
pop vari
mov ecx,0
step3:
mov eax, dword ptr[calcbuf+ecx]<---qui viene
sovrascritto eax
....db 0fh, 31h<--qui edx
Quequero seguendo le regole del buon programmatore prima del ciclo ha salvato nello stack i registri (bravo Que :-) Noi però non badiamo tanto all' estetica e, purtroppo per lui, togliamo via push e pop superflui.Per prima cosa ebx ,edi e esi che non vengono toccati dal loop, poi ecx che viene azzerato subito dopo.Infine possiamo sbattercene le balle di ripristinare eax e edx perchè tanto vengono sovrascritti poco dopo.
mov ecx ,0
step3:
mov eax, dword ptr[calcbuf+ecx]<---mette
una dword qualunque in eax
mov dword ptr[calcbuf+ecx], eax<---la riscrive
ESATTAMENTE dove era prima
inc ecx
cmp ecx, 99
jnz step3<---ripete il ciclo per 99 dword
mov ecx, dword ptr[calcbuf]<---sovrascrive ecx
and ecx, 0000FFFFh
mov eax,12345678h;db 0fh, 31h<--sovrascrive
eax
Non merita ulteriori commenti cancelliamo immediatamente le istruzioni in rosso e passiamo al prossimo paragrafo.
mov ecx, dword ptr[calcbuf]
and ecx, 0000FFFFh
step5:
mov edx, eax
mov esi, ebx
mov ebx, edx
mov eax, esi
loop step5
mov eax,00112234h;call GetTickCount<-----qui
riscrive eax e edx
mov ebx, eax<---qui ebx
Lo step 5 oltre ovviamente ad azzerare ecx riscrive continuamente i registri esi,edx,ebx,eax.Questi ultimi 3 vengono modificati poco dopo, mentre gli altri 2 non vengono più utilizzati in lettura.Quindi eliminiamo tutte le istruzioni senza problemi.
Come promesso ritorniamo allo step 2.Non notate un po' troppi puntatori a calcbuf?Se avete mai fatto caso agli opcode in Dasm o Sice, i puntatori occupano un macello di byte, questo perché ovviamente sono necessari 4byte per l'indirizzo oltre ai byte dell' istruzione vera e propria.Allora vediamo di fare un po' di pulizia osservando tutto il sorgente.
mov dword ptr[calcbuf+8], edx
mov dword ptr[calcbuf+8], eax
rcl dword ptr[calcbuf+8], 135<--non è seguita
da un salto condizionato da CF
Incominciamo dalla dword [calcbuf+8]:quelli sopra sono gli unici riferimenti. Come si nota il puntatore compare sempre a sinistra, questo vuol dire che vi si accede solo in scrittura, perciò possiamo eliminare tutto quanto.
Ora ci rimangono [calcbuf]che costituirà il numero random e[calcbuf+4] a cui
il programma accede sempre direttamente ,infatti abbiamo eliminato lo
step 3 in cui accedeva usando il puntatore[calcbuf+ecx].Ogni dword può stare
comodamente in un registro e noi abbiamo esi e edi che da quando abbiamo liberato
ai punti 2 e 4 non fanno più un cazzo(spiegato il motivo per cui sono ripassato
dallo step2 e non ho fatto tutto prima).Ora è sufficiente sostituire al posto
di [calcbuf] , esi e edi invece di[calcbuf+4] (e NON il contrario).
Compiliamo e testiamo il risultato con quello di test.exe, ma stranamente questi
non coincidono eppure abbiamo fatto tutto bene e al passo precedente coincidevano.Cosa
sarà successo? Di chi è la colpa? Quequero avrà voluto divertirsi alle nostre
spalle? E se in vi stessi prendendo per il culo? E se in realtà coincidessero?
E se vi dicessi cazzate? Abbiamo di nuovo bevuto troppo? O forse Quequero?Il
seguito alla prossima.
Adesso dovrei dirvi cosa è successo,probabilmente ci siete già arrivati, ma non posso dirvelo adesso occorrerebbe un po' di suspense per cui vi metto un po' di nop:
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
90
Benissimo adesso scopriamo chi è il colpevole: il buffer overflow. Se
proviamo a debuggare l'eseguibile di partenza scopriamo che una chiamata alla
funzione wsprint sovrascrive calcbuf.
call wsprintf, offset buffer, offset tempo, ebx
Infatti buffer comincia 80 byte prima di calcbuf, mentre "tempo"
è lungo più di 100 byte a cui si aggiunge la lunghezza di ebx, che è il numero
di millisecondi che impiega il programma.Questo ci spiega il motivo per cui
uscivano sempre gli stessi valori all' inizio, che potevano variare solo al
variare della lunghezza del n° di ms. Ad esempio 168632609 corrisponde
alla dword 21 21 0D 0A che è un pezzo del testo della 1° messagebox.
Adesso possiamo scegliere tra 2 soluzioni.
1_ Se seguissimo il principio di non modificare il funzionamento potremmo agevolmente eliminare tutta la 1a parte che si preoccupa di randomizzare calcbuf, potremmo dire di aver rimpicciolito l'area code a sufficienza e andare a giocare tutti quanti a briscola. Ma in questo modo tutto il lavoro perderebbe di significato e non sarebbe nemmeno troppo utile; per cui abbandoniamo questa idea.
2_Ora siamo obbligati a modificare il funzionamento ,contro il principio del punto 0, ma in fondo correggiamo un errore che forse Quequero ha messo per vedere se siamo veramente dei fessi ma in questo modo noi lo smentiamo.Per correggere questo bug non dobbiamo fare un cazzo, infatti con la mossa del paragrafo precedente l'abbiamo risolto senza volerlo quindi torniamo a test.asm e lo correggiamo anche lì:
step5:
mov edx, eax
mov esi, ebx
mov ebx, edx
mov eax, esi
loop step5
mov esi,dword ptr[calcbuf]<--aggiungiamo
qui questa riga
.....
call wsprintf,offset buf2,offset testo,esi<-- invece di dword ptr[calcbuf]
Facciamo un confronto: entrambe generano il numero -433958592 e passiamo al prossimo punto.
step2:
mov esi, eax<--mette eax in esi
mov edx, esi<--lo copia in edx
mov eax ,87654321h;db 0fh, 31h<--questo riscrive
eax e edx
(mov edx,xxxxx)<--ma di questo non ci preoccupiamo
add edx, eax
mov edi, eax
add esi, eax
mov edx, esi<--perchè qui edx viene riscritto
add eax, edx
mov eax, edi
loop step2
Come avevo preannunciato al punto 0 di edx ce ne possiamo fottere perchè viene riscritto senza essere copiato da altre parti quindi eliminiamo un po' di istruzioni.
step2:
mov esi, eax
mov eax ,87654321h;db 0fh, 31h
mov edi, eax
add esi, eax
mov edx, esi<---dopo aver tolto l'istr sotto edx non serve più
add eax, edx<----aggiunge a eax edx
mov eax, edi<--ma gli scrive sopra edi
loop step2
Ovviamente bisogna prima togliere add eax, edx dopodiché ci sarà consentito
eliminare mov edx,esi.
Per capire il prossimo passaggio bisogna immaginare di essere all'ultimo ciclo.
step2:
mov esi, eax<---copia il penultimo valore
di eax
mov eax ,87654321h;db 0fh, 31h<---"estrae"
l'ultimo valore di eax
mov edi, eax<----lo copia in edi
add esi, eax<---lo somma a esi
mov eax, edi<---eax e edi contengono già lo stesso valore
loop step2
Dopo n cicli avremo esi=eax(n-1)+eax(n) e edi=eax(n). Eax(n-1) e eax(n) sono a tutti gli effetti casuali per cui si potrebbe pensare di eliminare il loop mettendo per 2 volte RDTSC. Forse questa è la soluzione migliore come numero di byte, ma secondo me altera il funzionamento del programma perchè un loop che si ripete un numero casuale di volte rende tutto "un po' più casuale".Quindi io seguirò questo principio anche se forse Que l'aveva pensata diversamente.Concludendo questo è ciò che rimane del ciclo.
step2:
mov esi, eax
(mov eax ,87654321h);db 0fh, 31h
add esi, eax
loop step2
mov edi, eax<--essendo l'ultimo valore di eax lo possiamo togliere dal ciclo
Sarebbe possibile anche lasciare l'ultima istruzione nel ciclo utilizzando lo stesso numero di byte, ma in questo modo ripeteremmo la stessa istruzione più volte utilizzando inutilmente il processore.
mov ecx, esi<--mette esi, reduce dallo step2, nel contatore
and ecx, 0000FFFFh<---ne azzera la parte
alta
mov eax,12345678h;db 0fh, 31h<--estrae un
valore random
step4:
and esi, eax<---fa l'and bit a bit con eax in esi
add edi, eax
mov ebx, edi<--sposta edi in ebx
add eax, ebx<---- e lo somma a eax ; più velocemente add eax, edi
mov esi, eax<---riscrive esi
mul esi<----li vediamo dopo
mul esi
mul esi
mul esi
mul esi
mul esi
add eax, 1<---3Byte!?
inc eax
add eax, eax
div esi
loop step4
Passiamo ad un analisi un po' più matematica della routine(questo è totalmente inutile e siete autorizzati a saltarlo):
mul esi<--moltiplica eax x esi e lo mette in edx:eax
inc eax
add eax, eax<---raddoppia eax
div esi<--divide edx:eax per esi e lo mette
in eax
In questo passaggio non eliminiamo alcuna istruzione, tuttavia ho preferito
inserirlo per analizzare queste istruzioni. Debuggando mi sono accorto in diversi
casi che eax dopo queste 4 istruzioni era uguale al valore che aveva prima di
esse aumentato di 1.Prima di cambiare qualcosa mi sono chiesto il perchè di
questo e se ciò fosse sempre vero. Dobbiamo pensare a cosa succede arrivati
a queste istruzioni.
Eax viene moltiplicato per esi e il risultato viene messo nella qword edx:eax;a
questa,prima di essere divisa per esi, viene aggiunto il valore contenuto in
eax dopo la moltiplicazione(aumentato di 2,ma per il nostro ragionamento non
è una variazione significativa); se questo valore è maggiore di esi, ma minore
di esi*2 può essere pensato come esi+k(con k<esi); edx:eax dopo quest'ultima
somma sarà esi*eax(0)+esi+k dividendo per esi
si otterrà eax(0)+1 (in eax)resto k (in edx);quindi risulta chiaro che quest'aumento
non avverrà sempre anche perchè se eax dopo la moltiplicazione è maggiore
di 80000000h, raddoppiandolo si ottiene un riporto.Ho anche pensato, viste le
premesse di poter eliminare inc eax, che , complessivamente aumenta di 2 eax,
una variazione minima che potrebbe in casi molto remoti influenzare il risultato:
se ad esempio esi per qualche strano caso valesse 2 con eax uguale per esempio
a 400h:
mul esi
edx:eax=800
inc eax
edx:eax=801
add eax, eax edx:eax=1002
div esi
edx:eax=801(contro 800 senza quell'istruzione)
(mov eax,00112234h);call GetTickCount<---Conta
il numero di millisecondi
mov ebx, eax<---lo sposta in ebx
pop eax<--- mette in eax il numero di millisecondi
pushati all'inizio nello stack
sub ebx, eax<-- fa la differenza tra i tempi
call wsprintf, offset buffer, offset tempo, ebx<--la
formatta(call bastarda!!!)
call MessageBox.......<---Ce la mostra
Qui possiamo evitare il travaso di registri semplicemente invertendoli risparmiando 2 byte:
......
pop ebx
sub eax,ebx
call wsprintf,ebx, offset tempo, eax
Già che siamo qui guardiamo l'ultima parte:
call wsprintf, offset buffer, offset tempo, eax
call MessageBox, NULL, offset buffer, offset
titolo, MB_OK OR MB_ICONSTOP
call wsprintf,
offset buf2, offset testo, esi
call MessageBox, NULL, offset buf2, offset
titol0, MB_OK OR MB_ICONQUESTION
call ExitProcess, NULL
Non sono uno spreco 20 Byte per 4 push a 2 a 2 uguali? Se pushamo un registro ci occupa un solo byte, scegliamo ebx perchè eax ecx e edx sono modificati dalle call:
mov ebx,offset buffer
call wsprintf, ebx, offset tempo, eax
call MessageBox, NULL, ebx, offset titolo,
MB_OK OR MB_ICONSTOP
mov ebx,offset buf2
call wsprintf,
ebx, offset testo, esi
call MessageBox, NULL, ebx, offset titol0,
MB_OK OR MB_ICONQUESTION
Il ciclo "loop" viene usato in assembler per ottimizzare, ma nulla
ci vieterebbe di cambiare ogni loop in 3 istruzioni:
dec ecx
test ecx,ecx
jnz xxxxx
Dove la seconda non è necessaria perchè dec ecx setta già ZF quando ecx vale
zero.Quindi scriveremmo in 3 Byte ciò che prima scrivevamo in 2, ma questa non
è eccessivamente ottimizzazione! Guardate però questo ciclo.
and eax, 0000FFFFh<---azzera la word alta di eax
mov ecx, eax<--copia tutto in ecx
step2:
mov esi, eax<-- vi ricordate?Quello che conta
sono gli ultimi 2 cicli e non importa il 1° eax
mov eax ,87654321h;db 0fh, 31h
add esi, eax
loop step2
Ecco, perché utilizzare inutilmente i registri a 32bit quando la natura ci ha dato quelli a 16?
and eax, 0000FFFFh<----non serve più
mov ecx, eax
step2:
mov esi, eax
mov eax ,87654321h;db 0fh, 31h
add esi, eax
dec cx
jnz step2
Quindi,2 byte sprecati, ma 5 guadagnati!
Adesso ritorniamo allo step4 dove facciamo lo stesso.
mov ecx, esi
and ecx, 0000FFFFh
;Solo perchè sono gentile :))))<--Grazie
Que, ma la togliamo
mov eax,12345678h;db 0fh, 31h
step4:
and esi, eax
add edi, eax
mov ebx, edi
add eax, ebx
mov esi, eax
mul esi<---qua dopo mettiamo il ciclo
mul esi
mul esi
mul esi
mul esi
mul esi
inc eax
add eax, eax
div esi
loop step4<---dec cx jnz step4
Avevamo lasciato in sospeso quei 6 mul esi da ottimizzare con un ciclo: l' ideale sarebbe un repz, ma come ci dicono i manuali sull'assembler vuole un'istruzione di stringa,io ho provato a fregarlo, ma si è accorto e non ha voluto collaborare. Potremmo usare bl per il ciclo interno usando 6B. Ci sarebbe anche il loop, ma ecx è già impegnato con il ciclo esterno, per cui andrebbe salvato ogni volta nello stack.Ma se usassimo bx come contatore del ciclo esterno e ecx per il ciclo annidato...
mov ebx, esi
mov eax,12345678h;db 0fh, 31h
step4:
add edi, eax
add eax,edi
mov esi, eax
push 6<--stanno per mov ecx,06 che però richiederebbe 5B
pop ecx<-mentre così ne bastano 3.
mul:
mul esi
loop mul
inc eax
add eax, eax
div esi
dec bx
jnz step4
Tutto quello che troverete in questo paragrafo è frutto di malattia mentale. Penso che ci possano essere almeno 2 errori, tuttavia lo faccio lo stesso e tolgo 3B inutili. Se dovesse mai capitarvi una situazione del genere NON FATE NIENTE DI QUELLO CHE TROVATE QUI, a meno che non sia una situazione di reale necessità. Chiaramente questa È una situazione di reale necessità.
call wsprintf, offset buffer, offset
tempo, eax
call MessageBox, NULL, offset buffer, offset titolo, MB_OK OR MB_ICONSTOP
call wsprintf, offset buf2, offset testo, esi
call MessageBox, NULL, offset buf2, offset titol0, MB_OK OR MB_ICONQUESTION
call ExitProcess, NULL
Non vi danno un po' fastidio questi null non sembra che ci vogliano pigliare per il culo? Se penso che occupano 6 interi Byte che potrebbero contenere cose molto più utili.Allora cosa facciamo, azzeriamo un registro(2 byte) e lo pushamo 3 volte(3B), oppure cerchiamo un registro già azzerato,magari ecx, ma ecx in quel punto non vale zero perchè una certa funzione pensa bene di cambiarlo. L'unico registro che potrebbe valere 0 è edi, infatti debuggando ho visto che all'inizio vale 0 e nessuna funzione lo modifica.Non so esattamente perché edi valga 0 e gli altri no; non ho mai trovato niente che spiegasse cosa c'è nei registri all' entry point, ma ho sempre visto edi=0 e penso che sia sempre così. Ammesso di usare edi si pone un altro problema edi compare già in 3 punti ed è quello che era[calcbuf+4]. Occorrerebbe un altro registro. Eax, ecx e edx vengono cambiati dalle funzioni, ebx e esi ci servono ,esp e ebp sono per lo stack. Ebp non è però indispensabile IN QUESTO programma, che,essendo molto semplice non fa largo uso dello stack. Per cui sostituiamo edi con ebp in quei 3punti:
mov ebp,eax
add ebp, eax
add eax,ebp
e pushamo edi al posto di null
call wsprintf, offset buffer, offset
tempo, eax
call MessageBox, edi, offset buffer, offset titolo, MB_OK OR MB_ICONSTOP
call wsprintf, offset buf2, offset testo, esi
call MessageBox, edi, offset buf2, offset titol0, MB_OK OR MB_ICONQUESTION
call ExitProcess, edi
Prima di passare al punto successivo, togliamo le istruzioni da noi modificate, rimettendo rdtsc dove l' avevamo tolto.L'area code dopo vari passaggi è così:
.code
Start:
call GetTickCount ; Iniziamo il conteggio del tempo
push eax
db 0fh, 31h
mov ecx, eax
step2:
mov esi, eax
db 0fh, 31h
add esi, eax
dec cx
jnz step2
mov ebp,eax
mov ebx, esi
db 0fh, 31h
step4:
add ebp, eax
add eax,ebp
mov esi, eax
push 6
pop ecx
mul:
mul esi
loop mul
inc eax
add eax, eax
div esi
dec bx
jnz step4
call GetTickCount
pop ebx
sub eax, ebx
mov ebx,offset buffer
call wsprintf,ebx, offset tempo, eax
call MessageBox, edi,ebx, offset titolo, MB_OK OR MB_ICONSTOP
mov ebx, offset buf2
call wsprintf,ebx, offset testo, esi
call MessageBox, edi,ebx, offset titol0, MB_OK OR MB_ICONQUESTION
call ExitProcess, edi
ends
end Start
Adesso misura 142B;probabilmente sarebbe stato possibile ridurre questo numero se si fosse proceduto diversamente; io però adesso non saprei più cosa togliere ,ma in ogni caso sono già soddisfatto di questo, anche perchè non l'avevo mai fatto prima. Possiamo passare al secondo obbiettivo del corso: rimpicciolire l'exe.
Adesso il nostro eseguibile misura 4kb, ma in realtà è pieno di byte zero,aggiunti
dal linker o da chiunque altro per separare gli oggetti. Questi 0 non è che
mi diano grosso fastidio, anche perchè in ogni caso un file sull'HD non può
occupare meno di 4kB. Ma Quequero ci ha chiesto di rimpicciolirlo e noi proveremo
a farlo. Prima di cominciare diamo uno sguardo all'exe tenendo presente lo schema
del PE. Ricordate anche la sintassi Intel in cui per word e dword i Byte
sono "rovesciati": 01 00 00 00 vuol dire 1. Il mio non vuole
essere un tutorial sul PE perché non ho né intenzione né le conoscenze per farlo,
per cui se non l'avete ancora fatto vi consiglio caldamente di leggervi qualcosa
sull'argomento. Io ho letto un' ottima guida in inglese , ma non ricordo più
esattamente da dove l'ho scaricata; senza andare lontano potete trovare sul
sito della UIC nella sezione Uicstore un onesto tutorial scritto da Andrea Geddon.
Un'altra precisazione: tutti i valori che troverete da qui alla fine del paragrafo
saranno in esadecimale, per cui se scrivo che 200 è una potenza di 2 non è solo
causa dell' alcol.:-)
Ma torniamo al nostro file e diamogli uno sguardo: inizia con l'header dos;
al byte 100 inizia l'header PE poi c'è la tabella degli oggetti e dal byte 600
comincia la sezione CODE ,seguita a intervalli di 200B dalle altre sezioni:
data , idata(dove sono linkate le funzioni), reloc, che, come ho trovato scritto,
per gli eseguibili non serve praticamente mai. Allora noi la eliminiamo senza
problemi:prendiamo il nostro editor esadecimale di fiducia e cancelliamo i Byte
a partire dall'offset ABC, cioè dopo wsprintfA, ultima funzione importata in
idata; lasciamo però un Byte 00 per evitare problemi. Dobbiamo cancellare
reloc anche dalla object table, ci portiamo dove inizia .reloc (byte 270 dove
dovrebbe esserci 2E 72 65 6C...) e copriamo di 0 i byte seguenti; ci resta
ancora da modificare il numero di oggetti nell header:
50 45 00 00 4c 01 03 00.
Adesso sembra facile, basta cancellare i byte 0 e cambiare gli offset nella object table; ma ci troviamo di fronte a un problema: il file align. Infatti uno dei campi del PE header è il file align che stabilisce l'allineamento degli oggetti all'interno dell'exe; ovvero ogni oggetto deve cominciare ad un offset fisico che sia multiplo di tale valore. Per non avere problemi basterebbe ridurlo a 1byte, ma esso deve essere una potenza di 2 maggiore di 200; io ho provato a diminuirne il valore anche solo a 100 e a riallineare gli oggetti al nuovo valore, ma mi diceva che sul computer c'erano delle restrizioni; mentre il dasm, che non esegue materialmente il programma, ha decompilato in gran scioltezza con i riferimrnti alle stringhe al loro posto. Quindi penso che questo valore ,200, sia un valore limite per cui dovremo rispettare tale allineamento. Dovendo lasciare 200B per gli header cominceremo CODE a 200,.data a 400 e .idata a 600.Avevo anche provato mettere .data nella sezione CODE, come avviene nei com, ma non sono riuscito neanche cambiando il flag che lo rende accessibile in scrittura. Allora l' unica cosa che resta da fare è ridurre l'header. Considerando che non si possono togliere i byte tra la fine del PE header e la OT non ci resta che togliere i byte che seguono l'header dos.Possiamo togliere anche la stringa This program must..., ma non togliamo la riga precedente che può ancora servire: guardatela un pò, non vi dice niente. Se avete mai provato a compilare qualcosa a 16 bit, gli opcode della 1a cosa che avete compilato saranno simili a questi, che corrispondono a queste istruzioni:
mov dx, 0010
push cs
pop ds
mov ah, 09
int 21<---stampa la stringa puntata da ds:dx
mov ax, 4C01
int 21<---lascia il controllo al dos
nop
nop
Per cui non conviene cancellarle. Se venisse per un qualche motivo eseguito da dos non causerebbe problemi, mostrerebbe solamente un messaggio adesso incomprensibile(almeno credo). Windows invece non eseguirebbe mai queste istruzioni perché, dopo aver letto la Dword precedente (a 3C), passerebbe con arroganza all'header PE puntato da tale dw. A proposito,se spostiamo l'header dobbiamo modificarla con il nuovo valore (50), andiamo all'offset 3c e scriviamo 50 00 00 00. Modifichiamo il campo header size mettendo 200. Così ci ritroviamo con spazio libero all' offset 200, dove andremo ad allineare CODE cancellando i byte inutili. Come abbiamo detto spostiamo le altre sezioni; non ci resta che modificare la object table seguendo lo schema in questo modo:
8 nome
4 virtual size
4 rva
4 ph size
4 ph offset<-------- questo è quello che andiamo
a modificare
C reserved
4 flags
43 4F 44 45 00 00 00 00 00 10 00 00
00 10 00 00
00 02 00 00 00 02 00
00 00 00 00 00 00 00 00 00
00 00 00 00 20 00 00 60 44 41 54 41 00 00 00 00
00 10 00 00 5C 20 00 00 00 02 00 00 00 04 00 00
00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0
2E 69 64 61 74 61 00 00 00 10 00 00 00 30 00 00
00 02 00 00 00 06 00
00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 00 C0
Ci troviamo in una situazione apparentemente impossibile: infatti considerando quanto detto prima per 3 sezioni meno di 600h byte sono impossibili, dovremmo togliere una sezione. In un primo momento mi erano venuti in mente i COM, ma provando a compilare non sono riuscito a far convivere dati e codice nella stessa sezione. Ma dove non arriva il compilatore arriva l' editor esadecimale. Infatti codice e dati stanno comodi in 200h byte, ma così facendo mandiamo a puttane tutti i puntatori che dovremo ripristinare a mano. Ma andiamo con ordine, incominciamo a togliere l'oggetto, ci portiamo nella OT e cancelliamo da dove comincia .data 40 byte e per riallineare gli oggetti a 200h ne aggiungeremo altrettanti alla fine della OT. Poi scriviamo 2 nel campo numero di oggetti nell' header, come avevamo fatto prima. Eliminato l'oggetto nella OT dobbiamo eliminarlo fisicamente, spostandolo nella sezione CODE, io ho cancellato 60h byte in modo da far cominciare il primo buffer all'offset 2A0h, che corrisponde ad un indirizzo virtuale :004010A0, contro il :00402000 che aveva prima. Teoricamente avrei potuto farlo cominciare prima, ma tanto come sapete i byte alla fine dell'oggetto sarebbero stati sprecati. Successivamente ho cambiato manualmente gli offset dei puntatori alle stringhe, sottraendo dai valori F60h che è poi 402000 - 4010A0, alla fine è rimasto così.
E8 7D 00 00 00 50 0F 31 8B C8 8B F0
0F 31 03 F0
66 49 75 F6 8B E8 8B DE 0F 31 03 E8 03 C5 8B F0
6A 06 59 F7 E6 E2 FC 40 03 C0 F7 F6 66 4B 75 EA
E8 4D 00 00 00 5B 2B C3 BB A0 10 40 00 50 68
FC
10 40 00 53 E8 45 00 00 00 6A
10 68 62 11 40
00
53 57 E8 31 00 00 00 BB D2 10 40 00 56 68 85 11
40 00 53 E8 26 00 00 00 6A 20
68 75 11 40 00 53
57 E8 12 00 00 00 57 E8 00 00 00 00 FF 25 54 30
40 00 FF 25 58 30 40 00 FF 25 60 30 40 00 FF 25
64 30 40 00 00 00 00 00 00 00 00 00 00 00 00 00
Abbiamo liberato spazio a sufficienza per allineare .idata a 400h. Non l'ho unito in CODE perchè non sono riuscito e forse non si può neanche. Spostato idata dobbiamo comunicarlo alla OT e già che siamo qui cambiamo un flag di code, che deve essere scrivibile, al contrario di com'è appena compilato, e per far ciò aggiungiamo 80000000h alla dword flags:
43 4F 44 45 00 00 00 00 00 10 00 00
00 10 00 00
00 02 00 00 00 02 00 00 00 00 00 00 00 00 00 00
00 00 00 00 20 00
00 E0 2E 69 64 61 74 61 00 00<--flags
00 10 00 00 00 30 00 00 00 02 00 00 00 04 00 00<--Physical offset
00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0
A quanto pare è proprio finito e l' exe misura 1212 byte, forse un po' troppi, ma di meno non saprei come fare.
Winebag
| Note finali |
Siamo giunti ai ringraziamenti:
un tnx a B0nu$ e Cod, per avermi insegnato che esiste anche una cosa chiamata
assembler con i loro tute che potete trovare anche su questo sito.Ringrazio
anche chi ha letto quello che sta leggendo e non l'ha ancora cestinato. Un ringraziamento
finale va al padrone della baracca, che con i suoi corsi ci ha tenuto svegli
per delle notti. Mi spiace solo di essere arrivato tardi ed essermi trovato
di fronte uno strainer che mi ha solo fatto incazzare, però potevi anche aspettarmi
Que.
Ciauz a tutti!!!!!
| Disclaimer |
Vi ricordo che non sono responsabile di niente di quello che c'è scritto qui, perché quando l'ho scritto non ero in grado di intendere e di volere come non sono tutt'ora. Per cui non provate a chiedere un rimborso per avervi fatto perdere il vostro preziosissimo tempo. Non sono da ritenermi responsabile per i vostri reati penali e neanche se la vostra fidanzata vi fa le corna o vi ha appena lasciato. Ricordo anche che il software va comprato, cioè anche le altre cose vanno comprate, non dimenticatelo mai.
Noi reversiamo al solo scopo informativo e di miglioramento del linguaggio Assembly.