Уменьшение екзешников, или Борьба с компилятором

Предыдущая тема Следующая тема Перейти вниз

Уменьшение екзешников, или Борьба с компилятором

Сообщение  Замабувараев в Чт Апр 23, 2015 4:22 pm

С некоторых пор Я заметил, что размер исполняемых файлов после компиляции исходников растёт от версии к версии компилятора. Чем старше версия, тем толще на выходе получается екзешник. На сегодняшний день в версии компилятора 1.02.0 пустой исходный файл даёт на выходе исполняемый файл размером 16 384 байт.

Это недопустимо

Всё дело в том, что компилятор вставляет в исполняемый файл статические библиотеки времени выполнения: C runtime library, FreeBASIC runtime library и некоторые другие. В них происходит инициализация кучи, критических секций, функций Command и прочего, что позволяет спокойно пользоваться встроенными функциями бейсика.

Часть первая: Борьба с 15 килобайтами библиотеки времени выполнения


Чтобы убрать всю библиотеку времени выполнения вместе с CRT, достаточно всего одной волшебной опции компилятора:

Код:
fbc *.bas -lib

Так мы создадим библиотеку, однако она непригодна для компиляции исполняемого файла. Нам требуется ассемблерный код. Исправляем ситуацию:

Код:
fbc *.bas -lib -r

Мы получили голый ассемблерный код. Но так как мы удалили всю библиотеку времени выполнения, то нам теперь придётся самим создавать точку входа, откуда будет начинаться исполнение программы. Эту стартовую функцию можно было назвать как угодно, но для похожести на другие языки программирования Я использовал название Main. Кстати, а почему здесь используется Alias Main? Потому что в ассемблерном коде все названия функций и глобальный переменных переводятся в верхний регистр.
К сожалению, это не такая же функция Main, как в других языках, например, в си или VB.Net, операционная система не передаёт ей аргументы командной строки.

Код:

Public Function Main Alias "Main"()As Integer
   Return 0
End Function

Но можно схитрить и написать точку входа EntryPoint, где она будет получать параметры командной строки, передавать их функции Main и ожидать код её завершения.

Код:


Declare Function CommandLineToArgv Alias "CommandLineToArgvW"(ByVal CommandLineString As WString Ptr, ByVal ArgsCount As Integer Ptr)As WString Ptr Ptr

Public Function Main(ByVal ArgsCount As Integer, ByVal Args As WString Ptr Ptr)As Integer
   Return 0
End Function

Public Function EntryPoint Alias "EntryPoint"()As Integer
   Dim ArgsCount As Integer = Any
   ' Получить командную строку и разбить её по пробелам
   Dim Args As WSTring Ptr Ptr = CommandLineToArgv(GetCommandLine(), @ArgsCount)
   ' Теперь функция Main как в других языках программирования
   EntryPoint = Main(ArgsCount, Args)
   ' Очистка памяти, выделенной для агрументов командной строки
   LocalFree(Args)
End Function

Трюк с EntryPoint годится также в качестве написания своей реализации небольшой библиотеки времени выполнения, заточенной под конкретные нужды. Также нам нужно подключить библиотеку shell32.dll, так как в ней находится функция CommandLineToArgv. Эта функция принимает в качестве параметра юникодную командную строку и разбивает её по пробелу на массив юникодных подстрок. Для очистки памяти от массива используется LocalFree. Функция CommandLineToArgv работает только с юникодовой строкой и присутствует в системе начиная с Windows 2000.

Командный файл компиляции теперь будет выглядеть так:

Код:

fbc.exe -r -lib "file.bas"
"%ProgramFiles%\FreeBASIC\bin\win32\as.exe" --32 --strip-local-absolute "file.bas" -o "file.o"
"%ProgramFiles%\FreeBASIC\bin\win32\ld.exe" -m i386pe -e _EntryPoint@0 -subsystem console -s --stack 1048576,1048576 -L "%programfiles%\freebasic\lib\win32" -L "./" "file.o" -o "file.exe" -( -lkernel32 -lshell32 -)


Часть вторая: Борьба с ассемблерным листингом


Сто́ит только подключить заголовочные файлы "windows.bi" в исходный код, как исполняемый файл сразу же раздувается на несколько килобайт. Взглянем на ассемблерный листинг:

Код:

   .intel_syntax noprefix

.section .text
.balign 16
_GetCurrentFiber:
push ebp
mov ebp, esp
sub esp, 4
mov dword ptr [ebp-4], 0
.Lt_0017:
push 16
call ___readfsdword
add esp, 4
mov dword ptr [ebp-4], eax
.Lt_0018:
mov eax, dword ptr [ebp-4]
mov esp, ebp
pop ebp
ret
.balign 16
_InterlockedCompareExchange64@20:
push ebp
mov ebp, esp
sub esp, 8
mov dword ptr [ebp-8], 0
mov dword ptr [ebp-4], 0
.Lt_00AA:
push dword ptr [ebp+24]
push dword ptr [ebp+20]
push dword ptr [ebp+16]
push dword ptr [ebp+12]
push dword ptr [ebp+8]
call __InterlockedCompareExchange64
add esp, 20
mov dword ptr [ebp-8], eax
mov dword ptr [ebp-4], edx
.Lt_00AB:
mov eax, dword ptr [ebp-8]
mov edx, dword ptr [ebp-4]
mov esp, ebp
pop ebp
ret 20

