Эххх... RDR 2... Проработанный сюжет, большой открытый мир, внимание к деталям — вот что гласили заголовки статей. И всё это умещается в 116 ГБ памяти. Всего лишь? Нет. Много. Ждать, блин, ещё несколько часов пока он загрузится. «Вот никуда это не годится!» — подумал я, и тут же мне пришла идея «утереть нос» этим студиям — создать игру не хуже, которая будет размером с игру для NES — 40 КБ! Это всё фантазии конечно, но вспомнить запылишийся C можно и даже нужно, времени еще полно. Ладно, долой вступление, берем C99, GCC 14.2.0 и вперед.
Начнем с основы основ: "Hello, world!". Создаем main.c
:
#include <stdio.h>
void main(void)
{
printf("Hello, world!");
return;
}
Ну и сразу, чтобы не заморачиваться, напишем простенький Batch файл build.bat
, чтобы не вводить все тысячу раз, и сразу видеть размер файла:
@echo off
rem компилируем файл, изменять будем только эту строчку
gcc main.c -o main.exe
rem простенькая функция для вывода размера файла в байтах
call :size main.exe
goto :eof
:size
echo.
echo.Size: %~z1
Теперь вводим в консоль build
и видим катастрофу: Size: 257259
.
Целых 252 килобайта, на простейшую программу. Благо, есть решение — флаги gcc. Давайте пока глянем что у нас сейчас по флагам. main.c
определяет исходный файл с кодом, а -o main.exe
дает название exe'шнику, чтобы он не назывался a.exe
.
Сразу же можно докинуть в конец строки флаг -s
. Таким образом мы просим линкер выкинуть так называемую таблицу символов и некоторую отладочную информацию из файла, делая невозможным использования gdb. Но, как бы, нам и не нужно ничего дебажить, так что build
в консоль и фиксируем профит: Size: 44032
— ровно 43 килобайта, сокращение почти в 6 раз.
Давайте перечитаем программу от начала и до конца и найдем нашего следуещего врага. printf
— вот он! Почему? Вот почему:
printf
линкуется с libc, стандартной библиотекой C. Из-за этого требуется дополнительное место для кода некоторых инициализаций.printf
определена как вариадическая (принимает разное количество аргументов), что забирает ещё кучу места.printf
включает в себя сложное форматирование, что тоже занимает кучу байт.

К счастью у нас есть альтернатива: API Windows. Есть такая прекрасная функция MessageBoxA, которая линкуется с библиотеками Windows, не вариадическая и не требует каких-то инициализаций. Так что немного меняем код:
#include <windows.h>
void main(void)
{
MessageBoxA(0, "Hello, world!", "Hello, world!", 0);
return;
}
Ещё докинем флаг -mwindows
, чтобы нам не высвечивалась консоль. Вот все аргументы компиляции на данном этапе:
gcc main.c -o main.exe -s -mwindows
Прописываем build
: Size: 16384
— 16КБ. Неплохой прогресс.
Хорошо, теперь время для флагов немного подлиннее, сейчас они нам сэкономят всего 1 КБ, но если мы их спользовать не будем, то к концу размер exe'шника будет где-то в полтора раза больше чем мог бы быть.
Добавляем -Wl,--gc-sections
. Этот флаг используется для удаления ненужных секций, что в свою очередь приводит к сокращению и удалению неиспользуемого кода. И сразу добавим два огромных флага: -fno-unwind-tables
и -fno-asynchronous-unwind-tables
. Они удалят таблицы раскрутки стэка, которые по сути являются просто метаданными, не влияющими на работоспособность программы. Ну и конечно нужно добавить -Oz
— экстремальную оптимизацию кода, правда влияние на размер файла будет не такое экстремальное. Size: 14848
— минус еще один килобайт!
А теперь давайте глянем что у нас происходит с секциями.

Их много, причем тех, которые не использованны непосредственно в нашей программе: .reloc
, .xdata
, .tls
и другие. Так же много других функций. Зачем-то программа всё ещё требует malloc
, free
, strlen
. Всё это из-за main
'а — дефолтной точки входа в программу. На самом деле void main(void)
это не вся программа. До и после main
'а происходит много разных фокусов-покусов необходимых для большинства программ. Например, не смотря на то что мы не используем аргументы коммандной строки, они всё равно обрабатываются.
В этом случае нам придется создать свой main
. Меняем void main(void)
на void program(void)
. К флагам добавляем: -Wl,-eprogram
— для новой точки входа под названием program
и -nostartfiles
— для того чтобы убрать остальной мусор от main
'а. build
в консоль: Size: 2560
— 2.5 КБ, снова сокращение в разы.
Вот так сейчас выглядит строчка с флагами (не обращайте внимание на ^
, они используются для того чтобы разбить одну строку на несколько):
gcc main.c -o main.exe -s -mwindows ^
-Wl,--gc-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -Oz ^
-Wl,-eprogram -nostartfiles

