- хоть сколько-нибудь полезная прикладная функция (то есть не пустышка)
- наличие оконного интерфейса
- размер менее 1 Кб
Вообще, эталон приложения с размером до 1 Кб — это 1k intro, являющееся разновидностью демосцен. Чаще всего это написанная на ассемблере инициализация OpenGL с последующим скармливанием ему шейдера, который и выполняет основную работу (отрисовывает какие-нибудь раскрашенные фракталы). Плюс всё это пожато упаковщиком (например crinkler).
Эти приложения в буквальном смысле вылизаны до байта, их разработка занимает недели и даже месяцы.
Такой подход слишком суров, я решил не выходить за рамки обычного прикладного программирования на WinAPI.
В это время в Steam началась летняя распродажа, где взрослым людям предлагалось дичайше затыкивать инопланетян мышой в браузере (да-да, Valve снова делает игры, как и обещал Гейб).
Это оскорбило меня до глубины души, и я решил попробовать реализовать простейший автокликер с минимальными настройками.
Требовалось уместить в 1 Кб:
- создание и инициализацию интерфейса
- оконную функцию с обработчиками событий
- основную логику приложения (построенную на функциях GetAsyncKeyState и SendInput)
Приложение будет создаваться в MSVC на чистом C без ассемблера и СМС, а затем сжиматься пакером crinkler. Следует отметить, что данные (особенно разреженные) crinkler сжимает гораздо эффективнее, чем код (раза эдак в два). Поэтому будем стараться максимум функционала переносить внутрь данных.
Начав с классических CreateWindow для каждого элемента окна, я понял, что в требуемый размер я никак не влезу.
Пришлось искать альтернативу. И ей стала функция CreateDialogIndirect, создающая диалог из заполненной структуры DLGTEMPLATE (состоящей из кучи DLGITEMTEMPLATE)
Для удобного создания и заполнения структуры я завёл немножко макросов вроде таких:
#define NUMCHARS(p) (sizeof(p)/sizeof((p)[0]))
#define DLGCTL(a) struct{DWORD style; DWORD exStyle; short x; short y; short cx;
short cy; WORD id; WORD sysClass; WORD idClass; WCHAR wszTitle[NUMCHARS(a)]; WORD cbCreationData;}
#define RADIO(x,y,cx,cy,id,title) {WS_VISIBLE|WS_CHILD|BS_RADIOBUTTON, 0, (x)/2, (y)/2,\n
(cx)/2,(cy)/2, id, 0xFFFF, 0x0080, title, 0}
Теперь можно объявить и заполнить структуру элементами будущего окна:
static struct
{
DWORD style; DWORD dwExtendedStyle; WORD ccontrols; short x; short y; short cx; short cy; WORD menu; WORD windowClass;
DLGCTL(LBL_BTN_LEFT) button_left;
DLGCTL(LBL_BTN_MIDDLE) button_middle;
DLGCTL(LBL_BTN_RIGHT) button_right;
} Dlg =
{
WS_VISIBLE|WS_POPUP|WS_BORDER, 0, TOTAL_CONTROLS, 50/2, 50/2, WND_CX/2, WND_CY/2, 0, 0,
RADIO(10, 0, 80, 30, IDC_BTN_LEFT, LBL_BTN_LEFT),
RADIO(100, 0, 80, 30, IDC_BTN_MIDDLE, LBL_BTN_MIDDLE),
RADIO(190, 0, 68, 30, IDC_BTN_RIGHT, LBL_BTN_RIGHT),
};
Скармливаем структуру функции CreateDialogIndirect, и вот получившееся окно:
Так как мы умещаемся в 1 кб, то манифеста, как и всего прочего, у нас нет, а значит и визуальных стилей тоже. Всё серое и квадратное, как в молодости.
Но мы всё-таки извернёмся, дёрнув манифест из shell32.dll и применив контекст на его основе к нашему приложению:
static ACTCTX actCtx = {sizeof(ACTCTX), ACTCTX_FLAG_RESOURCE_NAME_VALID|ACTCTX_FLAG_SET_PROCESS_DEFAULT|ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID, "shell32.dll", 0, 0, tmp, (LPCTSTR)124, 0, 0};
GetSystemDirectory(tmp,sizeof(tmp));
LoadLibrary("shell32.dll");
ActivateActCtx(CreateActCtx(&actCtx),(ULONG_PTR*)&tmp);
Вот уже стильно, модно:
Оконную функцию удалось ужать до довольно компактной:
switch(msg)
{
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDC_BTN_LEFT:
case IDC_BTN_MIDDLE:
case IDC_BTN_RIGHT:
input[0].mi.dwFlags = wParam;
input[1].mi.dwFlags = (wParam<<1);
CheckRadioButton(hWnd,IDC_BTN_LEFT,IDC_BTN_MIDDLE,wParam);
break;
case IDC_BTN_HOLD:
case IDC_BTN_TRIGGER:
trigger_mode = (wParam==IDC_BTN_TRIGGER);
CheckRadioButton(hWnd,IDC_BTN_HOLD,IDC_BTN_TRIGGER,wParam);
break;
case IDC_EDIT_PERIOD:
period = GetDlgItemInt(hWnd,IDC_EDIT_PERIOD,(BOOL*)&tmp[0],0);
break;
case IDC_BTN_EXIT:
exit(0);
}
break;
}
return DefWindowProc(hWnd,msg,wParam,lParam);
И тут я подумал, что хорошо бы добавить обработку аргументов командной строки, дабы пользователь имел возможность запускаться с нужными настройками.
Например эдак:
input.exe /L /T /P:20 /K:9 - кликать левой кнопкой мыши каждые 20 мсек, режим включается/выключается клавишей Tab
Перенесём часть функционала внутрь данных и получим что-то вроде:
static unsigned int arg_to_cmd[] = {IDC_BTN_HOLD, 0, 0, IDC_EDIT_KEY, IDC_BTN_LEFT, IDC_BTN_MIDDLE, 0, 0, IDC_EDIT_PERIOD, 0, IDC_BTN_RIGHT, 0, IDC_BTN_TRIGGER};
i = (char*)GetCommandLine();
while(*i)
{
if (*(i++)=='/')//looking for argument
switch(*i)
{
case 'L':
case 'M':
case 'R':
case 'H':
case 'T':
SendMessage(hWnd,WM_COMMAND,arg_to_cmd[*i-'H'],0);//send button command
break;
case 'P':
case 'K':
t = atoi(i+2);
SetDlgItemInt(hWnd,arg_to_cmd[*i-'H'],t,0);
if(*i=='P')
period = t;
else
key = t;
break;
}
}
Обработчик вышел весьма небольшим. Естественно, никаких проверок и защит от неверного ввода, только необходимый минимум.
Теперь основной функционал (его я вынес в отдельный поток):
while(1)
{
key_state = (GetAsyncKeyState(key)>>1);
if (trigger_mode)
{
if ((key_state)&&(key_state!=prev_key_state))
active^= 1;
prev_key_state = key_state;
}
else
active = key_state;
if (active)
SendInput(2,(LPINPUT)&input,sizeof(INPUT));
Sleep(period);
}
Жмём получившийся obj-файл crinkler'ом — на выходе 973 байта.
Из них данные занимают 163 байта (степень сжатия 33.1%), код — 517 байт (степень сжатия 68.9%).
Можно ужать и сильнее (ещё байт на 50), но цель и так достигнута. Даже остался 51 запасной байт.
К первоначальнми требованиям по ходу добавились:
- обработка аргументов командной строки
- отображение окна с визуальными стилями
Местами код выглядит весьма криво. Это потому, что я его криво написал. А ещё кое-где это позволило сэкономить место.
Наверняка можно придумать ещё пару-тройку способов уменьшить размер приложения, но не кардинально (я думаю, байт на 50).
Результат:
Вполне возможно создавать сверхкомпактные приложения с реально используемым полезным функционалом и оконным интерфейсом.
Новизна:
Нулевая. Собрал в кучу несколько приёмов\наработок.
Целесообразность:
Бесполезно, забавы для.
Исходник
Бинарник
Принимаются критика, пожелания, предложения, восхищения, возмущения.
Комментарии (24)
Vitaly83vvp
04.07.2018 14:53А раньше писали приложение размером в 1 байт, но это было консольное. Кроме того на ASM с WinAPI GUI-приложение написать тоже не проблема (за несколько часов сделать можно). Поставил обработчик и всё.
Другое дело, добавление элементов и укладывание в 1K — такого я не пробовал. Я только рисовал в ресурсах, а код его обрабатывал.AntonioKharchenko
04.07.2018 20:08+1Поясните пожалуйста, как уместить приложение в 1 байт?
Boroda1 Автор
04.07.2018 20:21Думается мне, что COM-файл, который делает только выход с кодом возврата 0.
Vindicar
04.07.2018 20:40+1Я подозреваю два варианта.
1. Линуксовый true. Команда, которая ничего не делает но возвращает код успешного завершения. Может быть пустым, так как пустой шелл-скрипт считается успешно завершенным.
2. На какой-то древней платформе была особенность — после закрытия приложения память не очищалась. Таким образом, если память еще не была попорчена дргой программой, можно было передать управление на оставшийся в памяти код и «вернуться» в закрытую до этого программу, что было весьма круто в эпоху до многозадачности. Деталей не помню, но инструкция для перехода вряд ли занимала много места (дабы не попортить код, да).
Levhav
05.07.2018 08:14Наверное вот habr.com/post/147075 ровно ноль байт.
Меньше и при этом чтоб делало что то полезное уже не сделать.
Vitaly83vvp
05.07.2018 11:30Команда ret (код, C3, если не ошибаюсь). И, да, это COM файл с кодом завершения 0, но только более короткий вариант. Если использовать прерывание и непосредственное задание кода выхода, то получалось более 1 байта.
speakingfish
05.07.2018 00:16Существовали COM-файлы, состоящие из одного jmp на адрес в котором находилась строка параметров.
Соответственно строка параметров должна была быть исполняемым кодом.
Конечно на строку параметров накладывались ограничения, поэтому ничего сложного передать было нельзя, но некоторый несложный бинарный код запускать было можно.
rocket
04.07.2018 15:02Такое ощущение, что решение подогнали под результат. :)
Boroda1 Автор
04.07.2018 20:24Не совсем понял.
Естественно, решал задачу я именно так, чтобы в итоге получился минимальный размер кода и данных. Поэтому везде отрезал и ужимал.
Или имелось ввиду, что задача была подобрана таким образом, чтобы её удалось решить с заданными ограничениями?
DeXPeriX
04.07.2018 21:42+1Хех. Периодически встречаю такие статьи в сети, в стиле "делаем супер-бурбулятор всего за 5 кб". И возникал вопрос, что раз результат такой супер, то почему он не используется в реальных приложениях? Нужно обязательно попробовать, это будет круто! libc? Не, не слышали. Только хардкор.
Проект номер раз: dxPmdxConverter. Пока было без наворотов, влазило в 16кб. Здесь было: манифест (368 байт), загрузка форм из ресурсов (и соответственно возможность визуального их редактирования!), интерфейс на двух языках, быстрое чтение файлов через MemoryMappedFile. Потом захотелось кроссплатформенного PNG — это уже 32кб, т.к. библиотеку пришлось тащить с собой… Почти успех.
Проект номер два: Winter Novel — коммерческая игрушка, продающаяся в стиме (исходники не доступны). Все использованные данные конвертированы в Си-массивы и линкуются компилятором. Версия под Windows может собираться на чистом WinAPI. Максимально ужатая полная версия игры занимает порядка 350 кб. Туда вошли: TTF, libDUMB (воспроизведение трекерных IT-файлов). И это был полный провал. Пришлось реализовать большое количество функций из стандартной библиотеки. ЕХЕ с использованием стандартной библиотеки в итоге получается даже меньше. Кроме того, чистую WinAPI-версию люто ненавидят антивирусы. От сжатия пришлось отказаться вообще — кто-то из антивирусов неадекватно реагирует даже на UPX...
sabudilovskiy
05.07.2018 18:28+1Насчет UPX и WinApi. Что первый, что второй так долго использовали кулхакеры собиравшие крипторы на коленке, что большинство AV готовы стереть с лица земли обоих.
Boroda1 Автор
05.07.2018 18:38Дело в том, что рядовому пользователю совершенно всё равно сколько весит бинарник — 1 Кб или 101 Кб. А разработчику удобнее накидать интерфейс с помощью готовых фреймворков.
Да, жаль, что эти самые фреймворки такие тяжеловесные. Но увы, удобство разработки здесь в абсолютном большинстве случаев важнее.
Погоня за размером сейчас — это сугубо спортивный интерес, как и в демосцене.DeXPeriX
05.07.2018 18:451 или 100 кб — не важно. Только фрейморки обычно добавляют как минимум десятки мегабайт… И сравнение получается уже 1 или 100 мегабайт. А здесь уже разница чувствуется. Хотя по сравнению с современным софтом в десятки гигабайт и это мелочи :-)
MacIn
04.07.2018 22:20Так каков размер приложения-то?
Статья же о том, как написать маленькое приложение, а не о том, как запустить упаковщик, который, собственно, и создал маленький файл.Boroda1 Автор
04.07.2018 22:35Если вопрос о том, какого размера бинарник на выходе после линкера MSVC — 2560 байт, включая выравнивания по 512 (линкер меньше не умеет).
Этот же бинарник, сжатый, скажем, zip'ом — уже 1,27 Кб. Слинкованный crinkler'ом — 0,95 Кб.
Линкер MSVC — совсем неподходящий для описанных требований инструмент.
Tachyon
05.07.2018 07:15Когда то глядя на графический интерфейс QNX и её возможности, задавался вопросом какого ху… дожника W98 весит так несоразмерно много?
sanchezzzhak
05.07.2018 10:42Давно баловался Delphi+kolmck приложения получались довольно маленькими в 30-300кб
KVL01
05.07.2018 11:29Спасибо автору – подсказал, как чужой манифест дёрнуть. Но инфу о версии и иконку всё равно надо свои иметь, а это уже +2 кб, как минимум, да и то, если вы фанат пиксель-арта и 16 цветов.
aleksandy
Исходник на gist.github выложить было бы лучше.
Boroda1 Автор
Готово. Правда, форматирование несколько поехало.