Оптимальный кодинг на Fasm СТАТЬЯ ПО ОПТИМАЛЬНОМУ КОДИНГУ В FASMПри использовании компилятора FASM можно достичь малых размеров программы, а при использовании дополнительных вещиц еще больше уменьшить размер программы. 1. Переменные в функциях: При использовании переменных в функциях желательно не допускать следующих описаний: Code: 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е переменные располагались в стеке. Code: 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. Структура секций: Чаще всего структура программы выглядит так: Code: 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 секции, то это будет наблюдаться в трех местах программы. Исправляется это следующим образом: Code: 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 После компиляции данный код будет выглядеть примерно так(но не всегда!) Code: 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 для обхода блока данных. На реале получается еще более страшный код. Именно по этой причине лучше всего такие текстовые строки описывать как переменные. т.е. Code: 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 и тогда код будет строиться следующим образом: Code: 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 функции Также удобно использовать собственное построение таблицы импорта т.к. оно занимает чуть меньше места а именно: Code: 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 сервер. В случаях, когда код чуть отличается, то можно делать специальные флаги которые будут указывать какой вариант использовать. т.е. логика будет такая: Code: cmp param_2,0 je _next .-.-.predoperation.-.-.- _next: .-.-.operation.-.-.- ret Также не малую важную роль играет кеширование некоторых значений. К примеру для сетевых приложений желательно кешировать IP адрес сервера, а не резолвить его каждый раз при подключении. Тоже самое можно делать и с некоторыми API функциями. Которые используются в разное время, но возвращают одно и тоже. Примером может быть: Code: 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) Code: 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
что-то у меня фасм от такого стаба начал делать тестовый файл в 1,5Кб весом. а со стандартным стабом делает его в 1024байта.
Это глюки новой версии компилятора. сам заметил её. Под Win при версии 1.65.14 всё нормально работает. P.S. фактически привинчивание нового стаба не приведет к уменьшению программы, это просто уберет левые данные, такие как This program cannot be run in DOS mode.
стаб, который занимает на 4байта меньше стаба слеша Code: 4D 5A 3C 00 01 00 00 00 02 00 00 01 FF FF 02 00 MZ<.........яя.. 00 10 00 00 00 00 00 00 1C 00 00 00 00 00 00 00 ................ 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 44 4F ..є..ґ.Н!ё.LН!DO 53 20 69 73 20 64 65 61 64 0D 0A 24 S is dead..$ в fasm'е: format PE GUI on 'stub'
Stub 0Ch (c) wasm.ru Code: 00000000 4D 5A 00 00 01 00 00 00 01 00 00 00 50 45 00 00 MZ..........PE.. 00000010 4C 01 07 00 08 00 00 00 B0 21 CD 29 B4 4C CD 21 L........!.).L.! 00000020 E0 00 0E 01 0B 01 04 14 20 00 00 00 00 2E 00 00 ........ ....... 00000030 18 00 00 00 00 E0 00 00 00 10 00 00 0C 00 00 00 ................ вообще лучше всего использовать пакеры, которые умеют уменьшать FileAlign или хотябы использовать его более мение рационально.
slesh, вы б хоть указале, где по размеру, а где по скорости оптимизация. насчет апи - так можно вобще хеши зашить, исчо меньше места занимать будет. вобщем как вам, так и остальным рекомендую почитать маны от Intel'a касающиеся оптимизации, а также посмотреть формат РЕ более детально и провереть все, что вы здесь напесале, а то только сбиваете с толку юнных одептов.
Там где оптимизация идет по размеру, зачастую она идет и по скорости. Оптимизация по опкодам - это я только привел примеры таких вариантов, а не все их. Хотябы для того, чтобы люди хоть знали что есть разница. А на счет PE - тут уже вы переборщили. Как ни крути его, но PE он и остается PE. В статье я только раскрыл те моменты, которые люди могут сами(без особых проблем) реализовать. Если дело на то пошло, то можно и самому собираться MZ и PE заголовок. И оставить только одну таблицу для единственной секции, сразу за которой пойдет код этой секции. Но собирать всё вручную - это геморно, и уже выходит за рамки данной статьи.