.section .fbctinf
.ascii "-l\0"
.ascii "kernel32\0"
.ascii "-l\0"
.ascii "gdi32\0"
.ascii "-l\0"
.ascii "user32\0"
.ascii "-l\0"
.ascii "version\0"
.ascii "-l\0"
.ascii "advapi32\0"
.ascii "-l\0"
.ascii "imm32\0"

Что происходит? Оказывается, заголовочные файлы в версии 1.02.0 содержат код! Этот код якобы нужен для нормального функционирования CRT. (Заголовочные файлы не создаются самими авторами компилятора, они лишь импортируются из MinGW специальной утилитой.) Но мы уже выкинули CRT из своей программы, поэтому этот код нам не потребуется. Смело пишем скрипт RemoveLines.vbs, удаляющий из ассемблерного листинга ненужный код:

Код:


Option Explicit

' Прочитать файл построчно. Если встретим строку балигн 16, то будем настороже
' Читаем следующую строку, если она попадает в список запрещённых слов
' то ставим флаг, чтобы строку в файл не писать
' далее не пишем строки в файл, пока не встретится ret что‐нибудь
Dim objFSO
Dim strFileName
Dim objArgs
Set objFSO = CreateObject("Scripting.FileSystemObject")
' Получить параметр программы
Set objArgs = WScript.Arguments
For Each strFileName In objArgs
   REM WScript.Echo strFileName
   ' Открыть файл на чтение, прочитать до конца, закрыть
   Dim strFileNameWithoutExt
   strFileNameWithoutExt = LCase(objFSO.GetBaseName(strFileName))
   Dim objTS
   Set objTS = objFSO.OpenTextFile(strFileName)
   Dim strLines
   strLines = objTS.ReadAll
   REM WScript.Echo strLines
   objTS.Close
   Set objTS = Nothing
   ' Открыть снова на запись
   Set objTS = objFSO.CreateTextFile(strFileName)
   ' Разбить строку на массив
   ' Пройтись по массиву
   Dim blnSkipLines
   blnSkipLines = False
   Dim astrLines
   astrLines = Split(strLines, vbCrLf)
   Dim i
   For i = 0 To UBound(astrLines)
      Select Case astrLines(i)
         Case ".balign 16"
            ' Начало, нужно быть готовым
            Select Case astrLines(i + 1)
               Case "_GetCurrentFiber:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_InterlockedCompareExchange64@20:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_IN6_IS_ADDR_UNSPECIFIED:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_IN6_IS_ADDR_LOOPBACK:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_IN6_IS_ADDR_MULTICAST:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_IN6_SET_ADDR_UNSPECIFIED:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_IN6_SET_ADDR_LOOPBACK:"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case "_fb_ctor__" & strFileNameWithoutExt & ":"
                  ' Ставим флаг, что строку нужно пропустить
                  blnSkipLines = True
               Case Else
                  ' Записываем
                  objTS.WriteLine(astrLines(i))
            End Select
         Case ".section .ctors"
            If astrLines(i + 1) = ".int _fb_ctor__" & strFileNameWithoutExt Then
               i = i + 2
            End If
         Case "ret", "ret 20"
            If blnSkipLines Then
               '  Снимаем флаг и пропускаем эту строку
               blnSkipLines = False
            Else
               objTS.WriteLine(astrLines(i))
            End If
         Case Else
            ' Пишем строку, только если разрешено
            If Not blnSkipLines Then
               objTS.WriteLine(astrLines(i))
            End If
      End Select
   Next
   objTS.Close
   Set objTS = Nothing
Next
Set objArgs = Nothing
Set objFSO = Nothing

Этот скрипт принимает в качестве параметра имя файла с ассемблерным листингом и удаляет из него автоматически вставляемые функции и секции инициализации. В нашем случае они никогда не понадобятся.
Использование:
Код:
cscript //Nologo RemoveLines.vbs "файл.asm"

Часть третья: Борьба с собственным исходным кодом


Объявление глобальных переменных. Глобальные переменные хранятся в секции .bss исполняемого файла. Это такая секция, в которой память под переменные только резервируются, а но сама память выделяется только на этапе исполнения. Особенно это относится к различного рода буферам (массивам) и строкам.
Код:
Common Shared Variable As Integer
Common Shared Variable As WString * 4097 ' буфер для строки 4096 символов + 1 на нулевой

