slesh
14.08.2008, 12:26
СТАТЬЯ ПО ОПТИМАЛЬНОМУ КОДИНГУ В FASM
При использовании компилятора FASM можно достичь малых размеров программы, а при использовании дополнительных вещиц еще больше уменьшить размер программы.
1. Переменные в функциях:
При использовании переменных в функциях желательно не допускать следующих описаний:
proc myprog
xor eax,eax
-.-.-.-.-.-.-.
ret
data_1 dw 0
data_2 dd 0
data_3 db 10 dup (0)
endp
Данный код будет не оптимален потому что переменные data_1,data_2,data_3 будут располагаться в коде программы в виде участка, что ведет к увеличению размера. Также данный код недопустим при вызове данных функций из потоков, т.к. может быть неконтрольное изменение данных переменных. Для решения данной проблемы необходимо чтобы вcе переменные располагались в стеке.
proc myprog
locals
data_1 dw 0
data_2 dd 0
data_3 db 10 dup (0)
endl
xor eax,eax
-.-.-.-.-.-.-.
ret
endp
Небольшая поправка: описывать переменные необходимо следующим образом:
data_1 dw ?
data_2 dd ?
data_3 db 10 dup (?)
т.е. мы даем знать компилятору, что нас не волнует первоначальное значение данных переменных и => он не будет задавать им значение.
2. Структура секций:
Чаще всего структура программы выглядит так:
format PE GUI
entry _start
section '.code' code readable executable
_start:
section '.data' data readable writeable
data_1 dd 0
.-.-.-.-.
section '.idata' import data readable writeable
library ***
include ***
Данная структура наглядно показывает где и что находится в программе, но потребляет дополнительное место в exe файле. А именно:
Каждая секция физически имеет выравнение на 512 байт. => потери будут возникать как раз при выравнении, а т.к. у нас 3 секции, то это будет наблюдаться в трех местах программы. Исправляется это следующим образом:
format PE GUI
entry _start
section '.code' code import writeable readable executable
library *** ;импорт
include ***;импорт
_start: ; начало исполняемого кода
xor eax,eax
.-.-.-.-.-.-
ret / invoke ExitProcesse,0
; начало данных
data_1 dd 0
.-.-.-.-.
Как видно все 3 секции были помещены в одну. Сначала идут данные таблицы импорта, потом код программы а потом данные и переменные.
Недостатки данного способа в том, что секция кода имеет права на запись + нельзя совместить вместе секцию импорта, экспорта, ресурсов. Если присутствует хотя бы 2 из них, то необходимо так склеивать наименую из них.
3. Глобальные переменные:
Как было видно из вышеописанной структуры, секция данных находится в конце файла.
Допустим у нас есть множество переменных. Если эти переменные были описаны в самом конце программы как
data_1 dw ?
data_2 dd ?
data_3 db 10 dup (?)
то физически они не попадают в исполняемый файл и располагаются только в оперативной памяти.
по этому структура данных должна быть следующей
; инициализированные данные
xxx1 db 0
xxx2 dd 2
; неинициализированные данные
xxx3 db ?
xxx4 dd ?
4. Текст используемый в коде:
Допустим у нас есть строка:
invoke MessageBox,0,"Error","TITLE",0
После компиляции данный код будет выглядеть примерно так(но не всегда!)
jmp _next
data_1 db "Error",0
data_2 db "TITLE",0
_next:
push 0
push data_2
push data_1
push 0
call MessageBox
Как мы видим появилась лишняя инструкция jmp _next для обхода блока данных.
На реале получается еще более страшный код. Именно по этой причине лучше всего такие текстовые строки описывать
как переменные.
т.е.
invoke MessageBox,0,data_1,data_2,0
.-.-.-.-.-
;data
data_1 db "Error",0
data_2 db "TITLE",0
Теперь не будет в коде таких левых команд
5. Оптимизация команд:
Не секрет, что существуют команды которые при определенных условиях выполняют одну и туже операцию, но занимают меньше места
Среди таких команд можно перечислить часто используемые:
1. add/sub reg,1 лучше заменить на inc/dec reg
2. умножение/деление на степень двойки. - shl/shr reg,1 ; где 1,2,3 - степень
3. При записи в регистр какого либо числа командой mov, лучше всего использовать по возможности регистр eax т.к.
mov eax,xxxxxxxxh имеет более короткий опкод, нежили mov ecx,xxxxxxxxh
4. mov reg,0 заменяется на xor reg,reg
5. cmp reg,0 заменяться на test reg,reg
6. Для циклов использовать лучше использовать регистр ecx с командами loop и jzecx
7. Адресация данных. Допустим у нас есть массив и значения некоторых элементов нам нужно поместить в стек
для этого используем адресацию через регистр.
lea eax,[mas]
push [eax+4]
push [eax+8]
push [eax+12]
дело втом что адресация через регистры имеет более короткий опкод.
8. При использовании процедур желательно в начале и в конце использовать pushad/popad а не по отдельности сохранять значения изменяемых регистров.
6. Таблица импорта:
При написании программ с большим числом API функций лучше всего использовать динамический импорт.
Это связанно с тем, что можно более оптимально распределить данные.
В итоге таблица импорта должна будет состоять только из 2-х функций - GetProcAddress и LoadLibraryA
и тогда код будет строиться следующим образом:
invoke LoadLibraryA,lib_1
mov ebx,eax
invoke GetProcAddress,ebx,proc_1
mov [proc_1_adr],eax
invoke GetProcAddress,ebx,proc_2
mov [proc_2_adr],eax
.-.-.-.-
invoke proc_1_adr,param1,param2
.-.-.-.-.
lib_1 db 'LIBNAME.DLL',0
proc_1 db 'PROC_NAME_1',0
proc_2 db 'PROC_NAME_2',0
.-.-.-.-.-.-.
proc_1_adr dd ?
proc_2_adr dd ?
Данные изменения будут заметны только при большом числе функций.
Плюсы данного метода - при первичном осмотре файла средствами типа PE_edit будут незаметны используемые функции.
Также для большей скрытности имена функций и библиотек могут быть зашифрованные.
Дополнение: Также можно использовать вместо GetProccAddress другие алгоритмы нахождения адресов функций через таблицу экспорта библиотеки.
А при использовании метода импорта по хешам, это значительно сократит размер программы в ненадобности хранения полного имени API функции
Также удобно использовать собственное построение таблицы импорта т.к. оно занимает чуть меньше места
а именно:
dd 0,0,0,IT_lib-IMAGE_BASE,IT-IMAGE_BASE
dd 0,0,0,0,0
IT:
mLoadLibrary dd _mLoadLibrary-IMAGE_BASE
dd 0
IT_lib db 'KERNEL32.DLL',0
_mLoadLibrary dw 0 ; HINT
db 'LoadLibraryA',0,0 ; NAME
7. Аналоги стандартный API:
Некоторые Api функции созданы только для удобства получения некоторых данных. При этом на код самой функции может быть потрачено меньше байт, чем на поиск её адреса и вызова.
Примером таких функции являются:
1. invoke GetModuleHandleA,0
Её можно заменить на:
mov eax,[fs:18h]
mov eax,[eax+30h]
mov eax,[eax+3h]
2. invoke GetProcessHeap
Можно заменить на:
mov eax,[fs:18h]
mov eax,[eax+30h]
mov eax,[eax+18h]
3. invoke GetLastError
заменяем на
mov eax,[fs:18h]
mov eax,[eax+34h]
4. invoke lstrlen,my_str
в данном случаи код на ручной подсчет длинны строки будет меньше чем затраты на импорт функции
Таким образом можно оптимизировать использование и некоторых других функций.
8. Разбиение кода на функции и "кеширование":
Порой часто встречается, что необходимо выполнять похожие операции, для этого лучше разбивать код на функции которые будут делать данную операцию
Примером таких функций может быть функция отсыла запроса на http сервер.
В случаях, когда код чуть отличается, то можно делать специальные флаги которые будут указывать какой вариант использовать.
т.е. логика будет такая:
cmp param_2,0
je _next
.-.-.predoperation.-.-.-
_next:
.-.-.operation.-.-.-
ret
Также не малую важную роль играет кеширование некоторых значений.
К примеру для сетевых приложений желательно кешировать IP адрес сервера, а не резолвить его каждый раз при подключении.
Тоже самое можно делать и с некоторыми API функциями. Которые используются в разное время, но возвращают одно и тоже.
Примером может быть:
invoke GetModuleHandleA,0
mov [mhandle],eax
.-.-.-.-
mov eax,[mhandle] ; вместо вызова функции GetModuleHandleA
.-.-.-.-.
mov eax,[mhandle] ; вместо вызова функции GetModuleHandleA
.-.-.-.-.
mhandle dd ?
В это таким образом можно не только у уменьшить размер программы, но и увеличить быстродействие.
9. Использование альтернативного MZ заголовка:
Стандартный MZ заголовок у FASM имеет размер 128 байт. ПРи использовании альтернативного заголовка этот размер может быть уменьшен до 64 байт, без прибегания к разным хитростям.
делается это с помощью команды: format PE GUI on 'stub.inc'
примером такого стаба может быть следующий код (переведен в HEX)
4D 5A 00 00 01 00 00 00
02 00 00 00 FF FF 00 00
40 00 00 00 00 00 00 00
40 00 00 00 00 00 00 00
B4 4C CD 21 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Перед его использование его нужно перевести из HEX в BIN
ЗАКЛЮЧЕНИЕ
Конечно это не все способы которые помогут уменьшить размер программы, но всё же в совокупности они могут дать уменьшение размера программы на несколько килобайт, что немало важно бывает в некоторые специфических областях программирования.
(С) SLESH 2008
При использовании компилятора FASM можно достичь малых размеров программы, а при использовании дополнительных вещиц еще больше уменьшить размер программы.
1. Переменные в функциях:
При использовании переменных в функциях желательно не допускать следующих описаний:
proc myprog
xor eax,eax
-.-.-.-.-.-.-.
ret
data_1 dw 0
data_2 dd 0
data_3 db 10 dup (0)
endp
Данный код будет не оптимален потому что переменные data_1,data_2,data_3 будут располагаться в коде программы в виде участка, что ведет к увеличению размера. Также данный код недопустим при вызове данных функций из потоков, т.к. может быть неконтрольное изменение данных переменных. Для решения данной проблемы необходимо чтобы вcе переменные располагались в стеке.
proc myprog
locals
data_1 dw 0
data_2 dd 0
data_3 db 10 dup (0)
endl
xor eax,eax
-.-.-.-.-.-.-.
ret
endp
Небольшая поправка: описывать переменные необходимо следующим образом:
data_1 dw ?
data_2 dd ?
data_3 db 10 dup (?)
т.е. мы даем знать компилятору, что нас не волнует первоначальное значение данных переменных и => он не будет задавать им значение.
2. Структура секций:
Чаще всего структура программы выглядит так:
format PE GUI
entry _start
section '.code' code readable executable
_start:
section '.data' data readable writeable
data_1 dd 0
.-.-.-.-.
section '.idata' import data readable writeable
library ***
include ***
Данная структура наглядно показывает где и что находится в программе, но потребляет дополнительное место в exe файле. А именно:
Каждая секция физически имеет выравнение на 512 байт. => потери будут возникать как раз при выравнении, а т.к. у нас 3 секции, то это будет наблюдаться в трех местах программы. Исправляется это следующим образом:
format PE GUI
entry _start
section '.code' code import writeable readable executable
library *** ;импорт
include ***;импорт
_start: ; начало исполняемого кода
xor eax,eax
.-.-.-.-.-.-
ret / invoke ExitProcesse,0
; начало данных
data_1 dd 0
.-.-.-.-.
Как видно все 3 секции были помещены в одну. Сначала идут данные таблицы импорта, потом код программы а потом данные и переменные.
Недостатки данного способа в том, что секция кода имеет права на запись + нельзя совместить вместе секцию импорта, экспорта, ресурсов. Если присутствует хотя бы 2 из них, то необходимо так склеивать наименую из них.
3. Глобальные переменные:
Как было видно из вышеописанной структуры, секция данных находится в конце файла.
Допустим у нас есть множество переменных. Если эти переменные были описаны в самом конце программы как
data_1 dw ?
data_2 dd ?
data_3 db 10 dup (?)
то физически они не попадают в исполняемый файл и располагаются только в оперативной памяти.
по этому структура данных должна быть следующей
; инициализированные данные
xxx1 db 0
xxx2 dd 2
; неинициализированные данные
xxx3 db ?
xxx4 dd ?
4. Текст используемый в коде:
Допустим у нас есть строка:
invoke MessageBox,0,"Error","TITLE",0
После компиляции данный код будет выглядеть примерно так(но не всегда!)
jmp _next
data_1 db "Error",0
data_2 db "TITLE",0
_next:
push 0
push data_2
push data_1
push 0
call MessageBox
Как мы видим появилась лишняя инструкция jmp _next для обхода блока данных.
На реале получается еще более страшный код. Именно по этой причине лучше всего такие текстовые строки описывать
как переменные.
т.е.
invoke MessageBox,0,data_1,data_2,0
.-.-.-.-.-
;data
data_1 db "Error",0
data_2 db "TITLE",0
Теперь не будет в коде таких левых команд
5. Оптимизация команд:
Не секрет, что существуют команды которые при определенных условиях выполняют одну и туже операцию, но занимают меньше места
Среди таких команд можно перечислить часто используемые:
1. add/sub reg,1 лучше заменить на inc/dec reg
2. умножение/деление на степень двойки. - shl/shr reg,1 ; где 1,2,3 - степень
3. При записи в регистр какого либо числа командой mov, лучше всего использовать по возможности регистр eax т.к.
mov eax,xxxxxxxxh имеет более короткий опкод, нежили mov ecx,xxxxxxxxh
4. mov reg,0 заменяется на xor reg,reg
5. cmp reg,0 заменяться на test reg,reg
6. Для циклов использовать лучше использовать регистр ecx с командами loop и jzecx
7. Адресация данных. Допустим у нас есть массив и значения некоторых элементов нам нужно поместить в стек
для этого используем адресацию через регистр.
lea eax,[mas]
push [eax+4]
push [eax+8]
push [eax+12]
дело втом что адресация через регистры имеет более короткий опкод.
8. При использовании процедур желательно в начале и в конце использовать pushad/popad а не по отдельности сохранять значения изменяемых регистров.
6. Таблица импорта:
При написании программ с большим числом API функций лучше всего использовать динамический импорт.
Это связанно с тем, что можно более оптимально распределить данные.
В итоге таблица импорта должна будет состоять только из 2-х функций - GetProcAddress и LoadLibraryA
и тогда код будет строиться следующим образом:
invoke LoadLibraryA,lib_1
mov ebx,eax
invoke GetProcAddress,ebx,proc_1
mov [proc_1_adr],eax
invoke GetProcAddress,ebx,proc_2
mov [proc_2_adr],eax
.-.-.-.-
invoke proc_1_adr,param1,param2
.-.-.-.-.
lib_1 db 'LIBNAME.DLL',0
proc_1 db 'PROC_NAME_1',0
proc_2 db 'PROC_NAME_2',0
.-.-.-.-.-.-.
proc_1_adr dd ?
proc_2_adr dd ?
Данные изменения будут заметны только при большом числе функций.
Плюсы данного метода - при первичном осмотре файла средствами типа PE_edit будут незаметны используемые функции.
Также для большей скрытности имена функций и библиотек могут быть зашифрованные.
Дополнение: Также можно использовать вместо GetProccAddress другие алгоритмы нахождения адресов функций через таблицу экспорта библиотеки.
А при использовании метода импорта по хешам, это значительно сократит размер программы в ненадобности хранения полного имени API функции
Также удобно использовать собственное построение таблицы импорта т.к. оно занимает чуть меньше места
а именно:
dd 0,0,0,IT_lib-IMAGE_BASE,IT-IMAGE_BASE
dd 0,0,0,0,0
IT:
mLoadLibrary dd _mLoadLibrary-IMAGE_BASE
dd 0
IT_lib db 'KERNEL32.DLL',0
_mLoadLibrary dw 0 ; HINT
db 'LoadLibraryA',0,0 ; NAME
7. Аналоги стандартный API:
Некоторые Api функции созданы только для удобства получения некоторых данных. При этом на код самой функции может быть потрачено меньше байт, чем на поиск её адреса и вызова.
Примером таких функции являются:
1. invoke GetModuleHandleA,0
Её можно заменить на:
mov eax,[fs:18h]
mov eax,[eax+30h]
mov eax,[eax+3h]
2. invoke GetProcessHeap
Можно заменить на:
mov eax,[fs:18h]
mov eax,[eax+30h]
mov eax,[eax+18h]
3. invoke GetLastError
заменяем на
mov eax,[fs:18h]
mov eax,[eax+34h]
4. invoke lstrlen,my_str
в данном случаи код на ручной подсчет длинны строки будет меньше чем затраты на импорт функции
Таким образом можно оптимизировать использование и некоторых других функций.
8. Разбиение кода на функции и "кеширование":
Порой часто встречается, что необходимо выполнять похожие операции, для этого лучше разбивать код на функции которые будут делать данную операцию
Примером таких функций может быть функция отсыла запроса на http сервер.
В случаях, когда код чуть отличается, то можно делать специальные флаги которые будут указывать какой вариант использовать.
т.е. логика будет такая:
cmp param_2,0
je _next
.-.-.predoperation.-.-.-
_next:
.-.-.operation.-.-.-
ret
Также не малую важную роль играет кеширование некоторых значений.
К примеру для сетевых приложений желательно кешировать IP адрес сервера, а не резолвить его каждый раз при подключении.
Тоже самое можно делать и с некоторыми API функциями. Которые используются в разное время, но возвращают одно и тоже.
Примером может быть:
invoke GetModuleHandleA,0
mov [mhandle],eax
.-.-.-.-
mov eax,[mhandle] ; вместо вызова функции GetModuleHandleA
.-.-.-.-.
mov eax,[mhandle] ; вместо вызова функции GetModuleHandleA
.-.-.-.-.
mhandle dd ?
В это таким образом можно не только у уменьшить размер программы, но и увеличить быстродействие.
9. Использование альтернативного MZ заголовка:
Стандартный MZ заголовок у FASM имеет размер 128 байт. ПРи использовании альтернативного заголовка этот размер может быть уменьшен до 64 байт, без прибегания к разным хитростям.
делается это с помощью команды: format PE GUI on 'stub.inc'
примером такого стаба может быть следующий код (переведен в HEX)
4D 5A 00 00 01 00 00 00
02 00 00 00 FF FF 00 00
40 00 00 00 00 00 00 00
40 00 00 00 00 00 00 00
B4 4C CD 21 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
Перед его использование его нужно перевести из HEX в BIN
ЗАКЛЮЧЕНИЕ
Конечно это не все способы которые помогут уменьшить размер программы, но всё же в совокупности они могут дать уменьшение размера программы на несколько килобайт, что немало важно бывает в некоторые специфических областях программирования.
(С) SLESH 2008