«Digital Rain» для Windows в 314 байтах

В комментариях к недавнему топику возникло обсуждение: до какого размера можно ужать Windows EXE, печатающий в консоли «Hello, World!» Ответ: 268 байт, меньшие файлы Windows просто отказывается загружать.

Раз для «Hello, World!» предел возможного ужатия уже достигнут, то мне стало интересно, до какой степени удастся ужать программу, делающую хоть что-нибудь более интересное.

Сначала похвастаюсь результатом: моя программа всего на 46 байт больше теоретического минимума!

28d49c4c317d4944b3fc065d0bcba275.gif

base64
TVprZXJuZWwzMgAAUEUAAEwBAQC4AwABAPdlEIlFEMN4AA8BCwEFDL0UEEAAjXyNAFfraD
gQAAAzyesoDAAAAAAAQAAAEAAAAAIAAAAAAAACAgoCBAAAAAAAAAAAQAAAAAIAALFQ68AD
AAAAEgEAAAAAAABQABkAABAAAFAAGQADAAAAAAAAAAAAAAAoEQAAKAAAAAAAAAAAAAAA/9
Wr4vvrEQAAMAAAABAAADkBAAABAAAAi/df6wMAAAAzybFQV4sHgPwZdygPttyNHJvB4waN
HItQweAYwegei0RFOIhEMwKIpDPC/v///9WIJDNY/sSA/GR8Av/Vq+LFjUVcUFH/dWhWZI
tBMItAEP9wHP9VWOuiV3JpdGVDb25zb2xlT3V0cHV0QQBsEAAAAAAAAAAAAAACAAAAbBA=

(Если найдётся доброволец захостить эти 314 байт, добавлю сюда ссылку.)

9a75d8376677a2e46f1bb16902097582.gif

Предыдущими энтузиастами было установлено, что самая маленькая программа для Win2000 и WinXP занимает 133 байта; самая маленькая 32-битная программа для Windows x64 — 268 байт. Последнее ограничение жёстко зашито в загрузчик Windows x64: если от начала заголовка PE до конца файла меньше IMAGE_NT_HEADERS64=0×108 байт, то Windows отказывается загружать файл. Заголовок PE не может начинаться раньше, чем по смещению 4, поэтому содержимое программы, занимающее (в минимально возможном варианте) даже меньше, чем 268 байт, приходится добивать нулями до минимально допустимого размера.

Существующие примеры программ размером 268 байт не содержат ни одной секции, и фактически, целиком помещаются внутри заголовка PE, для загрузки которого Windows выделяет одну страницу памяти (4КБ). Но для «Digital Rain» нужно хранить буфер экрана (80 столбцов × 25 строк × 4 байта на символ = 8000 байт), поэтому без секции в программе не обойтись. Это не так уж расточительно: заголовок секции — всего 0×28 байт, из которых больше половины не используются, так что их можно занять кодом. Фактически, «внутри» секции — после его заголовка — в моей программе располагаются часть исполнимого кода, имя импортируемой функции, и часть таблицы импорта (последние 0×16 её байт нулевые, и в файле не хранятся). Таблица импорта и следующие за ней 8КБ памяти во время работы используются для хранения данных (массив состояния и буфер экрана). Все инициализированные данные, часть служебной информации (цепочка импорта и имя библиотеки), и часть кода — распиханы по заголовкам. «Дважды используемые» поля подписаны на вышеприведённой презентации: данные лежат в MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, SizeOfStackCommit, SizeOfHeapReserve и LoaderFlags, а код — в SizeOfCode, SizeOfInitializedData, SizeOfUninitializedData, BaseOfCode, CheckSum, а также в поле имени секции и в её заголовках PointerToRelocations, PointerToLinenumbers, NumberOfRelocations, NumberOfLinenumbers и Characteristics. Для поля Characteristics (флаги секции) достаточно было, чтобы был установлен MSB (IMAGE_SCN_MEM_WRITE); на значения некоторых остальных полей также накладываются ограничения — например, SizeOfStackCommit и SizeOfHeapReserve должны помещаться в память. Значение поля PointerToLinenumbers системой по недоразумению принимается за размер таблицы отладочной информации, и если там значение больше размера программы в памяти (0×4000), то программа рушится при загрузке.

В предыдущих «минимальных» программах использовались значения FileAlignment=4 и SectionAlignment=4. В файле без секций Windows x64 такое позволяет;, но если хотя бы одна секция объявлена, то FileAlignment должен быть не меньше 0×200, а SectionAlignment должен быть 0×1000. Поэтому создать программу размером 268 байт, в которой была бы секция, невозможно: перекрытие e_lfanew=4 и SectionAlignment недопустимо. Сдвинуть заголовок PE на 4 байта мало, потому что ImageBase должен быть кратен 0×1000. Получается, что минимальный возможный размер программы с секцией — это 276 байт, с перекрытием e_lfanew=0xC и BaseOfData. Иными словами, моя программа на 38 байт больше минимально возможной, которая использовала бы >4КБ памяти.

Работоспособность своей программы я проверял на Win7 и на Win2008. На WinXP моя программа, к сожалению, не работает: та отказывается загружать программы, у которых в каталоге менее 0xD записей. Если в программе «Hello, World!» весь код занимает 0×10 байт, и им не жалко набить в файл 0×50 нулевых байт ради совместимости с WinXP — то мне обидно было ради этого раздувать программу на четверть размера. Приношу свои извинения всем любителям винтажных ОС.

© Habrahabr.ru