Прекращение инициализации переменных по умолчанию. При объявлении новой переменной ей сразу же присваивается значение по умолчанию, если мы не сделали это сами. Например, целочисленным переменным присваивается значение 0. Если нам не требуется инициализация переменных, для этого переменной нужно присвоить специальное значение Any. Особенно это актуально для структур и строк фиксированного размера, так как в структурах инициализуются все поля, а вся память под строку заполняется нулями, что создаёт много дополнительного беспорядочного кода.
Код:
Dim Variable As WString * BufferSize = Any

Использование строковых констант везде, где это возможно. Все строковые константы хранятся в секции .data. При использовании строковых литератов прямо в коде высока вероятность повторения одних и тех же данных несколько раз, что увеличит размер секции .data.
Код:
Const MyConst = "Это константа"
avatar
Замабувараев

Сообщения : 99
Дата регистрации : 2008-08-20
Возраст : 33
Откуда : Красноярск

Посмотреть профиль http://www.freebasic.su

Вернуться к началу Перейти вниз

Re: Уменьшение екзешников, или Борьба с компилятором

Сообщение  Замабувараев в Пт Апр 24, 2015 2:06 pm

Забыл добавить, что такое BSS, слышащим это впервые может быть не ясно. BSS — Block Started by Symbol. В образе бинарника в BSS записывается только количество байт, которое должно быть выделено при загрузки программы в память. Например, 10 переменных типа Integer — это 10 * SizeOf(Integer) = 40 байт на 32‐битной системе. Когда программа загружается в память, то уже в запущенной будет зарезервировано 40 нулей.
Таким образом всё, что определено как Common Shared занимает память только в процессе исполнения программы.
avatar
Замабувараев

Сообщения : 99
Дата регистрации : 2008-08-20
Возраст : 33
Откуда : Красноярск

Посмотреть профиль http://www.freebasic.su

Вернуться к началу Перейти вниз

Re: Уменьшение екзешников, или Борьба с компилятором

Сообщение  electrik в Пн Апр 27, 2015 4:22 pm

Да, к сожалению,иногда, в fb не всё хорошо за оптимизировано. есть пример с select case
Код:

sub test()
dim a as integer
select case a
case 2
exit sub
case 5
a+=2
case 9.
exit sub
case 12
a=1
case 17
exit sub
end select
end sub
test()

а теперь asm
Код:

_TEST@0:
push ebp
mov ebp, esp
sub esp, 4
.Lt_0004:
mov dword ptr [ebp-4], 0
cmp dword ptr [ebp-4], 2
jne .Lt_0007
.Lt_0008:
jmp .Lt_0005
jmp .Lt_0006
.Lt_0007:
cmp dword ptr [ebp-4], 5
jne .Lt_0009
.Lt_000A:
add dword ptr [ebp-4], 2
jmp .Lt_0006
.Lt_0009:
cmp dword ptr [ebp-4], 9
jne .Lt_000B
.Lt_000C:
jmp .Lt_0005
jmp .Lt_0006
.Lt_000B:
cmp dword ptr [ebp-4], 12
jne .Lt_000D
.Lt_000E:
mov dword ptr [ebp-4], 1
jmp .Lt_0006
.Lt_000D:
cmp dword ptr [ebp-4], 17
jne .Lt_000F
.Lt_0010:
.Lt_000F:
.Lt_0006:
.Lt_0005:
mov esp, ebp
pop ebp
ret
Посмотрите внимательней, в некоторых местах, вы заметите по 2 подряд jmp.\
вот программка, которая удаляет такие вещи из asm файла.
без оптимизаций на чистом FreeBasic.

Код:

#include "file.bi"
dim s as string
dim r as string
dim jumps as integer
open command for input as #1
if fileexists(command) then
print "removing jumps"
open "tmp" for output as #2

while not eof(1)
line input #1,s
if left(s,4) = "jmp " then
r=s
line input #1,s
if left(s,4) = "jmp " then
jumps+=1
print #2,r
continue while
end if
print #2,r
end if
print #2,s
wend
end if
close
print jumps,"jumps removed"
kill command
name "tmp",command

в моём проекте весом в 35 килобайт, эта штука убрала 24 джампа. бывает так, что часто в блоке select case пишут exit sub или exit select или exit function.

electrik

Сообщения : 394
Дата регистрации : 2008-09-02
Возраст : 36
Откуда : галактика Млечный путь, система Солнечная, планета Земля, страна россия, город Санкт Петербург

Посмотреть профиль

Вернуться к началу Перейти вниз

Re: Уменьшение екзешников, или Борьба с компилятором

Сообщение  Спонсируемый контент


Спонсируемый контент


Вернуться к началу Перейти вниз

Предыдущая тема Следующая тема Вернуться к началу


 
Права доступа к этому форуму:
Вы не можете отвечать на сообщения