Авторские статьи Опитимальный кодинг на Fasm

Discussion in 'Статьи' started by slesh, 14 Aug 2008.

  1. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,702
    Likes Received:
    1,224
    Reputations:
    455
    Оптимальный кодинг на 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 slesh, 14 Aug 2008
    Last edited: 14 Aug 2008
    5 people like this.
  2. dmnt

    dmnt Elder - Старейшина

    Joined:
    6 Jun 2007
    Messages:
    89
    Likes Received:
    36
    Reputations:
    15
    что-то у меня фасм от такого стаба начал делать тестовый файл в 1,5Кб весом.
    а со стандартным стабом делает его в 1024байта.
     
  3. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,702
    Likes Received:
    1,224
    Reputations:
    455
    Это глюки новой версии компилятора. сам заметил её. Под Win при версии 1.65.14 всё нормально работает.

    P.S. фактически привинчивание нового стаба не приведет к уменьшению программы, это просто уберет левые данные, такие как This program cannot be run in DOS mode.
     
    #3 slesh, 14 Aug 2008
    Last edited: 15 Aug 2008
  4. eLWAux

    eLWAux Elder - Старейшина

    Joined:
    15 Jun 2008
    Messages:
    860
    Likes Received:
    616
    Reputations:
    211
    стаб, который занимает на 4байта меньше стаба слеша :D

    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'
     
  5. gibson

    gibson Elder - Старейшина

    Joined:
    24 Feb 2006
    Messages:
    391
    Likes Received:
    247
    Reputations:
    88
    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 или хотябы использовать его более мение рационально.
     
  6. FrMn

    FrMn Elder - Старейшина

    Joined:
    8 Aug 2008
    Messages:
    51
    Likes Received:
    16
    Reputations:
    7
    slesh, вы б хоть указале, где по размеру, а где по скорости оптимизация. насчет апи - так можно вобще хеши зашить, исчо меньше места занимать будет.
    вобщем как вам, так и остальным рекомендую почитать маны от Intel'a касающиеся оптимизации, а также посмотреть формат РЕ более детально и провереть все, что вы здесь напесале, а то только сбиваете с толку юнных одептов.
     
    1 person likes this.
  7. slesh

    slesh Elder - Старейшина

    Joined:
    5 Mar 2007
    Messages:
    2,702
    Likes Received:
    1,224
    Reputations:
    455
    Там где оптимизация идет по размеру, зачастую она идет и по скорости.
    Оптимизация по опкодам - это я только привел примеры таких вариантов, а не все их. Хотябы для того, чтобы люди хоть знали что есть разница.
    А на счет PE - тут уже вы переборщили. Как ни крути его, но PE он и остается PE.
    В статье я только раскрыл те моменты, которые люди могут сами(без особых проблем) реализовать. Если дело на то пошло, то можно и самому собираться MZ и PE заголовок. И оставить только одну таблицу для единственной секции, сразу за которой пойдет код этой секции. Но собирать всё вручную - это геморно, и уже выходит за рамки данной статьи.