На досуге задался вопросом возможности создания приложения, со следующими требованиями:

  • хоть сколько-нибудь полезная прикладная функция (то есть не пустышка)
  • наличие оконного интерфейса
  • размер менее 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)


  1. aleksandy
    04.07.2018 12:30

    Исходник на gist.github выложить было бы лучше.


    1. Boroda1 Автор
      04.07.2018 20:18

      Готово. Правда, форматирование несколько поехало.


  1. Vitaly83vvp
    04.07.2018 14:53

    А раньше писали приложение размером в 1 байт, но это было консольное. Кроме того на ASM с WinAPI GUI-приложение написать тоже не проблема (за несколько часов сделать можно). Поставил обработчик и всё.
    Другое дело, добавление элементов и укладывание в 1K — такого я не пробовал. Я только рисовал в ресурсах, а код его обрабатывал.


    1. AntonioKharchenko
      04.07.2018 20:08
      +1

      Поясните пожалуйста, как уместить приложение в 1 байт?


      1. Boroda1 Автор
        04.07.2018 20:21

        Думается мне, что COM-файл, который делает только выход с кодом возврата 0.


      1. Vindicar
        04.07.2018 20:40
        +1

        Я подозреваю два варианта.
        1. Линуксовый true. Команда, которая ничего не делает но возвращает код успешного завершения. Может быть пустым, так как пустой шелл-скрипт считается успешно завершенным.
        2. На какой-то древней платформе была особенность — после закрытия приложения память не очищалась. Таким образом, если память еще не была попорчена дргой программой, можно было передать управление на оставшийся в памяти код и «вернуться» в закрытую до этого программу, что было весьма круто в эпоху до многозадачности. Деталей не помню, но инструкция для перехода вряд ли занимала много места (дабы не попортить код, да).


      1. gbg
        04.07.2018 21:47

        Опытные специалисты могут создать коммерчески успешную программу длиной 0 байт


      1. Levhav
        05.07.2018 08:14

        Наверное вот habr.com/post/147075 ровно ноль байт.
        Меньше и при этом чтоб делало что то полезное уже не сделать.


      1. Vitaly83vvp
        05.07.2018 11:30

        Команда ret (код, C3, если не ошибаюсь). И, да, это COM файл с кодом завершения 0, но только более короткий вариант. Если использовать прерывание и непосредственное задание кода выхода, то получалось более 1 байта.


    1. speakingfish
      05.07.2018 00:16

      Существовали COM-файлы, состоящие из одного jmp на адрес в котором находилась строка параметров.
      Соответственно строка параметров должна была быть исполняемым кодом.
      Конечно на строку параметров накладывались ограничения, поэтому ничего сложного передать было нельзя, но некоторый несложный бинарный код запускать было можно.


  1. rocket
    04.07.2018 15:02

    Такое ощущение, что решение подогнали под результат. :)


    1. Boroda1 Автор
      04.07.2018 20:24

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


  1. arsenenko
    04.07.2018 20:08

    +


  1. Igor_ku
    04.07.2018 20:44
    +2

    Очень импонирует стиль изложения автора. Да и статья интересная получилась


  1. DeXPeriX
    04.07.2018 21:42
    +1

    Хех. Периодически встречаю такие статьи в сети, в стиле "делаем супер-бурбулятор всего за 5 кб". И возникал вопрос, что раз результат такой супер, то почему он не используется в реальных приложениях? Нужно обязательно попробовать, это будет круто! libc? Не, не слышали. Только хардкор.


    dxPmdxConverter


    Проект номер раз: dxPmdxConverter. Пока было без наворотов, влазило в 16кб. Здесь было: манифест (368 байт), загрузка форм из ресурсов (и соответственно возможность визуального их редактирования!), интерфейс на двух языках, быстрое чтение файлов через MemoryMappedFile. Потом захотелось кроссплатформенного PNG — это уже 32кб, т.к. библиотеку пришлось тащить с собой… Почти успех.


    Winter Novel


    Проект номер два: Winter Novel — коммерческая игрушка, продающаяся в стиме (исходники не доступны). Все использованные данные конвертированы в Си-массивы и линкуются компилятором. Версия под Windows может собираться на чистом WinAPI. Максимально ужатая полная версия игры занимает порядка 350 кб. Туда вошли: TTF, libDUMB (воспроизведение трекерных IT-файлов). И это был полный провал. Пришлось реализовать большое количество функций из стандартной библиотеки. ЕХЕ с использованием стандартной библиотеки в итоге получается даже меньше. Кроме того, чистую WinAPI-версию люто ненавидят антивирусы. От сжатия пришлось отказаться вообще — кто-то из антивирусов неадекватно реагирует даже на UPX...


    1. sabudilovskiy
      05.07.2018 18:28
      +1

      Насчет UPX и WinApi. Что первый, что второй так долго использовали кулхакеры собиравшие крипторы на коленке, что большинство AV готовы стереть с лица земли обоих.


    1. Boroda1 Автор
      05.07.2018 18:38

      Дело в том, что рядовому пользователю совершенно всё равно сколько весит бинарник — 1 Кб или 101 Кб. А разработчику удобнее накидать интерфейс с помощью готовых фреймворков.
      Да, жаль, что эти самые фреймворки такие тяжеловесные. Но увы, удобство разработки здесь в абсолютном большинстве случаев важнее.
      Погоня за размером сейчас — это сугубо спортивный интерес, как и в демосцене.


      1. DeXPeriX
        05.07.2018 18:45

        1 или 100 кб — не важно. Только фрейморки обычно добавляют как минимум десятки мегабайт… И сравнение получается уже 1 или 100 мегабайт. А здесь уже разница чувствуется. Хотя по сравнению с современным софтом в десятки гигабайт и это мелочи :-)


  1. MacIn
    04.07.2018 22:20

    Так каков размер приложения-то?
    Статья же о том, как написать маленькое приложение, а не о том, как запустить упаковщик, который, собственно, и создал маленький файл.


    1. Boroda1 Автор
      04.07.2018 22:35

      Если вопрос о том, какого размера бинарник на выходе после линкера MSVC — 2560 байт, включая выравнивания по 512 (линкер меньше не умеет).
      Этот же бинарник, сжатый, скажем, zip'ом — уже 1,27 Кб. Слинкованный crinkler'ом — 0,95 Кб.
      Линкер MSVC — совсем неподходящий для описанных требований инструмент.


  1. Alcpp
    05.07.2018 00:57

    Да, лет 10 назад баловался подобным, но на ASM и WinAPI.


  1. Tachyon
    05.07.2018 07:15

    Когда то глядя на графический интерфейс QNX и её возможности, задавался вопросом какого ху… дожника W98 весит так несоразмерно много?


  1. sanchezzzhak
    05.07.2018 10:42

    Давно баловался Delphi+kolmck приложения получались довольно маленькими в 30-300кб


  1. KVL01
    05.07.2018 11:29

    Спасибо автору – подсказал, как чужой манифест дёрнуть. Но инфу о версии и иконку всё равно надо свои иметь, а это уже +2 кб, как минимум, да и то, если вы фанат пиксель-арта и 16 цветов.