Введение

Существует масса способов уменьшить размер исполняемого файла. Когда кастрация системных RTL юнитов уже завершена, а размер по-прежнему режет простор для фантазии демосценера, работающего в направлении 4k intro или 64k demo, в ход вступают компрессоры. Один из самых известных среди них - UPX, т.к. существует под массой платформ и безразличен к содержимому исполняемого файла. Но существуют и компрессоры-линковщики, которые благодаря своей специфике способны производить более тонкие махинации с исполняемым файлом ещё на этапе сборки.
В этой статье пойдёт речь о широко известном в кругу демосценеров линковщике Crinkler и проблемах которые мне пришлось решить, чтобы приспособить это чудо для сборки Delphi проекта.

Как это работает

Первым делом следует объяснить принцип работы. Результатом работы компилятора являются объектные файлы, в которых находится уже скомпилированный машинный код. За линковщиком стоит задача грамотно собрать все эти файлы и связать вызываемые функции (раздать адреса). Применительно к Windows файл состоит из PE секций, основными из них являются секции непосредственно кода и таблица импорта функций внешних библиотек. Одной из недокументированных особенностей PE исполняемого файла является возможность уменьшить размер заголовка практически в 10 раз. Также присутствует одна из приятных особенностей таблиц импорта в PE - их можно индексировать! Это означает, что вовсе не обязательно сохранять имя для каждой ипортируемой из dll функции, а достаточно лишь порядкового индекса. Это безусловно является не безопасным в случае, если индексация функций в длл меняется от версии к версии, но применительно к системным библиотекам Windows "прокатит".

Компрессоры исполняемых файлов способны сжимать код и таблицу импорта внедряя в исполняемый файл свой код распаковки. Crinkler же буквально собирает все знания о компрессии PE накопленные человечеством (пафос) и выдаёт порезаный донельзя exe.

Подготовка и компиляция

Итак, с теорией разобрались, теперь приступаем к делу и тут же натыкаемся на грабли связанные с генерацией объектных файлов. Дело в том, что современные версии Delphi начиная с 5 используют OMF формат объектников, в то время как все C++ компиляторы (на которые расчитан Crinkler) генерируют файлы COFF формата. Отрешившись от некоторого "синтаксического сахара" можно смело откатиться до Delphi 3 которая умела создавать COFF. Но и на этом проблема не исчезнет, т.к. даже полученные COFF не совсем точно совпадают с необходимыми. Чтобы не вдаваться в подробности различия форматов просто скажу, что существует стандартный линковщик от Microsoft, который способен понимать и самое главное - исправлять, скомпилированные в Delphi 3 COFF для использования в Crinkler.
В итоге компиляция и исправление будет выглядеть как-то так:
dcc32.exe myunit.pas -jP link.exe -edit myunit.obj

Итак, допустим, компилировать объектные файлы нужного формата научились. Остаётся всего 2 проблемы: обрезка системных модулей и различие в именовании точки входа.

Системные модули в Delphi представлены в виде System.pas и SysInit.pas, они содержат в себе практически весь фундаментальный функционал используемый в программе (строки, динамические массивы, классы, интерфейсы и прочая не нужная демосценеру ерунда). Именно там находится точка входа программы. Сам компилятор сильно зависит от их содержимого, поэтому для успешной компиляции их наличие обязательно. Следует отметить, что компилятор Delphi неявно для каждого unit'а сохраняет код вызова initialization/finalization секций, даже если те совершенно пустые. Поэтому в целях экономии размера исполняемого файла демосценер-паскалеязычник должен сильно подумать, прежде чем создавать ещё один unit в проекте.
Путём многочисленных экспериментов, мне удалось уменьшить System.pas до вот такой радости:
unit System; interface type TGUID = Byte; var _HandleFinally : Byte; implementation end.

Для справки скажу, что _HandleFinally в оригинале является функцией и используется в недрах компилятора, но т.к. мы не хотим, чтобы наш линковщик ругался на неизвестные ему функции, делаем такой "хак" с переменной.

В SysInit.pas я бы порекомендовал включать код вашей демосцены, чтобы не плодить лишний код инициализации кучи unit'ов. А ещё лучше, включить ваш код прямо в секцию инициализации.
unit SysInit; interface var _HandleFinally : Byte; procedure ExitProcess(uExitCode: Cardinal); stdcall; external 'kernel32.dll' name '_ExitProcess@4'; implementation initialization //... код ... ExitProcess(0); end.

Наверняка зоркий глаз читателя заметил особенности именования внешних dll функций. Если смысл не ясен, то просто добавляйте "_" в начало и "@размер_параметров" в конец, либо же можно открыть блокнотом нужный lib файл и найти определение функции. Также, снова видим нашу фейк-переменную.
Компилируется это дело нехитрым bat'ником:
rm system.dcu rm system.obj rm sysinit.dcu rm sysinit.obj echo -------------- dcc32.exe system.pas sysinit.pas -jP link.exe -edit sysinit.obj crinkler.exe kernel32.lib sysinit.obj /OUT:test.exe /ENTRY:initialization$qqrv /PRINT:IMPORTS /PRINT:LABELS /SUBSYSTEM:WINDOWS /COMPMODE:SLOW /UNSAFEIMPORT /HASHSIZE:256 /HASHTRIES:1000 /ORDERTRIES:10000 /TRUNCATEFLOATS:8 pause

При желании можно поиграться с параметрами и получить от этого какой-то профит для конкретной ситуации.
После компиляции получим чистенький файл размером 572 байта. Если сейчас на файле пустышке это не кажется большим достижением, то по мере роста кода в ход пойдёт компрессор со всеми его достоинствами.

Стоит иметь ввиду, что процесс линковки Crinkler'ом занимает приличное время, потому для отладки весьма полезно будет приспособить link для сборки:
link.exe kernel32.lib user32.lib gdi32.lib opengl32.lib sysinit.obj /OUT:test_orig.exe /ENTRY:initialization$qqrv /MERGE:.rdata=.text /MERGE:_INIT_=.text /FILEALIGN:512 /SECTION:.text,ERWX /IGNORE:4078 /IGNORE:4108 /IGNORE:4089 /NODEFAULTLIB /SUBSYSTEM:WINDOWS В таком случае моментально получим наше приложение-пустышку размером 1.5кб.

Пример к статье