Уменьшение екзешников, или Борьба с компилятором
Участников: 2
FreeBasic :: Программирование :: Общее
Страница 1 из 1
Уменьшение екзешников, или Борьба с компилятором
С некоторых пор Я заметил, что размер исполняемых файлов после компиляции исходников растёт от версии к версии компилятора. Чем старше версия, тем толще на выходе получается екзешник. На сегодняшний день в версии компилятора 1.02.0 пустой исходный файл даёт на выходе исполняемый файл размером 16 384 байт.
Это недопустимо
Всё дело в том, что компилятор вставляет в исполняемый файл статические библиотеки времени выполнения: C runtime library, FreeBASIC runtime library и некоторые другие. В них происходит инициализация кучи, критических секций, функций Command и прочего, что позволяет спокойно пользоваться встроенными функциями бейсика.
Чтобы убрать всю библиотеку времени выполнения вместе с CRT, достаточно всего одной волшебной опции компилятора:
Так мы создадим библиотеку, однако она непригодна для компиляции исполняемого файла. Нам требуется ассемблерный код. Исправляем ситуацию:
Мы получили голый ассемблерный код. Но так как мы удалили всю библиотеку времени выполнения, то нам теперь придётся самим создавать точку входа, откуда будет начинаться исполнение программы. Эту стартовую функцию можно было назвать как угодно, но для похожести на другие языки программирования Я использовал название Main. Кстати, а почему здесь используется Alias Main? Потому что в ассемблерном коде все названия функций и глобальный переменных переводятся в верхний регистр.
К сожалению, это не такая же функция Main, как в других языках, например, в си или VB.Net, операционная система не передаёт ей аргументы командной строки.
Но можно схитрить и написать точку входа EntryPoint, где она будет получать параметры командной строки, передавать их функции Main и ожидать код её завершения.
Трюк с EntryPoint годится также в качестве написания своей реализации небольшой библиотеки времени выполнения, заточенной под конкретные нужды. Также нам нужно подключить библиотеку shell32.dll, так как в ней находится функция CommandLineToArgv. Эта функция принимает в качестве параметра юникодную командную строку и разбивает её по пробелу на массив юникодных подстрок. Для очистки памяти от массива используется LocalFree. Функция CommandLineToArgv работает только с юникодовой строкой и присутствует в системе начиная с Windows 2000.
Командный файл компиляции теперь будет выглядеть так:
Сто́ит только подключить заголовочные файлы "windows.bi" в исходный код, как исполняемый файл сразу же раздувается на несколько килобайт. Взглянем на ассемблерный листинг:
Что происходит? Оказывается, заголовочные файлы в версии 1.02.0 содержат код! Этот код якобы нужен для нормального функционирования CRT. (Заголовочные файлы не создаются самими авторами компилятора, они лишь импортируются из MinGW специальной утилитой.) Но мы уже выкинули CRT из своей программы, поэтому этот код нам не потребуется. Смело пишем скрипт RemoveLines.vbs, удаляющий из ассемблерного листинга ненужный код:
Этот скрипт принимает в качестве параметра имя файла с ассемблерным листингом и удаляет из него автоматически вставляемые функции и секции инициализации. В нашем случае они никогда не понадобятся.
Использование:
Объявление глобальных переменных. Глобальные переменные хранятся в секции .bss исполняемого файла. Это такая секция, в которой память под переменные только резервируются, а но сама память выделяется только на этапе исполнения. Особенно это относится к различного рода буферам (массивам) и строкам.
Прекращение инициализации переменных по умолчанию. При объявлении новой переменной ей сразу же присваивается значение по умолчанию, если мы не сделали это сами. Например, целочисленным переменным присваивается значение 0. Если нам не требуется инициализация переменных, для этого переменной нужно присвоить специальное значение Any. Особенно это актуально для структур и строк фиксированного размера, так как в структурах инициализуются все поля, а вся память под строку заполняется нулями, что создаёт много дополнительного беспорядочного кода.
Использование строковых констант везде, где это возможно. Все строковые константы хранятся в секции .data. При использовании строковых литератов прямо в коде высока вероятность повторения одних и тех же данных несколько раз, что увеличит размер секции .data.
Это недопустимо
Всё дело в том, что компилятор вставляет в исполняемый файл статические библиотеки времени выполнения: 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 = "Это константа"
Re: Уменьшение екзешников, или Борьба с компилятором
Забыл добавить, что такое BSS, слышащим это впервые может быть не ясно. BSS — Block Started by Symbol. В образе бинарника в BSS записывается только количество байт, которое должно быть выделено при загрузки программы в память. Например, 10 переменных типа Integer — это 10 * SizeOf(Integer) = 40 байт на 32‐битной системе. Когда программа загружается в память, то уже в запущенной будет зарезервировано 40 нулей.
Таким образом всё, что определено как Common Shared занимает память только в процессе исполнения программы.
Таким образом всё, что определено как Common Shared занимает память только в процессе исполнения программы.
Re: Уменьшение екзешников, или Борьба с компилятором
Да, к сожалению,иногда, в fb не всё хорошо за оптимизировано. есть пример с select case
а теперь asm
вот программка, которая удаляет такие вещи из asm файла.
без оптимизаций на чистом FreeBasic.
в моём проекте весом в 35 килобайт, эта штука убрала 24 джампа. бывает так, что часто в блоке select case пишут exit sub или exit select или exit function.
- Код:
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
вот программка, которая удаляет такие вещи из 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- Сообщения : 391
Дата регистрации : 2008-09-02
Возраст : 43
Откуда : галактика Млечный путь, система Солнечная, планета Земля, страна россия, город Санкт Петербург
FreeBasic :: Программирование :: Общее
Страница 1 из 1
Права доступа к этому форуму:
Вы не можете отвечать на сообщения
|
|