По сути, это последнее легальное сокращение. Здесь обычно стопорятся люди прошедшие весь этот путь. Но не мы. Теперь придется немного заморочиться. Нет, мы не будем изменять файл вручную, как делал один человек, получивший exe'шник размером в 1 КБ (834 Б).
Если заглянуть в наш файл на данном этапе, нас ждут громадные пропасти из NUL
'ов — заполнителей пространства между секциями. Просто удалить их — не вариант, ничего не сработает, а вот удалить их сложно — можно. По сути все эти нули — результат того что позиция и размер секций выравнены по степеням двойки, причем большим. Поэтому стоит глянуть документацию PE (Portable Executable) файлов, причем именно документацию заголовка.
Там есть два интересных поля:
SectionAlignment
: отвечает за выравнивание секций в памяти (RAM)FileAlignment
: отвечает за выравнивание секций в самом файле, обычно равен 512, собственно такими блоками ОС и переносит данные из exe в память.
Оба значения можно установить через компилятор: -Wl,--file-alignment=0x8,--section-alignment=0x8
. Установить их нужно на минимальное значение: 8. Может появиться вопрос: А зачем нужно выравнивание именно в памяти, на размер файла это ведь не влияет? Не знаю. Шаманские фокусы и темная магия. Но без установки section-alignment
на то же значение, что и file-alignment
, ничего не запустится.
Итак, последний build
: Size: 744
— МЕНЬШЕ КИЛОБАЙТА! Празднуем! Вот строка с флагами:
gcc main.c -o main.exe -Oz -s -mwindows ^
-Wl,--gc-sections -fno-unwind-tables -fno-asynchronous-unwind-tables -Oz ^
-Wl,-eprogram -nostartfiles
-Wl,--file-alignment=0x8,--section-alignment=0x8
В смысле «где обещанные 640 байт»? Сейчас всё будет. Полностью очищаем main.c
и записываем там только void program(void) { return; }
. Компилируем и вот вам 640 Байт. Напомню, оно всё ещё запускается и не вызывает каких либо ошибок от Windows, так что технически является полноценной рабочей программой. (Еще добавлю, что можно докинуть -mwin32
, что выкинет еще 8 байт. 632 Б — мой рекорд).
Вы, я думаю, догадываетесь как можно замотивировать меня сделать 3D игру в 40 КБ. А RDR 2, у меня как раз скачался, так что спасибо за прочтение, а я пойду изучать, за что отдал 100 с лишним ГБ памяти...
P. S. Спасибо всем тем, кто поправил мои опечатки
Комментарии (32)
Akuma
26.06.2025 19:34Помню была такая штука как «Демо сцены» - маааленькие экзешники, которые выводили на экран всякие потрясающие штуки
Maccimo
26.06.2025 19:34Помню была такая штука как «Демо сцены»
У нас тут целых хаб на эту тему: https://habr.com/ru/hubs/demoscene/articles/
ItsYuuta Автор
26.06.2025 19:34Видел такое, даже статья на Хабре есть такая: https://habr.com/ru/articles/713550/. OCEAN32 - вообще крышесносная вещь, правда это .COM фаил а не .EXE, но всеравно круто
Timick
26.06.2025 19:34А как там нынче с .com? Во времена доса когда не нужно было больше 64кб оперативки компилировл в .com. размер получался смехотворный.
horribile
26.06.2025 19:34org 100; ret;
Работает, но нельзя привязывать библиотеки
n0isy
26.06.2025 19:34Разве работает? Я думал с win10 выпилили.
Panzerschrek
26.06.2025 19:34COM-файлы запускались через виртуальную машину DOS, которую убрали из 64-битных систем. Но никто не мешает использовать Dosbox.
jnzeax
26.06.2025 19:34Если и дальше бороться за истинный размер файла, то далее будет препарирование непосредственно самого бинарника, т.к можно выкинуть DOS-stub, поместить библиотеку импорта и данных в одну секцию рядом, и также оставить alignment маленьким, и в итоге получить что-то абсолютно крошечное.
jnzeax
26.06.2025 19:34P.S. Ради спортивного интереса, на основе предоставленного минимального файла, используя бинарный редактор, ловкость рук
и со скрипом зубовмне удалось сделать минимально работающий вариант в 376 байт. Запускал соответственно на Win10 x64.
P.P.S. Я пробовал установить поля FileAlignment и SectionAlignment отличные от тех что рисует компилятор (обычно это 0x200 и 0x1000) на другие произвольные значения, но все они давали нерабочий х32 бинарник, что впрочем следует из их документации, так что допустимое значений 0x8 в PE32+ стала в какой-то степени неожиданностью.ItsYuuta Автор
26.06.2025 19:34Это реально круто. Я глянул файл и в самом деле не нашел привычного: "This program cannot be run in DOS mode". Хорошо было бы самому по подробнее разобраться в документации PE файлов, и написать скрипт по вырезанию всего ненужного...
Panzerschrek
26.06.2025 19:34Я относительно недавно баловался с минимизацией размера исполняемых файлов, вот результаты. Простейший Hello World в 32-битном режиме умещается в 624 байта. Но это с использованием MSVC, а не MinGW. А в 4 килобайта можно уже какую-нибудь графическую демку или игру вроде Тетриса уместить.
NutsUnderline
26.06.2025 19:34Некоторые простейшие приложения требуют под 200-300 МБ,
можно подумать там .exe такой большой, там же не просто electron а еще и либы, на все случаи жизни
а вот в играх еще модели всяккие, текстуры, видео и музыка, их наоборот добавляют чтобы не напоминало тусовку мыльных пикселей
Turbo_Pascal_55
26.06.2025 19:34А Turbo Pascal сразу без оптимизаций делал минимальный exe в 16Кб.
pvvv
26.06.2025 19:34#include <windows.h> void main(){ MessageBoxA(0, "Hello, world!", "Hello, world!", 0); }
tcc main.c -luser32
main.exe: 3584 bytes
drWhy
26.06.2025 19:34Дельфи 3 ЕМНИП около 25 КБ без оптимизации. И 2,4 КБ с нею.
Ещё можно юзать командный процессор для прямого программирования в машинных кодах:
copy con reboot.com & [Alt+205, Alt+25] & ^Z
И всё же по соотношению функциональности к цене почти ничто не может быть лучше
Бесконечно выгодной программы.
domix32
26.06.2025 19:34Прикол обычно в том, что размер кода в играх - меньшая из проблем, т.к. основной объём занимают звуки и текстуры/материалы к моделям, когда на одну модель приходитсся несколько текстур с разрешением 2k+ - карты высот, теней/освещения, маски прозрачности и прочее в том же духе. Ну и звуки соотвественно на озвучку всех тех сотен часов диалогов, кои присутвуют в том же RDR2, это не считая тысяч SFX, включая озвучку взаимодействия разных же материалов.
AllSoliton
26.06.2025 19:34Я считаю, здесь нужно упомянуть Animal Well, который занимает меньше 40 Мб при наличии весьма красивой графики. А также консольные вещи вроде NetHack, некоторые модификации которого, к моему большому удивлению, весят больше Animal Well.
И при этом в этих играх — удивительное внимание к деталям.
Antimatter
26.06.2025 19:34Батник получился бы в разы меньше. Особенно сплевывающий в консоль.
Maccimo
26.06.2025 19:34Давайте перечитаем программу от начала и до конца и найдем нашего следуещего врага. printf — вот он!
В этот момент ожидал, что автор вспомнит про puts().
ItsYuuta Автор
26.06.2025 19:34В своих первых попытках уменьшить .EXE, я и использовал puts, но здесь я не стал рассказывать об этой функции из-за того что она требует инициализации stdout, а я её вырезал в одном из последующих шагов
А вот WriteConsoleA потребует AllocConsole, GetStdHandle или еще чего по хуже, так что о размере меньше 800Б можно было бы забытьsic
26.06.2025 19:34Ну шеллкодам в 100- байт ничего не мешает и побольше функций системы импортировать.
MountainGoat
Я хотел сделать скринсейвер, но я графикой никогда не занимался. И у меня был пунктик - нужна поддержка многих мониторов сразу. Кое как вник в Vulkan, шейдеры, вся фигня. Нормально заработало на одном экране. Но при отображении сразу на несколько экранов у меня полезли ошибки, которые никто на форумах объяснить не мог, и тексты в логах, при гуглении которых находятся только исходники той библиотеки ,что их пишет.
В итоге появилось ощущение, что мне сейчас нужно потратить год на изучение теории этого всего, или идти другим путём.
Теперь мой скринсейвер открывает Internet Explorer на весь экран без элементов интерфейса, в нём грузится страница, а на ней моя анимация на TypeScript. Всё плавно и сочно, никаких подтормаживаний. Стартует секунды две, в основном из-за антивируса. Система сообщает, что на два экрана готова рисовать до 5 000 FPS, но конечно она VSync-нута. EXEшник весит 5 Мб, жор памяти ОС показывает 3 Мб.
Так что может ну их нафиг уже, эти примитивы...