В статье вы узнаете как сделать маленькие программы для MS-DOS на ассемблере, я покажу как рисовать 2D графику напрямую в видео-буфер. Может быть, вы даже вдохновитесь на создание собственного демо, которое будет ставить рекорды по размерам исполняемого файла.

INIT

Демосцена удивляет не только эффектными программами выжимающими максимум из маломощных платформ, но и ультра-маленькими исполняемыми файлами. На сайте Pouet.net можно найти программы размером не более 32 байт и большинство из них написаны под ОС MS-DOS, В некоторых демках даже играет звук!

OCEAN32 by Desire
OCEAN32 by Desire

На гифке выше изображён океан с движущимися волнами и бликами на воде. Вся эта красота занимает всего 32 байта. Это настолько маленькая программа, что эта гифка весит в 30 раз больше неё и из этой демки даже нельзя выйти, потому что автор демки убрал такую возможность, ибо это заняло бы ещё несколько лишних байтиков на диске. Также есть бонусная версия этой программы под названием OCESC64, она весит 64 байта и из неё даже можно выйти нажав Escape, а также в неё добавлен шум моря.

Для запуска этих программ вам понадобится любой эмулятор Доса, подойдёт даже DOSbox. В этом гайде я расскажу как компилировать программы для этой системы используя Windows/Linux или как сделать это прямо внутри DOS. Эксперименты можно ставить даже на телефоне с эмулятором.

Почему эти программы такие маленькие?

DOS умеет запускать .exe файлы и .com файлы, во вторых-то и кроется секрет малого размера. Дело в том, что бинарники .exe (или elf в Linux) содержат в себе не только код программы но и хедер с дополнительными функциями, без которых программа не сможет запускаться и корректно завершаться. Хотя exe или elf файл можно написать вручную, побайтово заполнив хедер по всем канонам и добиться минимального размера программы, но это всё будет долго, больно и всё равно выйдет больше по размеру чем .com программа для Доса. Формат .com программ настолько прост, что не содержит в себе ничего кроме инструкций самой программы - всё будет весить ровно столько, сколько весят сами ассемблерные команды и даже здесь есть трюки, которые позволят уменьшить размер бинарника, но об этом позже.

Часть заголовка .exe файла. Код юзера начинается спустя сотни байт ниже...
Часть заголовка .exe файла. Код юзера начинается спустя сотни байт ниже...
Программа OCEAN32 для сравнения масштабов
Программа OCEAN32 для сравнения масштабов

Начинаем кодить на ассемблере под DOS в 2023

Создать программу можно прямо в Windows/Linux, открываете текстовый редактор и пишите код ниже.

org 100h
ret

Этот код ничего не делает, будем использовать его для тестирования возможности компиляции. Сохраните код в файл main.asm и скомпилируйте командой: yasm -wall -fbin main.asm -o PROG.COM

Yasm это ассемблер, можно использовать и Nasm, гуглите и качаете. Получившийся файл PROG.COM запускаем в эмуляторе. Для досбокса достаточно перенести этот файл на ярлык программы и всё запустится само. Итак, если вы уже видите работающую консоль Доса, значит всё работает и ничего не произошло (ведь программа просто вышла из самой себя). Убедитесь что файл PROG.COM создался без ошибок.

Программу можно собрать и из под Доски, для этого вам понадобится компилятор A86. Команда для компиляции следующая: a83 main.asm prog.com

Если вы и код собираетесь писать на досе, то можете попробовать EDITV, там даже мышка поддерживается.
Если вы и код собираетесь писать на досе, то можете попробовать EDITV, там даже мышка поддерживается.

Какие подводные?

Если вы сделаете что-то не так в вашей программе, то всё взорвётся - любой вечный цикл застопорит всю систему и вы не сможете выйти даже если будете нажимать Esc или Ctrl+С.

Некоторые графические программы будут выглядеть по-разному на настоящем MS-DOS, установленном на компьютере и на эмулируемом в DOSbox.

Тут не будут описаны азы ассемблера и если вы не знаете что такое регистр памяти и статус выполнения команды, то вы вряд ли поймёте всё что я тут понаписал. Погуглите или посмотрите видосы по теме.

Программировать .exe файлы для Доса можно и на Си, но этот гайд не об этом - чем меньше программа тем лучше, а в .exe файле есть лишние данные, которые не являются самой программой.

Ассемблер действительно сложный язык, поэтому запаситесь справочными материалами. Например скачайте таблицу со всеми командами процессора i8086 и коды системных прерываний Доса. Вы должны понимать, что DOS это 16-битный ОС и даже если ваш процессор поддерживает x64 регистры, то в Досе вы всё равно будете работать с двухбайтовыми значения (аналог short из Си).

Наша первая программа - цветная эпилепсия

Сейчас вы узнаете как сделать полноэкранное мерцание разными цветами, буду описывать каждую строчку кода.

В реалтайме оно мигает ещё быстрее
В реалтайме оно мигает ещё быстрее
Весь код программы (разъяснения ниже)
  MX equ 320
  MY equ 200
  VID_SZ equ MX * MY
  VID_BUF equ 0A000h ; start pointer to video mem (for ES)
