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

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

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



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

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




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

Существующие примеры программ размером 268 байт не содержат ни одной секции, и фактически, целиком помещаются внутри заголовка PE, для загрузки которого Windows выделяет одну страницу памяти (4КБ). Но для «Digital Rain» нужно хранить буфер экрана (80 столбцов ? 25 строк ? 4 байта на символ = 8000 байт), поэтому без секции в программе не обойтись. Это не так уж расточительно: заголовок секции — всего 0x28 байт, из которых больше половины не используются, так что их можно занять кодом. Фактически, «внутри» секции — после его заголовка — в моей программе располагаются часть исполнимого кода, имя импортируемой функции, и часть таблицы импорта (последние 0x16 её байт нулевые, и в файле не хранятся). Таблица импорта и следующие за ней 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 системой по недоразумению принимается за размер таблицы отладочной информации, и если там значение больше размера программы в памяти (0x4000), то программа рушится при загрузке.

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

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

Комментарии (17)


  1. cjmaxik
    05.02.2016 06:54
    +6

    Разместите вторую gif-ку покадрово, пожалуйста.



  1. gasizdat
    05.02.2016 07:19

    Бинарник проще в онлайне формировать, чем хранить линк, например, так.
    VirusTotal, кстати напрягся.


  1. FSA
    05.02.2016 09:54
    +1

    Эх. Помню я как-то я журнале ZX-формат соревновались у кого меньше байт будет для плавной гасилки экрана. Вот были времена, когда программу в 21 байт укладывали.


  1. ef_end_y
    05.02.2016 12:47

    А где текст самой программы? Наверняка, общими усилиями сможем сделать меньше


    1. ComradeAndrew
      05.02.2016 14:26
      +1

      В спойлер под base64 спрятан.


  1. grechnik
    05.02.2016 13:04
    +1

    Ещё не всё прочитал, но навскидку:

    Существующие примеры программ размером 268 байт не содержат ни одной секции, и фактически, целиком помещаются внутри заголовка PE, для загрузки которого Windows выделяет одну страницу памяти (4КБ).
    Для загрузки заголовка выделяется ALIGN_UP(SizeOfHeaders, max(SectionAlignment, PAGE_SIZE)). Так что можно поставить SizeOfHeaders больше страницы и без секций обойтись.

    На WinXP моя программа, к сожалению, не работает: та отказывается загружать программы, у которых в каталоге менее 0xD записей.
    Там просто часть кода поддержки совместимости смотрит на Directories[DEBUG].Size, не проверяя, что DEBUG действительно есть. Если выставить соответствующий dword в нуль, всё будет работать.


    1. grechnik
      05.02.2016 13:21

      Минуточку. На XP SP3 оно вполне работает. (DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size у вас хоть и не 0, но 3, но пока оно меньше 0x1C, это нормально.) При отсутствии IMAGE_DIRECTORY_ENTRY_IAT = 0xC в XP есть засада, что таблица импорта должна быть в какой-нибудь секции, не в заголовке, но это проблема, только если делать программу целиком в заголовке без секций.


      1. tyomitch
        05.02.2016 14:30

        Как-то странно оно работает. Один и тот же файл запускаю — то запускается нормально, то выскакивает «Application Error: The application failed to initialize properly (0xc000007b). Click on OK to terminate the application.»
        Поди, опять что-нибудь связанное с файловым кэшем.

        (Всё равно спасибо за подсказку! Исправил пост, чтобы указать, что на WinXP программа иногда работает.)


        1. grechnik
          05.02.2016 15:51

          У меня на виртуалке запускается совершенно стабильно. Ошибка явно от юзермодного загрузчика, ему на файловый кэш должно быть ну совсем пофиг.
          WinDbg на машине есть?


          1. tyomitch
            05.02.2016 16:46

            WinDbg есть, но не помогает — ошибка вываливается ещё до DbgBreakPoint внутри LdrpInitializeProcess:



            Не исключено, что это шалит стоящий на машине антивирус (Symantec), но с ним я сделать ничего не могу — машина не моя, я только запустил программу.


            1. grechnik
              05.02.2016 17:01

              У WinDbg в настройках можно включить Debug -> Event filters -> Create process -> enabled, перезагрузить процесс, сделать bu ntdll!LdrInitializeThunk, g, и отлаживать совсем с начала.

              Но у меня появилась мысль, что может быть не так.

              часть таблицы импорта (последние 0x16 её байт нулевые, и в файле не хранятся)
              Если честно добить файл нулями, проблема будет воспроизводиться?


              1. tyomitch
                05.02.2016 17:52

                Окей, исключение прилетает отсюда:

                ntdll!_LdrpInitialize+0x178:
                7c84a446 57              push    edi
                7c84a447 e85e530100      call    ntdll!LdrpInitializationFailure (7c85f7aa)
                7c84a44c 57              push    edi
                7c84a44d e890e0fdff      call    ntdll!RtlRaiseStatus (7c8284e2)
                7c84a452 cc              int     3

                Выше по стеку вызовов — только ntdll!KiUserApcDispatcher+0x25

                Если честно добить файл нулями, проблема будет воспроизводиться?

                Да.


                1. grechnik
                  05.02.2016 18:16

                  Естественно. LdrpInitializeProcess возвращает 0xC000007B, а уже верхний уровень бросает исключение.
                  Тупой способ выяснить, откуда оно — после останова на LdrInitializeThunk выполнить команду
                  r;t;z(eax!=c000007b)
                  подождать, посмотреть, где остановилось, и посмотреть команды перед этим — чтобы понять, почему управление дошло до этой ветки кода.
                  Более интеллектуальный — настроить символы
                  .sympath SRV*c:\symcache*http://msdl.microsoft.com/download/symbols
                  .reload
                  (папку c:\symcache потом можно почистить), поставить bp ntdll!LdrpSnapIAT, дождаться второго срабатывания (первый вызов — для kernel32 -> ntdll) и запустить команду выше уже с этого момента.


    1. tyomitch
      06.02.2016 01:52

      Для загрузки заголовка выделяется ALIGN_UP(SizeOfHeaders, max(SectionAlignment, PAGE_SIZE)). Так что можно поставить SizeOfHeaders больше страницы и без секций обойтись.

      А можно пример? А то у меня с SizeOfHeaders больше 0x1000 прога напрочь отказывается загружаться.


      1. grechnik
        06.02.2016 16:02

        На XP и семёрке, казалось бы, никаких проблем: yadi.sk/d/4yHs4lWioPtmW
        В восьмёрке добавили несколько странную проверку, что EntryPoint должна быть либо 0, либо не меньше SizeOfHeaders, причём 0 для exe-шника — валидное значение (MZ = dec ebp, pop edx), так что можно так: yadi.sk/d/jY0Tn_lSoPvGc
        Но вообще формула выше — таки только для ветки с SectionAlignment >= PAGE_SIZE, на ветке с SectionAlignment < PAGE_SIZE весь файл просто маппится одним куском, так что можно просто сделать ImageSize побольше и даже не менять SizeOfHeaders: yadi.sk/d/c_Oc3oyooPwS8


  1. grechnik
    05.02.2016 14:01
    +2

    если хотя бы одна секция объявлена, то FileAlignment должен быть не меньше 0x200, а SectionAlignment должен быть 0x1000

    Строго говоря, можно. Например, если размеры всех секций кратны 0x1000, то проблем не будет и при SectionAlignment, меньшем размера страницы. (Вообще-то, соответствующий код в подсистеме WOW64 писался не под энтузиастов минимизации вроде нас с вами, а ровно под такую ситуацию. Конкретно — эмуляция x86 на Itanium, где винда использует размер страницы 0x2000.) Но не очень полезно — если честно записывать все дополняющие нули в файл, то о минимизации размера можно забыть, а если не писать, то винда от такой наглости может даже в BSOD выпасть. (ЕМНИП починили где-то между XP SP1 и SP2. Это к вопросу о винтажности. Хотя, конечно, истинно винтажная система — Windows 95.)