Осторожно: в этой статье сотни эмодзи. Если вы пользуетесь экранным диктором, проследите, чтобы он не зачитал их все вслух.
Целая куча эмодзи. Что бы они могли значить?
Этот пост взялся из ниоткуда. Я возился со своим веб-приложением, онлайн-дизассемблером для x86, и заметил, что эмодзи кодируются прямо в URL.
Вставил эмодзи козла 🐐 и увидел кодировку %F0%9F%90%90.
Если вы хоть немного знакомы с ассемблером x86, шестнадцатеричное 90h вам, скорее всего, встречалось. Это опкод пустой операции, NOP.
Я коротко хихикнул над мыслью, что козлы среди эмодзи играют роль NOP, а потом мне стало любопытно. F0h на 8088 это префикс LOCK. Обычно он нужен, чтобы согласовать монопольный доступ к шине с сопроцессором вроде 8087, а для большинства инструкций на 8088 ничего не делает и игнорируется (на более поздних процессорах Intel это изменится). Остаётся 9Fh. 9Fh это LAHF. Весь эмодзи козла оказывается корректным машинным кодом 8088, и читается эта последовательность так:
lock lahfnopnop
Как выяснилось, подавляющее большинство графем эмодзи, как они называются, начинается с последовательности F09F. Над моей головой замерцала тусклая лампочка.
Можно ли написать программу для 8088, используя только отображаемые эмодзи?
У идеи есть прецеденты. Давно известно, что исполняемые файлы можно собирать только из печатных символов ASCII. Самый знаменитый пример, пожалуй, тестовый файл EICAR: ASCII-строка, которая одновременно служит рабочим исполняемым файлом DOS, печатает EICAR-STANDARD-ANTIVIRUS-TEST-FILE! и завершается. Другие маленькие ASCII-программы печатали в журналах или распространяли иными способами. Такова крошечная терминальная утилита TCOM, весь исходник которой приведён ниже:
XPHPD[0GG0G,0G51G31GB'(G+(G:u'0g?(G>(GE1G@arwIV_F*=US@>1|_,5wXNg-7muTu(41m0ss1k260s@3G1g360@3G0i7t2g3A1g350@3G2E1=0C1g350@3T2M0^\1g3>0@3T=1s2g0T1g3;0@3ToN2g391g0t@3G0^F1k0s2?0@3T4
Любопытное решение для «аварийного терминала»: если других способов загрузить исполняемый файл на компьютер нет, его можно просто ввести с клавиатуры.
Меня удивляет, что идею напрямую исполнять эмодзи, судя по всему, никто ещё не исследовал.
Hello (World)!
Разумеется, первым делом стоит попробовать Hello World! на эмодзи. Ради экономии места и отчасти из-за мучений с любой арифметикой на эмодзи мы выведем только строку HELLO.
Вот программа целиком:
🐸☺️🐰🐎♐🗃️🧯🧯🧯🐮💗🦮♐🐰🐹🗃️🧯🧯🧯🧯💗🪗🧯😗🧮😗🧮😗🐮😪😔⭐
Вставьте её в текстовый редактор и сохраните в UTF-8 без BOM с расширением .COM. Должен получиться файл размером 141 байт с суммой MD5 0a5c91475ca2de33e36aacc2f0b7b840.
Дизассемблированный листинг всей программы можно посмотреть здесь. Если эмодзи плохо копируются из текста, возьмите их по этой ссылке.
Некоторые эмодзи могут отображаться как «тофу», то есть пустые прямоугольники. Зависит от браузера и от того, в каком году вы это читаете.
-
это лопата, она появилась в Unicode 16.0 в 2024 году. До обновлений шрифтов такие глифы доползают не сразу. -
🪗это аккордеон, его добавили в Unicode 13.0 в 2020 году, но в Chrome на Windows 10 он почему-то до сих пор не виден. Поди разбери. -
это Distorted Face («Искажённое лицо»), совсем новый символ из Unicode 17.0, утверждённого в 2025 году.
Теоретически символ-тофу можно скопировать с сохранением байтов, но некоторые операционные системы и программы с самой идеей сохранения байтов справляются плохо.
Программа опирается на несколько недокументированных псевдонимов инструкций 8088, поэтому ей нужно достаточно точное ядро 8088. Посмотрим, что она делает в DOSBox-X с настройкой cpu cputype=8086:
Программа «Hello (World!)» выполняется в DOSBox-X
Обратите внимание, что программа начинается с 🐸☺️. Эта последовательность делает важную подготовку и объясняет, откуда берётся указатель на видеопамять. Эти эмодзи дают последовательность байтов F09F90B8E298BAEFB88F.
00000000 F0 9F lahf00000002 90 nop00000003 B8 E2 98 mov ax,98E2h00000006 BA EF B8 mov dx,B8EFh00000009 8F db 0x8F
B8EFh всё ещё лежит внутри базового сегмента текстового режима B800, примерно в 3824 байтах от начала экрана. Поэтому текст и появляется в правом нижнем углу. Выбирать не приходится, так что сделаем вид, будто расположение текста было задумано.
Эмодзи 💗, он же F09F9297, переносит наш видеосегмент в DI.
0000012C F0 9F lahf0000012E 92 xchg dx,ax0000012F 97 xchg di,ax
Буквы синтезируются начиная с H. Здесь очень выручает 🗃️, он же F09F9783EFB88F:
00000146 83 EF B8 sub di,FFB8h
Вычесть FFB8h то же самое, что прибавить 48h. А какой ASCII-код у H? 48h. Изящно.
Остальные буквы приходится неуклюже собирать по одной. L, понятное дело, можно повторить. Вы заметите, что одна из L зелёная. Причина в том, что одна из инструкций LAHF перезаписывает AH. Так уж вышло, что содержимое регистра флагов даёт видимый байт атрибута символа, в данном случае зелёный. Атрибут можно было бы сбросить ценой ещё нескольких байтов, но мне, пожалуй, нравится это несоответствие. Маленький намёк на проклятые дела, которые творятся за кулисами.
Справочник по эмодзи-ассемблеру
Если вам хватит смелости поэкспериментировать с кодом на эмодзи самостоятельно, вот справочные материалы.
Самостоятельные эмодзи
Есть несколько самостоятельных эмодзи, которые дают полезные инструкции или пары инструкций.
|
Эмодзи |
Байты UTF-8 |
Интерпретация на 8088 |
Полезный эффект и типичное применение |
|---|---|---|---|
|
|
|
|
Четырёхбайтовое заполнение. Ещё полезен внутри тела цикла, когда нужно точное смещение перехода. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Копирует один байт из |
|
|
|
|
Загружает один байт из |
|
|
|
|
Записывает |
|
|
|
|
Переносит знаково расширенный |
|
|
|
|
Передаёт знаково расширенный байт между |
|
|
|
|
Прокручивает значения через |
|
|
|
|
Прибавляет |
Эмодзи с открытым хвостом
Самое интересное начинается, когда мы сцепляем эмодзи, оставляющие «открытый хвост», то есть незавершённый байт инструкции. Следующие эмодзи оставляют висящий байт, и он бывает очень полезен в зависимости от того, какой эмодзи идёт дальше:
|
Эмодзи |
Байты UTF-8 |
Открытый хвост |
Типичное применение |
|---|---|---|---|
|
|
|
|
Поглощает начало |
|
|
|
Первые два байта могут быть непосредственным значением; завершающему |
После |
|
|
|
|
Поглощает следующий |
|
|
|
|
Поглощает заголовок |
|
|
|
|
Вместе с |
|
|
|
|
Поглощает заголовок |
|
|
|
|
Сначала прибавляет |
|
|
|
Начинается и заканчивается байтами, предназначенными для соседей |
После того как |
|
|
|
Завершающие |
Сохраняет указатель вывода в |
|
|
|
Первые два байта это |
Трёхбайтовый хвост цикла, когда тело цикла размещено точно на нужном смещении. |
Полезные гаджеты
Это законченные полезные последовательности из нескольких эмодзи.
BunnySad: 🐰😔, починка стека
Эта восьмибайтовая последовательность намеренно заменяет текущий указатель стека на FFF0h:
0000: F0 9F lahf0002: 90 nop0003: B0 F0 mov al,F0h0005: 9F lahf0006: 98 cbw0007: 94 xchg sp,ax
Последний байт кролика B0h поглощает ведущий F0h грустного лица и загружает AL=F0h. Второй LAHF идёт перед CBW, который превращает значение в AX=FFF0h, а XCHG делает его указателем стека.
Так можно восстановиться после намеренных POP и удержать асинхронные записи прерываний в стек у вершины сегмента. Текущий стек при этом отбрасывается, так что приём безопасен, только если в стеке нет нужного адреса возврата или сохранённого значения. Старый SP остаётся в AX.
BunnySnowParty: 🐰☃️🐰😔, безопасное сложение DI += 48h
Эта 18-байтовая последовательность увеличивает DI на 48h (72 в десятичной системе), поглощает висящий POP снеговика и чинит стек:
0000: F0 9F lahf0002: 90 nop0003: B0 E2 mov al,E2h0005: 98 cbw0006: 83 EF B8 sub di,FFB8h0009: 8F F0 pop ax000B: 9F lahf000C: 90 nop000D: B0 F0 mov al,F0h000F: 9F lahf0010: 98 cbw0011: 94 xchg sp,ax
Первый кролик поглощает E2h снеговика. B8h это знаковый непосредственный операнд -72, поэтому SUB DI,-72 прибавляет 72. Завершающий 8Fh снеговика поглощает F0h следующего кролика и даёт недокументированную кодировку POP AX в виде 8F F0. Последняя пара из кролика и грустного лица восстанавливает SP=FFF0h.
Это один из первых найденных гаджетов. Гаджеты с глазом и тарелкой обычно эффективнее, но этот приведён для полноты.
PlateGoat: 🍽️🐐, увеличение SP и сдвиг DI
Тарелке нужен следующий байт, чтобы завершить её последний 8Fh. Удобный безвредный хвост даёт козёл-заполнитель.
0000: F0 9F lahf0002: 8D BD EF B8 lea di,[di-4711h]0006: 8F F0 pop ax0008: 9F lahf0009: 90 nop000A: 90 nop
B8EFh это 16-битное представление -4711h, поэтому LEA выполняет DI += B8EFh. 8Fh тарелки поглощает ведущий F0h козла как недокументированный POP AX, а оставшиеся 9F 90 90 это LAHF; NOP; NOP.
Тарелки сдвигают SP на два и заодно перемещают DI на значительное расстояние. Для арифметики с указателями они превосходны, но их влияние на стек нужно учитывать.
CatGoat: 😿🐐, абсолютный сброс DI
Плачущий кот открывает MOV DI,imm16. Козёл-заполнитель поставляет непосредственный операнд.
0000: F0 9F lahf0002: 98 cbw0003: BF F0 9F mov di,9FF0h0006: 90 nop0007: 90 nop
Этот восьмибайтовый гаджет сбрасывает DI в 9FF0h независимо от прежнего значения. Если заменить козла другим эмодзи, его ведущие F0 9F всё равно станут операндом, но оставшиеся байты выполнятся как хвост и могут дополнительно изменить DI.
CamelWrite: 🗓️ + арифметика + 📗🐪, генерация и запись байта
Обобщённая форма занимает 15 фиксированных байтов плюс выбранную арифметическую последовательность. Вот простейший конкретный пример, 🗓️🐮📗🐪, где корова прибавляет единицу:
0000: F0 9F lahf0002: 97 xchg di,ax0003: 93 xchg bx,ax0004: EF out dx,ax ; spurious OUT to B8EFh0005: B8 8F F0 mov ax,F08Fh0008: 9F lahf0009: 90 nop000A: AE scasb000B: F0 9F lahf000D: 93 xchg bx,ax000E: 97 xchg di,ax000F: F0 9F lahf0011: 90 nop0012: AA stosb
На входе DI содержит настоящий указатель вывода, а AX содержит арифметическую затравку. 🗓️ переносит затравку в DI и сохраняет указатель вывода в BX.
Средняя последовательность эмодзи выполняет арифметику над этим временным DI. Корова в примере выше прибавляет единицу.
📗 восстанавливает указатель вывода и переносит вычисленный младший байт в AL, а 🐪 его записывает.
Календарь также выполняет OUT DX,AX. Стандартный запуск задаёт DX=B8EFh, порт, на который обычно ничего не отображено. На настоящем железе используйте с осторожностью!
FrogStrap: 🐸☺️😖🐰😗😯😯😯😯😯😯😯, общая инициализация регистров
Эта 50-байтовая последовательность получает гарантированный ноль из обычного стека при входе в COM-файл и задаёт определённое состояние регистров:
0000: F0 9F lahf0002: 90 nop0003: B8 E2 98 mov ax,98E2h0006: BA EF B8 mov dx,B8EFh0009: 8F F0 pop ax000B: 9F lahf000C: 98 cbw000D: 96 xchg si,ax000E: F0 9F lahf0010: 90 nop0011: B0 F0 mov al,F0h0013: 9F lahf0014: 98 cbw0015: 97 xchg di,ax0016: F0 9F lahf0018: 98 cbw0019: AF scasw001A: F0 9F lahf001C: 98 cbw001D: AF scasw001E: F0 9F lahf0020: 98 cbw0021: AF scasw0022: F0 9F lahf0024: 98 cbw0025: AF scasw0026: F0 9F lahf0028: 98 cbw0029: AF scasw002A: F0 9F lahf002C: 98 cbw002D: AF scasw002E: F0 9F lahf0030: 98 cbw0031: AF scasw
🐸☺️ загружает DX=B8EFh и оставляет висящий 8Fh. Первый байт 😖 завершает POP AX, получая гарантированный ноль по адресу SS:FFFEh и заворачивая SP в 0000h. Остальная часть 😖 переносит этот ноль в SI.
🐰 поглощает ведущий F0h из 😗, давая AL=F0h, после чего CBW; XCHG DI,AX устанавливает DI=FFF0h. Семь инструкций SCASW доводят его до FFFEh.
Итоговое состояние: DX=B8EFh, SI=0000h, DI=FFFEh и SP=0000h.
Значение B8EFh в DX, если использовать его как сегмент, попадает в нижнюю часть видеопамяти CGA.
Ещё один 😯 может прокрутить DI до 0000h. У значения FFFEh в DI есть вероятное применение в качестве константы -2.
🐸☺️🐰🐎♐, загрузка сегмента CGA
Эта 21-байтовая последовательность использует недокументированный псевдоним сегментного регистра исходного 8088 и задаёт ES=B8EFh:
0000: F0 9F lahf0002: 90 nop0003: B8 E2 98 mov ax,98E2h0006: BA EF B8 mov dx,B8EFh0009: 8F F0 pop ax000B: 9F lahf000C: 90 nop000D: B0 F0 mov al,F0h000F: 9F lahf0010: 90 nop0011: 8E E2 mov es,dx0013: 99 cwd0014: 90 nop
Простой загрузчик
Честно говоря, писать прямо на эмодзи-ассемблере это мучительная каторга: ни нормальных непосредственных значений, ни арифметических операций, ни вменяемых смещений переходов. Поэтому разумно сделать загрузчик, который просто соберёт код 8088 в памяти и перейдёт на него.
Вся хитрость в том, как закодировать в эмодзи произвольные байты. Графем эмодзи меньше 4000, так что на 16-битную таблицу поиска они никак не тянут. Более того, в наборе четырёхбайтовых графем на третьей и четвёртой позициях встречаются далеко не все 256 восьмибитных значений. Однако по чистому везению 8-битную таблицу поиска можно построить из двухбайтовых хвостов набора из 256 уникальных эмодзи.
Основная идея такая:
bits 16org 0 push di pop si ; SI = encoded emoji data mov di,0100h push di ; RET target after reconstruction mov cx,RAW_SIZE.decode: lodsw ; discard the fixed F0 9F prefix lodsw ; AL=third UTF-8 byte, AH=fourth aad 8Fh ; AL=(AL + 8Fh*AH) & FFh; AH=0 stosb loop .decode ret ; execute reconstructed COM at 0100h
Первый lodsw читает двухбайтовый префикс каждой «графемы данных» и отбрасывает его. Второй lodsw читает 16-битный хвост, который преобразуется в 8-битное значение с помощью магической константы 8Fh. Ключевая деталь здесь это особенность инструкции aad на 8088: она позволяет передать недесятичное основание непосредственным операндом.
Разумеется, всё это не сработает, если lahf постоянно затирает AH, поэтому сам 17-байтовый декодер сначала нужно собрать в памяти. Это можно сделать описанным выше гаджетом CamelWrite.
Минус здесь в том, что эффективность закодированных данных всего 25%, но любой другой подход заметно усложнил бы декодер и, следовательно, код, который его выводит.
Демо для VGA на эмодзи
Можно ли сделать простенькое демо для VGA из одних эмодзи? С нашим эмодзи-декодером довольно просто упаковать простое демо для VGA, декодировать его в память и перейти на него.
Вот вся программа:
🐸☺️😖🐰😗😯😯😯😯😯😯😯😹😯🕳️😯🐮🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐🐐⭐🍽️🍽️🍽️🍽️🐮🐰😔😬😬🗓️🍽️👁️📗🐪🗓️🧮🧯📗🐪🐮🗓️😿🧯🧯🧯🧯📗🐪🗓️🐮📗🐪🗓️🧮🧮🧯🧯👁️📗🐪🐮🗓️🧮🧯🧯🧯🧯🧯🧯📗🐪🗓️😿🧯🧯🧯🧯📗🐪🗓️😿🍽️🍽️🍽️🍽️🐮📗🐪🐮🗓️😯🍽️🍽️👁️📗🐪🐮🐮🐮🗓️😿🧮🧮🧮📗🐪🗓️🧮👁️👁️🍽️👁️📗🐪🐰😔🐐🐐♿🇹🇭⏪✋💕🐀🍥🐐📣🈚🌌🌋🏐🌕👻🌺🌻🍵🆙🌮🍝🌐🌋🍠🌋🐅🍜💒🌳👹🎇👹🉑🎎🉑💞🆖🌳🆎🎇👹🉑🎎🉑💞🐖👯🆖🌻🕕🌿🐐🍙👺👽🌳👹🎇💗🉑🎎🉑💞🖕🎎🌙👻🌺🌻🐻🎂🌻🌳👮🐖🐸🍤🕕🌻🐠🐻🍝🌻🌳🌷🐎🌷🍭🌌💗👻🐖🐖🌜🌛📛🍕🍓🍪🍷🌙🍘🍾🍪👻🌺🌻🌙🌘🌙🐗🌊🏐💗🎐🧯🔥🌉
Если сохранить её как COM-файл, размер должен быть 1092 байта, а сумма MD5 9ed65cc927b257b113a298dee37215cc. Полный дизассемблированный листинг можно посмотреть здесь. Если эмодзи плохо копируются из текста, возьмите их по этой ссылке.
Прим. пер.: на странице автора в этой строке есть десять лишних символов U+FE0F, и скопированный оттуда текст занимает 1122 байта. Мы взяли байты из файла в репозитории автора: 1092 байта, совпадают с дизассемблером по ссылке выше. Их MD5 равен 95482be7ae8d8ddcefca5a1ebbc7363f, это же значение указано в README репозитория. Сумма из поста с ними не сходится.
Эта программа запустится в любой версии DOSBox.
Не хочу портить вам сюрприз, если вы предпочтёте запустить её сами. А если готовы поверить на слово и посмотреть результат, откройте спойлер ниже.
Спойлер: нажмите, чтобы увидеть результат
Думаю, концепцию я доказал к собственному удовлетворению. Осталось разве что сделать более эффективный упаковщик и декодер, возможно, с компрессией LZ4. Утилита COM2EMOJI получилась бы довольно простой, а может, и EXE2EMOJI.
Можно ли собрать эмодзи-BIOS? Целую операционную систему на эмодзи? Будет ли у нас EmojiDOOM?
Другой исполняемый Unicode
Эмодзи не единственные графемы Unicode, которыми можно представить машинный код 8088. Следующие японские каны печатают в DOS «Hello World!».
ヹほむゐダぐビヘビヿぐツめぁツめしツめみギツめぁツめがツめみギツめぴツめみギツめぁツめけツめみギツめうギツめぁツめぼギツめぁツめぽギツめかツめみギツめぐギツめてツめみギツめぁツめぢツめみギツめぁツめぜツめみギツめぉギツめぁツめぬツめみギツめぐギツめふツめみギツめぁツめぬツめみギツめぁツめぢツめみギツめぉギツめぁツめちツめみギツめけギツめぁツめどギツめぇギツめごババババババババババババババババババババババ
Сохранённый как COM-файл, этот текст должен занимать 609 байт с суммой MD5 8df97d77cf17a9ad64b22d554eaebd37. Если не получается вставить символы, скопируйте их из дизассемблера здесь.
Следующий корейский хангыль тоже печатает «Hello World».
컓쿃뺰콙꾆퐀가긨숑낀가굀꿀꿨숑삺가꿀꿨숑킨숐뀺가꼀꿨숐쀺가가가가꺨숐퀀가가가깨숓넺가꼀꿨숓섺가김꿨숓턺가꿀꿨숒낺가가가가귀꿨숒삺가가가가깨숒킀가궨숑냀꺨숑샺가가꼀꿀꿨숑탺가곀꿀꿨숐끀걀꿀꿀꿀꿨숐쁺가가꼀꿀꿨숐큺가검꿀꿨숓녀깨숓셀가김꿀꿀꿨숓텀곀꿨숒냀글꿀꿀꿨숒샀귀꿀꿀꿨숒탺가글꿀꿨숑넀굀꿨숑섀관꿨숑턀가꿀꿀꿀꿨숐낀가귨숐삀괨숐킺가꿀꿨숓놀껨숓솀곀꿀꿀꿀꿨숓톀가가깨숒넀글꿀꿀꿨숒섀가글꿀꿨숒턺가글꿀꿨숑듀굀꿨숑쓀관꿨숑퓀가꿀꿀꿀꿨숐둺가가글꿨숐쑀궨숐푺가가검꿀꿨숓땻뀃밃밃뺃뿃뿃뿀가검꿨숗뀀가꺨숗쀀가글꿨숗퀐뀀
Сохранённый как COM-файл, этот текст должен занимать 768 байт с суммой MD5 b3b0a19cde830932f0b0d09ff8fd3d88. Дизассемблированный листинг здесь.
Новые примеры исполняемого Unicode я буду собирать в этом репозитории на GitHub.
ссылка на оригинал статьи https://habr.com/ru/articles/1104704/