segment .bss
  fill_color db ?
segment .code
  org 100h
  ;set video mode:
  ; 13h 320x200 256/256K VGA,MCGA,ATI VIP
  mov ax, 13h
  int 10h
redraw:
  mov ax, VID_BUF ; set video pointer
  mov es, ax
  xor di, di ; clear addr
  mov cx, VID_SZ
  mov al, byte [fill_color]
  inc al
  mov byte [fill_color], al
fill:
  stosb ; ES:DI = fill_color value (al)
  loop fill
; wait ESC
  push ax
  in al, 60h
  dec al
  pop ax
  jnz redraw ; if !ESC key, goto main
; return 2 text mode:
  mov ax, 3h
  int 10h
; exit:
  mov ah, 4Ch
  int 21h
  ret

Для начала, в коде описываются константы через команду equ (аналог #define из си):

  MX equ 320
  MY equ 200
  VID_SZ equ MX * MY
  VID_BUF equ 0A000h

MX и MY - разрешение экрана в видео-режиме, который будет выбран далее. При старте, MS-DOS настраивает биос на вывод символьной графики, но режим можно переключить и рисовать цветные пиксели в любом месте экрана. Для рисования точек на экране в биос предусмотрена отдельная функция, но я не буду её использовать, потому что она медленно работает, а пикселей нам придётся нарисовать очень много. Я буду сразу записывать значения цветов в видео-буфер, так гораздо быстрее. Все изменения в видео-буфере сразу отображаются на экран, никакой вертикальной синхронизации в коде не прописано. Код для VSync я покажу позже.

VID_SZ - вес всех пикселей в видео-буфере. Я буду использовать палитровую графику, поэтому размер каждого пикселя будет равен одному байту. Цвета будут в диапазонах от 0 до 255, а какой цвет к какому числу относится я не помню, но это и не важно - программа покажет их все.

VID_BUF - адрес начала видео-буфера. Число 0A000hзаписано в шестнадцатеричном виде, об этом нам говорит буковка h в конце (hexadecimal), 0 в начале числа обязателен, так как нельзя начинать число с буквы, такое уж правило у компилятора. Всё что будет записано в оперативку начиная с этого адреса (0A000h) будет автоматически отображено на экране.

segment .bss
  fill_color db ?

строка segment .bss намекает компилятору что описывается расположение переменных в памяти. На ассемблере вы должны всегда держать в уме сколько байтов занимают ваши переменные, чтобы случайно не заскочить на данные других соседних переменных. И код и переменные - всё грузится в оперативную память, а значит вы можете изменить код своей уже загруженной в программы просто перезаписав данные. MS-DOS никак не защищает код вашей программы от изменения (свои системные данные тоже не защищает). Что же значит dw и db? Так обозначается сколько байт нужно выделить под переменную:

  • db - 1 байт, int8_t, BYTE;

  • dw - 2 байта, uint16_t, short, WORD;

  • dd - 4 байта, uint32_t, long, DOUBLE WORD;

  • dq - 8 байт, uint64_t, long long, QUAD WORD;

  • dq, dd, dt можно использовать чтобы хранить числа с плавающей запятой, но в i8086 нет FPU чтобы с ними работать.

Символ ? обозначает данные без начального значения.

Переменные x/y нужны будут для итерации по всем пикселям буфера, а переменная fill_colorбудет хранить код цвета для заливки экрана.

segment .code
  org 100h

segment .code подсказывает компилятору что начинается сегмент с кодом программы. Сегменты имеют большую роль в .exe файлах, но в .com это не особо важно.

org 100h указывает компилятору что код программы должен грузиться в оперативную память с отступом в 256 байт (число 100 в hex) от нулевого адреса, чтобы не задеть данные ОС Дос. В принципе, вы можете перезаписывать память Доса или полностью её стирать - от этого ваш комп не взорвётся, но потом вы не сможете вернуться обратно в систему после завершения программы, так как возвращаться будет уже некуда...

mov ax, 13h
int 10h

А это уже началась интересная часть. Тут выбирается видео-режим под номером 13h. Многие материнские платы обеспечивают работу данного режима, он позволяет выводить графику в разрешении 320x200 пикселей и с использованием палитры (цвета фиксированы в таблицу, RGB и прозрачности нет в этом режиме нет). Есть много других режимов позволяющих рисовать четырёхцветную графику или ЧБ, а также есть экзотические коды видео-режимов, которые включают большие разрешения и большую глубину цветоа, но у вас скорее всего не получится их использовать, потому что их не реализовали в эмуляторе и на вашей материнской плате.

палитра цветов режима 13h
палитра цветов режима 13h

Команда mov перемещает данные из одного места в другое. Нельзя перемещать напрямую данные из одной ячейки оперативки в другую, нужно сначала загрузить ячейку из памяти в регистр, а потом в другую ячейку (кому-то было лень добавить ещё одну перегрузку для mov).

mov ax, 13h - записываем константу с числом 13h в регистр ax. Так как ax это 16-битный регистр, то в нём будет лежать число 0013h.

int 10h вызывает программное прерывание из BIOS, отвечающее за работу с видео. Прерывание — это остановка вашей программы и передача управления обработчику прерываний в подпрограмме биоса. Когда подпрограмма отработает, ваша программа возобновит свою работу в месте, где она прервалась. Через int 10h можно выводить буквенные символы в консоль, рисовать пиксели или менять видео-режим. Действие, которое будет выбрано зависит от того, что находится в регистре ah - старшая часть двухбайтового регистра ax, а в него мы ранее записывали 0013h, то есть в ah будет 0, а в al (младшая часть ax) будет записан 13h. Если вам трудно это представить, то посмотрите на схему:

В прерывании 10h выбирается функция под номером 0 (команда выбора видео-режима) и выставляется видео-режим под номером 13h (VGA 320x200). О том, какие видео-режимы есть и какие числа вам надо вписать в ah/al вы можете узнать из инфы по прерыванию 10h.

; Можно было бы написать более понятнее
mov ah, 0 ; set video mode
mov al, 13h ; VGA 320x200 plt
int 10h

; но я сэкономил одну инструкцию для уменьшения веса бинарника
mov ax, 13h
int 10h
redraw:
  mov ax, VID_BUF
  mov es, ax
  xor di, di
  mov cx, VID_SZ
  mov al, byte [fill_color]
  inc al
  mov byte [fill_color], al

redraw: - метка, она ничего не весит и не делает, но на неё можно ссылаться чтобы "прыгать" на участок кода стоящий после метки. Работает аналогично меткам для goto из языка Си.

В ax записывается адрес начала видео-буфера, потом это перекидывается в es, потому что сразу константу нельзя туда закинуть (опять кому-то было лень реализовывать это аппаратно в процессоре). Можно ещё закинуть значение ax в стек и загрузить оттуда значение в es через команды push/pop, но это уже лишние инструкции.

xor di, di - смысл команды в том, чтобы записать в di число 0. Почему же нельзя просто сделать mov di, 0 ? Потому что опять придётся записывать 0 в какой-нибудь регистр, а оттуда перекидывать 0 в di. XOR это побитовая операция "отрицательное или", а как известно, если применить операцию xor к одному и тому же числу, всегда получится ноль. Зачем же так извращаться? Дело в том, что xor X,X будет кодироваться в один байт, а это экономия размера, такой вот фокус.

mov cx, VID_SZ - в cx записывается размер видео-буфера.

mov al, byte [fill_color]- грузим значение цвета заливки в al. byte [fill_color] обозначает что мы работает с байтом по ссылке указанной в fill_color. Компилятор сам подменяет fill_color на адрес ячейки в памяти, fill_color это лишь алиас цифрового значения и при компиляции в ассемблерном коде он будет именно числом.

inc al - инкремент регистра al, просто прибавляем к нему 1. Это нужно для прокрутки цветов по палитре. Если надо прибавить 1 к регистру, используйте всегда инкремент заместо add, потому что инкремент займёт меньше памяти.

mov byte [fill_color], al - новое значение цвета заливки грузим обратно в fill_color. В регистре al всё ещё остаётся значение пикселя, которое будет использоваться далее.

fill:
  stosb ; ES:DI = fill_color value (al)
  loop fill

loop повторяет переход к метке fill: до тех пор, пока cx не станет равен нулю, а в cx я ранее записал размер видео-буфера, то есть этим кодом я пробегаюсь по всем пикселям экрана и записываю в них значение цвета заливки (оно в al). Для организации цикла можно было бы написать метку, операцию сравнения и условный переход на метку, но это бы заняло больше места.

stosb - команда не имеющая параметров, но каждый её вызов записывает новый пиксель в видео-буфер. Значение пикселя должно быть в al, в регистре es хранится адрес начала массива данных, то есть начало видео-буфера, а di служит в роли индекса элемента и автоматически увеличивается на 1 при каждом вызове stosb. Можете считать эту конструкцию аналогом memset из Cи, но работающим пошагово. Видео-буфер находится по далёкому адресу и поэтому доступ к нему такой мудрёный.

Как заметил @pfemidi, можно просто написать rep stosb. Команда rep повторяет операцию cx раз.

push ax
in al, 60h
dec al
pop ax
jnz redraw

смысл конструкции выше заключается в проверке на нажатие клавиши Esc.

push ax - бэкап значения из ax в стек.

in al, 60h - чтение сканкода из порта 60h в al. На порту 60h располагается клавиатура.

dec al - отнимаем от al 1, если в результате будет 0, то значит сканкод был равен Escape (01h). Нужно получить именно 0, чтобы далее было легче по флагу Z произвести условный переход.

pop ax - возвращаем из стека старое значение ax.

jnz redraw - jnz значит jump if not zero, выполнить переход к метке redraw, если после декремента al не было нулевого результата. Проверяем что код клавиши не был равен 1 (Esc) и переходим к началу отрисовки чтобы поменять цвет. Таким образом программа будет работать пока мы не нажмём Esc.

Если же мы нажали Escape, то jnz пропустит нас к коду ниже.

mov ax, 3h
int 10h

вызываем выбор видео-режима в биосе с параметрами al = 3 и ah = 0, это позволяет вернуться в текстовый режим Доса. Если этого не сделать, то по завершению вашей программы вы не сможете нормально работать с консолью. Можно ещё попробовать написать команду очистки консоли cls (это в консоли Доса писать) и тогда режим консоли сам восстановится.

mov ah, 4Ch
int 21h
ret

Это код корректного закрытия приложения.

int 21h вызовет системное прерывание Дос, а параметр 4Ch в ah укажет что приложение завершило работу.

ret - возвращает нас из программы обратно в систему

Вторая программа - кислотная анимешка

Программа работает по принципу верхней, но содержит в своём коде целую картинку, которая рисуется на экран и переливается разными цветами

В гифке я сильно пожал цвета чтобы она мало весила, но думаю смысл программы вы поняли
В гифке я сильно пожал цвета чтобы она мало весила, но думаю смысл программы вы поняли
Код
  MX equ 320
  MY equ 200
  VID_SZ equ MX * MY
  VID_BUF equ 0A000h ; start pointer to video mem (for ES)
segment .bss
  state dw ?
segment .code
  org 100h
  ;set video mode:
  ; 13h 320x200 256/256K VGA,MCGA,ATI VIP
  mov ax, 13h
  int 10h
; print start screen:
  ; set to video pointer
  mov ax, VID_BUF 
  mov es, ax
  xor di, di
  mov cx, VID_SZ
  mov bx, img
copy_img:
  mov al, byte [bx]
  inc bx
  stosb ; ES:DI = pixel value (al)
  loop copy_img
redraw:
  ; set to video pointer
  mov ax, VID_BUF 
  mov es, ax ; es:di 4 write
  xor di, di
  mov ds, ax ; ds:si 4 read
  xor si, si
  mov cx, VID_SZ
col_rot:
  lodsb ; pixel = DS:SI
  inc al
  stosb ; ES:DI = pixel value (al)
  loop col_rot
; wait ESC
  push ax
  in al, 60h
  dec al
  pop ax
  jnz redraw ; if !ESC key, goto main
; return 2 text mode:
  mov ax, 3h
  int 10h
; exit:
  mov ah, 4Ch
  int 21h
  ret
img: ; 320x200 bytes of picture
  incbin "anime.dat"

Компилятор встраивает файл anime.dat прямо в исполняемый файл благодаря команде incbin, сам же файл картинки представляет из себя сырые данные пикселей без сжатия - 320x200 байт. Программа копирует картинку на экран и прибавляет к цветам единицу, что заставляет цвета картинки перекручиваться в радужные узоры. Вам нужен специальный софт для преобразования картинок в палитровый формат, я такой не знаю и использовал свою программу написанную на C++, а вы погуглите, если не лень. Можно ещё сгенерировать картинку через FFmpeg и заменить название файла в конце кода на ваш. Вот пример команды для генерации картинки для встраивания в код: ffmpeg -y -i "название исходной картинки" -vf "scale=320:200:flags=lanczos, format=gray" -f rawvideo "название выходного файла картинки.dat"

Я проверил, у вас тоже получится
Я проверил, у вас тоже получится

Прочее

Ameisen by Rrrola
Ameisen by Rrrola
Fourmis by Rrrola
Fourmis by Rrrola
dirojedc
dirojedc

Скачать эти демки

Все эти узоры действительно умещаются в 32 байта, а некоторые даже в 21. Я сам не представляю как авторы демок до такого додумались. В следующем разделе я подскажу как уменьшить размер программ.

Вы можете реверс-инжинирить любую программу и даже эти демки, для этого можно использовать утилиту ojbdump поставляемую вместе с компилятором gcc. Команда для просмотра кода .com файла: objdump -D -M intel -b binary -m i8086 "ваш .com файл"

Дизассемблинг в objdump
Дизассемблинг в objdump

Программировать можно не только с набором команд i8086, в FreeDOS можно даже задействовать XMM регистры. Я просто взял i8086 в качестве минимальной поддерживаемой Досом платформы.

Код для VSync
wait_sync:
    mov dx,03DAh
wait_end:
    in al,dx
    test al,8
    jnz wait_end
wait_start:
    in al,dx
    test al,8
    jz wait_start
    ret

Советы по уменьшению размера программы

  • На ассемблере можно писать свои функции, вызывать их через call и возвращаться из них через ret, это удобно, но затратно по размеру, так что заместо этих функций используйте метки и условные/безусловные переходы на них. Используя функции вам бы ещё пришлось пушить регистры и подготавливать аргументы перед вызовом - это бы заняло десятки байт, а с джампами всё просто.

  • Если хотите занулить регистр, то делайте это через xor. Применение операции xor для одинаковых чисел всегда заканчивается результатом 0 и бонусом вы получаете выигрыш в размере программы.

  • Предпочитайте loop для циклов заместо перехода на метку по условию.

  • При записи значений в массивы используйте stosb (для байтов) или stosw (он перемещает 16-битный данные)

  • Деление и умножение может оказаться медленным, но если число делится или умножается на 2, то можно использовать побитовой сдвиг влево и вправо соответственно, это даст эквивалентный результат делению и умножению на 2.

  • В моей программе картинка загружена без сжатия, но вы можете смастерить свой алгоритм сжатия, например сжатие повторений. Можете сделать картинку чёрно белой и хранить цвета в виде битов, а остальные свободные 7 бит от байта, вы можете отдать под хранение количества повторений пикселя от 1 до 128 раз.

  • Рисование графики функциями - всегда экономнее чем хранение кадров прямо в программе (гуглите фрактальное сжатие изображений), но если вы собрались запихнуть в бинарник видео с Рикроллом, то уменьшите частоту кадров, глубину цвета и храните полностью только первый кадр, а остальные кадры храните в виде разницы между кадрами, так вы сэкономите данные храня только изменения в кадре - так сделано в .gif формате.

  • Используйте свободные регистры по максимуму. Несмотря на то что у регистров есть своё назначение, например ax - аккумулятор и т.д., вы должны игнорировать эти условности и плотно паковать промежуточные данные по всем свободным регистрам.

  • Если вы будете хранить данные в оперативке, то вам придётся их оттуда грузить, а это уже лишняя инструкция. А если вы ещё и захотите обмениваться данными между двумя ячейками памяти, то вам придётся их грузить в промежуточный регистр, потому что отдельной команды делающей это за 1 раз просто нету, так что меньше обращайтесь к памяти.

  • Стек тоже старайтесь не использовать, так как на любой вызов push вы наверняка ещё и вызовете pop.

  • Забудьте про правильный выход из программы, даже про ret в конце забудьте. Вам же важно показать шоу и при малом размере, поэтому сосредоточьтесь именно на коде демки, даже если её невозможно будет закрыть.

  • Чтение с порта клавиатуры вы можете использовать в качестве источника зёрен рандом-генераторов, так сделано во многих демках. Поэтому вы можете не писать громоздкие алгоритмы получения псевдослучайных чисел.

  • Системный вызов setpixel не используйте, он медленный. Напрямую пишите в видео-буфер предварительно погуглив с какого адреса он там начинается в вашем режиме.

С учётом всех премудростей, мне удалось сократить размер первой программы-мигалки до размеров 22 байт

  MX equ 320
  MY equ 200
  VID_SZ equ MX * MY
  VID_BUF equ 0A000h
  mov ax, 13h
  int 10h
redraw:
  mov dx, VID_BUF
  mov es, dx
  xor di, di
  mov cx, VID_SZ
  inc al
fill:
  stosb
  loop fill
  jmp redraw

RET

Всё описанное здесь имеет мало практической пользы в наше-то время, но зато теперь вы сможете хвастаться о своих навыках ужимания бинарников. На ассемблере проще всего программировать именно под старые системы, потому что железо раньше было проще и инструкций в процессоре было меньше. Процессоры Intel обратно совместимы с i8086 и в них присутствуют все регистры из 16-битных предков, будь у вас хоть кор2дуо, хоть i9, вы можете ставить свои эксперименты даже на современном железе. Также легко рисовать графику как в Досе вы не сможете в Linux или Windows, потому что они переводят процессор в защищённый режим, который запрещает вам большинство прерываний, вы не будете иметь прямого доступа к видео-буферу и не сможете стирать данные системы из оперативки как в Дос. Некоторые трюки, как например xor-зануление, используют и сейчас - компиляторы gcc и clang могут заменять mov X,0 на xor X,X для оптимизации производительности и размера программы.

Если вы являетесь бородатым динозавром и эта статья напомнила вам вас в молодые годы, то поделитесь в комментариях своими трюками и хитростями хардкодинга, а если же вы ничего не поняли и у вас что-то не получилось, то задавайте вопросы.

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


  1. grungeroid
    31.01.2023 03:57

    Нда, низкоуровневый доступ к фрейм-буферу в win/linux... А если нельзя, но очень хочется? Есть что-нить наподобие?


    1. AtariSMN82 Автор
      31.01.2023 04:04
      +3

      Учитывая что в Линуксе всё - файл, то можно подключиться /dev/fb0 через системные вызовы и писать туда данные как в файл, это рабочий способ общаться с кадровым буфером из защищённого режима. Есть программы, которые способны без графического режима сохранять кадровый буфер, когда линукс работает в режиме консоли. Наверное можно и звуки издавать, если отправлять на аудио устройство /dev/audio байтики.


    1. AtariSMN82 Автор
      31.01.2023 04:05
      +1

      Для Винды же придётся подключить winapi и получить дескриптор нулевого окна - это автоматом даст вам доступ ко всему экрану и вы сможете рисовать пиксели поверх всех окон, я так делал только на C++.


      1. grungeroid
        31.01.2023 21:47

        Благодарю.


    1. AtariSMN82 Автор
      31.01.2023 04:10

      Можно ещё на ассемблере инициализировать окно с помощью SDL2 и получить доступ к фрейм-буферу из OpenGL. Но это всё не то, вот на Досе весь экран принадлежит только вам без программных посредников.


      1. EXL
        31.01.2023 18:36
        +2

        У @w23 (он же provod), которого вы наверняка знаете, давно была неплохая серия статей про написание демок для Linux, где он описывал все эти подводные камни там, почитайте на досуге если интересно, начало здесь: Создание 1k/4k intro для Linux, часть 1.


  1. IvanPetrof
    31.01.2023 05:36
    +6

    Помню в детстве, когда в паскале писал свой менеджер текстовых окон (с прямой адресацией в видеопамять), очень удивился, когда увидел, что моя dos-программа, запущенная под виндой (не помню какой, может 95) внезапно корректно отображалась внутри окошка dos-сеанса, а не гадила артефактами поверх графики винды.

    Тогда я стал догадываться, что в процессоре есть какое-то колдунство для перехвата прямого обращения к памяти (про защищённый режим я тогда слышал только название)


  1. quakin
    31.01.2023 07:11
    +3

    Можно пойти ещё дальше и набирать COM-файлы под DOS-ом - без использования компилятора - прямо с нуля в редакторе HIEW (он синхронно отображает HEX-коды и ASM-код для них).
    Не знаю кто будет так извращаться в наше время, но вдруг захочется получить ачивку "написать программу в машинных кодах без использования компилятора" ¯\_(ツ)_/¯


    1. SuperTEHb
      31.01.2023 12:22
      +2

      Добавлял так в прошивку микроконтроллера незнакомой доселе архитектуры одну функцию. Действительно, довольно увлекательное извращение.


    1. Astroscope
      31.01.2023 19:46
      +1

      Когда-то писал в HEX editor сразу в машинных кодах. Делал это из академического интереса - трудоемко и неудобно в сравнении с человекочитаемым ассемблером, но в целом успешно.


    1. amberovsky
      02.02.2023 14:11
      +1

      Мы так в ГУАПе писали лабы в эмуляторе pdp-11. Сначала пишешь на асме, потом вручную по табличке переводишь в хекс коды, потом вручную забиваешь байт за байтом.


  1. pfemidi
    31.01.2023 08:55
    +10

    А зачем нужно

    fill:
        stosb ; ES:DI = fill_color value (al)
        loop fill


    Разве простой 'rep stosb' не спасёт Отца Руской Демократии?


    1. AtariSMN82 Автор
      31.01.2023 20:22

      точно точно, просто stosb и loop я выучил первее чем rep. Добавил в статью упоминание об этом


  1. ash_lm
    31.01.2023 09:04
    +4

    Насчёт демосцены. В своё время меня очень впечатлил Kkrieger. Это, конечно, не 32 байта, но очень впечатляет. Как раз, в то время, перед этим я обновил железо и сам увидел этот шедевр, а то тогда мне это казалось каким-то колдунством.


    1. AtariSMN82 Автор
      31.01.2023 20:25
      +2

      Говорят что в Кригере, есть только две настоящие текстуры, а другие сгенерированы через синус функцию


  1. vadimk91
    31.01.2023 09:40
    +1

    В 90-х, когда я ещё был студентом, такие фокусы мог показать примерно каждый второй, кто подрабатывал на кафедре выч. техники. Не на эмуляторе доса на телефоне тех времен конечно, а на пк. Теперь это конечно ближе к магии.
    Кстати, некоторые демки напомнили программу life. А палитра цветов CGA/EGA... когда мне впервые удалось в программе вывести цвета, отличные от 16 стандартных - очень гордился :)


    1. PuerteMuerte
      31.01.2023 18:37

      В EGA же 64 стандартных цвета, если мне память не изменяет.


    1. AtariSMN82 Автор
      31.01.2023 20:45

      Вот Life от Rrrola в 31 байт


  1. DenisSDK
    31.01.2023 09:52

    Во времена DOS были популярны такие демки - и .COM, размера до 4Кб и .EXE до 64Кб.

    Одна из них - CD2.EXE, как раз около 64Кб


    1. CrashLogger
      31.01.2023 13:00

      Они и сейчас популярны, и фестивали регулярно проходят и конкурсы с призовыми местами есть) Не только под DOS, но и Spectrum, Commodore, Amiga и прочие ретроплатформы.


      1. AtariSMN82 Автор
        31.01.2023 20:28

        Теперь модно делать демки под виртуальную приставку TIC-80, демки там тоже по 32 байта


    1. AtariSMN82 Автор
      31.01.2023 21:49
      +3

      прикольный музен в демке


      1. DenisSDK
        01.02.2023 02:12

        Да, она самая


  1. serge-sb
    31.01.2023 17:02
    +1

    Про демки: Heaven7. Не понимаю только двух вещей: как они это уместили в 64к и как это работает в реальном времени.

    Про статью:

    ; заполнить нулём фрейм-буфер
    ; далее в цикле:
    xor al, al
    dec al
    out 3C6h, al
    xor al, al
    out 3C8h, al
    mov al, red_color
    out 3C9h, al
    mov al, green_color
    out 3C9h, al
    mov al, blue_color
    out 3C9h, al
    ; , где *_color - значения от 0 до 63


    1. orbion
      31.01.2023 21:55
      +2

      Что там 64 килобайта... Elevated занимает всего 4066 байт :)

      Из 64-килобайтных когда-то очень понравилась Fermi Paradox

      Видео


  1. nuclight
    31.01.2023 17:40
    +2

    Нельзя перемещать напрямую данные из одной ячейки оперативки в другую, нужно сначала загрузить ячейку из памяти в регистр, а потом в другую ячейку (кому-то было лень добавить ещё одну перегрузку для mov).

    Ну в самом деле, добавить удорожающий всю систему (а в 8088 и так пытались удешевить) контроллер, аналогичный DMA - это ж не экономика, а просто лень. Дальше читать не стал.


    1. AtariSMN82 Автор
      31.01.2023 20:30
      +1

      можно было бы сделать микрокод, который делал все манипуляции в виде одной инструкции


  1. beremour
    31.01.2023 17:43
    -1

    Году в 1994-1995 было актуально


  1. EXL
    31.01.2023 18:30
    +4

    Отличная статья, спасибо, прочитал с удовольствием! Как вижу с творчеством Řrřola вы уже знакомы.

    В своё время меня очень сильно впечатлила 32-байтная демка Dírojed от этого разработчика.

    $ echo 'B013CD10C51F380F10276BDBE58A0F0209028FBFFE02483F4BE460FEC875E7C3' \
    | xxd -r -p - dirojed.com
    $ dosbox dirojed.com
    
    <32-байтная магия!>
    

    Выглядит очень эффектно, а если оставить её на экране подольше, то можно узреть как зарождается жизнь :D


    1. litos
      31.01.2023 19:34
      +1

      Что-то подобное по графике было в скринсейвере программы DOS Navigator, но за 25 лет уже мог и забыть )


      1. tormozedison
        31.01.2023 21:35
        +2

        Огонь там был.


    1. AtariSMN82 Автор
      31.01.2023 20:32
      +1

      В статье как раз была гифка с другой прогой - dirojedc, я просто не знал кто автор и не подписал, но видимо тоже Rrrola


      1. AtariSMN82 Автор
        31.01.2023 20:39
        +1

        Жесть, там даже выйти из программы можно и на это хватило места


    1. AtariSMN82 Автор
      31.01.2023 20:38
      +1

      Даже не верится что такое уместилось в 32b


    1. MagisterAlexandr
      01.02.2023 11:05
      +1

      B013CD10C51F380F10276BDBE58A0F0209028FBFFE02483F4BE460FEC875E7C3

      Вот так демки распространять и надо, а не ссылками на vk. :-)


      1. AtariSMN82 Автор
        02.02.2023 02:20
        +1

        в qr-коде


      1. AtariSMN82 Автор
        02.02.2023 03:16
        +1

        новая версия мерцания - 18 байт
        B81300CD106800A007B900FAFEC0F3AAEBF7


  1. Watashiwa
    31.01.2023 20:33
    +1

    Интересные были времена когда пытались сделать что-то как можно сложнее но меньше. Больше всего поражало то как данные для программы превращались в код нужным прыжком на необходимый адрес.


    1. firehacker
      01.02.2023 00:12

      А сейчас вкладка вконтактика в браузере потребляет гигабайт...


      1. AtariSMN82 Автор
        01.02.2023 00:41

        веб-амёбам далеко до премудростей оптимизаций


        1. Watashiwa
          01.02.2023 19:27

          Кстати не совсем верно насчет "Всё описанное здесь имеет мало практической пользы в наше-то время" По сути программирование узкоспециализированных контроллеров происходит( или уже нет, но происходило) на asm подобном языке (только своем). Да и условно ардуино тоже можно слегка считать обобщенным порядком действия между низкоуровневым и тем же С


          1. AtariSMN82 Автор
            02.02.2023 02:12

            Ну на микрухах совсем другие приколы, особенно на восьмибитных - там даже не делают команды деления и умножения, приходится их мастерить из суммирований и битовых сдвигов, а чтобы был int32, надо слепить вместе 4 байта и каждый раз при работе с ними обрабатывать каждый байтик и чекать флаги статуса. А компилятор си ещё и умеет работу с дробными числами имитировать софтверно...


  1. R0bur
    31.01.2023 20:33
    +1

    В стандартную поставку MSDOS и Windows 9x, 2000 и XP входила программа debug.exe, с помощью которой можно как просматривать 16-битный код, так и компилировать Assembler в COM-файлы. Пример компиляции текста на ассемблере можно посмотреть в статье "МышеOFFка" (https://www.osp.ru/pcworld/2008/04/5095968).


  1. ef_end_y
    01.02.2023 00:02
    +1

    В 200х в определенных кругах было что-то типа соревнования на написание самого маленького резидентного вируса под dos. Там реально годами уменьшали по несколько байт. В начале соревнования было что-то типа 113 байт (точные цифры не помню), под конец 64. Я потратил несколько дней (!) для того чтобы добиться этих самых 64. Это была тяжелейшая головоломка. На всякий случай скажу, что эти вирусы никакой опасности не представляют ибо время доса ушло, это чисто спорт, вирус был фактически на бумаге. К сожалению все исходники этих микро вирусов Гугл потер(


    1. MagisterAlexandr
      01.02.2023 11:07

      64 байта не смогли сохранить?


      1. ef_end_y
        01.02.2023 18:05

        Свой вариант сохранил. Мне были интересны других. В частности, по-моему название было viking. Я не думал, что интернет можно "потереть". Остались только упоминания без исходных кодов


        1. MagisterAlexandr
          02.02.2023 12:27

          Поделитесь хотя бы своим, здесь сохранится.

          Я не думал, что интернет можно "потереть". Остались только упоминания без исходных кодов

          Есть ссылки? Может, на каком-нибудь веб-архиве найдётся?


          1. ef_end_y
            02.02.2023 16:23
            +1

            Поискал я, максимум что нашел, так это название: Virus.DOS.Small.59.a или Viking.59, что говорит о том, что он 59 байт, значит мой был меньше, наверное 58. Мой валяется где-то в бекапах, искать не буду, потому что и выкладывать не буду - хоть абсолютно понятно, что никакой опасности он не представляет, но часто люди не думают, забанят и все. Могу просто на словах описать основные моменты, почему удалось написать таким маленьким:

            1. долгое время все эти микрики дописывали себя в конец СОМ-файла, а в начало ставили только JMP - так было легко восстанавливать файл уже загруженный в память. До того пока кто-то не додумался "конвейер"! После чего стали писать в начало файла и восстанавливать код в памяти по-живому, т.е записывать прямо поверх выполняющегося кода - это не вредило, потому что он был в конвеере. Хотя прерывания никто не запрещал, так что мог быть сбой, но экономия байтов)

            2. при старте DOS предустанавливает регистры во всем известные значения, это экономило несколько байт

            3. резидентный кол писался прямо в таблицу векторов прерываний, во 2-ю половину, которая обычно не использовалась, но я помню я далекие 90-е у меня EGA использовал какой-то вектор. Так что в моем случае был бы крах

            4. многие константы были взаимосвязанными, например, размер вируса, адрес куда записываеттся резидентая часть, возможно сегмент - деталей уже не помню, помню что играясь с этими параметрами можно было выгадать несколько байт

            5. точно помню что было обсуждение, что ХХХ из всемирно известных разрабов антивирусов "ввязался" в эту гонку и сделал ради спортивного интереса еще на байт меньше. Код никто не видел, а менеджер ХХХ, когда я об этом спросил его через несколько лет, отвечал: нет! нет! нет! такого не могло быть ни в коем случае

            6. как уменьшить на один байт у меня была идея, но я не реализовал ее, поскольку пришла в голову на следующий день как я забил на это и не стал возвращаться. Суть была в том, что первый запуск подготавливает резидента, потом выходит в ДОС, второй запуск запускает все и код-носитель выполняется. Выглядело б это формально как глюк первого запуска. Хз, может ХХХ так и сделал


            1. MagisterAlexandr
              02.02.2023 22:15

              Недавно вышла статья, где предлагается метод сжатия изображений путём составления текстового описания. Получилось 600 байт из 474КБ.

              В нашем случае наоборот, текстовое описание 4220 байт (или 3761 байт, если в UTF-8) кодирует демку ~58 байт. Интересно, когда удастся её восстановить (может быть, это сделает ИИ), это будет та же самая демка или чуть другая...


  1. AtariSMN82 Автор
    01.02.2023 00:56

    Похоже что статья вышла интересной и не перевелись ещё деды на хабре. Может быть потом сделаю другую статью по отдельной демке с понятными пояснениями для ассемблера


  1. PapaKarlo787
    01.02.2023 22:00
    +1

    Ну конечно прикольно, но что-то квело. Спасибо конечно за ссылки на авторов, но вот интереснее было бы посмотреть разбор гуру. Хотя статья может и не задумывалась, как погружение в микро демки, скорее как краткий экскурс. Но даже так, многие недочёты просто убили.
    "указывает компилятору что код программы должен грузиться в оперативную память с отступом в 256 байт" - а вот ему не пофиг, вроде он это решает. Эта директива нужна чтобы сместить абсолютные адреса, чтоб при исполнении не хапнуть горя. И ниже тоже про затирание этих несчастных байтиков из префикса доса - ты же не сам дос сотрёшь, а только все его плюшки, вернуться то всегда сможете 20 инт использовать. Но возможно тут речь шла более глобально о всей памяти и там чего не постирать. Код итоговый тоже не ахти, вроде сказали про луп, но вот ещё Inc Al и сохранение сегмента в es вызывают большие вопросы, если память не изменяет, на этом можно сэкономить 2 байта