This particular post comes out of left field a bit. I was playing around with a web application I had made - an online disassembler for the x86 - when I noticed that emoji were being encoded into the url.
I pasted a goat emoji, 🐐 and I noticed the encoding %F0%9F%90%90.
Now, if you're familiar with x86 assembly language at all, hexadecimal 90h is probably familiar to you. It's the opcode for a null operation or NOP.
I had a brief nerd chuckle over the thought that goats were the NOPs of emoji, but then I got curious. F0h on the 8088 is the LOCK prefix. This prefix is generally used to coordinate exclusive bus access with a coprocessor such as the 8087, but otherwise does nothing for most instructions on the 8088 and is ignored (this would change on later Intel CPUs). That leaves us with 9Fh.
9Fh is LAHF.
The entire goat emoji is valid 8088 machine code, a sequence that reads
lock lahf
nop
nop
As it turns out, the vast majority of emoji graphemes, as they are called, start with the sequence F09F. A dim little light bulb started to flicker above my head. Could you actually write an 8088 program using nothing but displayable emoji?
The idea is not without precedent. It has been well-established that executables can be generated with only printable ASCII characters - the most famous example probably being the EICAR test file, an ASCII string that is also a valid DOS executable that prints "EICAR-STANDARD-ANTIVIRUS-TEST-FILE!" and exits.
Other small ASCII programs were printed in magazines or distributed in other ways, such as the tiny terminal utility TCOM, the entire source of which is reproduced below:
XPHPD[0GG0G,0G51G31GB'(G+(G:u'0g?(G>(GE1G@arwIV_F*=US@>1|_,5wXNg-7muTu(4
1m0ss1k260s@3G1g360@3G0i7t2g3A1g350@3G2E1=0C1g350@3T2M0^\1g3>0@3T=1s2g0T
1g3;0@3ToN2g391g0t@3G0^F1k0s2?0@3T4This is an interesting "emergency terminal" solution: if someone had no other means of loading an executable onto a computer system, it could simply be entered in via the keyboard.
It surprises me that the idea of directly executing emoji has apparently never been explored.
Hello (World)!
Of course, the first thing to do is attempt Hello World! in emoji. For space reasons and partly due to the pain of doing any sort of arithmetic in emoji, we will only print the string HELLO.
Here is the full program:
🐸☺️🐰🐎♐🗃️🧯🧯🧯🐮💗🦮♐🐰🐹🗃️🧯🧯🧯🧯💗🪗🧯😗🧮😗🧮😗🐮😪😔⭐
Pasted into a text editor and saved as UTF-8, no BOM, with a .COM file extension, the result should be 141 bytes with an MD5 sum of 0a5c91475ca2de33e36aacc2f0b7b840.
The disassembly of the entire program can be viewed here.
Several emoji here may display as tofu, depending on your browser and what year you are reading this article.
is the shovel emoji, introduced in Unicode 16.0 in 2024. These glyphs still take time to trickle down into font updates.
🪗 is the accordion emoji, added to Unicode 13.0 in 2020, but somehow still not visible in Chrome on Windows 10. Go figure.
is the "Distorted Face" emoji and is brand new in Unicode 17.0, approved in 2025.
In theory, it should be possible to copy the relevant tofu character and preserve the representational bytes, but some operating systems and programs seem to struggle with the byte-preserving concept.
This program relies on a few undocumented 8088 aliases, and so requires a fairly accurate 8088 core to execute successfully. Let's see what it does in DOSBox-X with cpu cputype=8086:
| The "Hello (World!)" program executing in DosBox-X |
Note the program starts with 🐸☺️. This sequence does some important setup and explains how we get a pointer to video memory. These emoji represent the byte sequence
F09F90B8E298BAEFB88F. 00000000 F0 9F lahf
00000002 90 nop
00000003 B8 E2 98 mov ax,98E2h
00000006 BA EF B8 mov dx,B8EFh
00000009 8F db 0x8F
F09F9297, moves our video segment into DI. 0000012C F0 9F lahf
0000012E 92 xchg dx,ax
0000012F 97 xchg di,ax
F09F9783EFB88F, comes in clutch here:00000146 83 EF B8 sub di,FFB8h
Subtraction by FFB8h is equivalent to addition by 48h. What's H's ASCII hex code? 48h. Neat. Emojissembly Reference
Standalone Emoji
| Emoji | UTF-8 bytes | 8088 interpretation | Useful effect and typical use |
|---|---|---|---|
| 🐐 | F0 9F 90 90 |
LAHF; NOP; NOP |
Four-byte padding. Also useful inside a loop body when an exact branch displacement is needed. |
| 🐮 | F0 9F 90 AE |
LAHF; NOP; SCASB |
DI += 1. A small, clean pointer increment. |
| 😯 | F0 9F 98 AF |
LAHF; CBW; SCASW |
DI += 2. Denser than two 🐮; AX becomes the sign extension of AL. |
| 🧮 | F0 9F A7 AE |
LAHF; CMPSW; SCASB |
SI += 2, DI += 3. Useful when SI may also advance. Reads DS:SI and ES:DI and changes flags. |
| 🧯 | F0 9F A7 AF |
LAHF; CMPSW; SCASW |
SI += 2, DI += 4. A compact four-byte advance. Also useful in the event of 🔥. |
| 🤐 | F0 9F A4 90 |
LAHF; MOVSB; NOP |
Copies one byte from DS:SI to ES:DI, then increments both pointers. |
| 😬 | F0 9F 98 AC |
LAHF; CBW; LODSB |
Loads one byte from DS:SI into AL, then increments SI. |
| 🐪 | F0 9F 90 AA |
LAHF; NOP; STOSB |
Writes AL to ES:DI, then increments DI. |
| 😖 | F0 9F 98 96 |
LAHF; CBW; XCHG AX,SI |
Moves the sign-extended AL into SI while saving the old SI in AX. Useful after obtaining a known zero. |
| 😗 | F0 9F 98 97 |
LAHF; CBW; XCHG AX,DI |
Transfers a sign-extended byte between AX and DI. Useful for turning AL=F0h into DI=FFF0h. |
| 📗 | F0 9F 93 97 |
LAHF; XCHG AX,BX; XCHG AX,DI |
Rotates values through AX, BX, and DI. The byte writer uses it to restore a saved output pointer and recover the synthesized byte in AL. |
| 👁️ | F0 9F 91 81 EF B8 8F |
LAHF; XCHG AX,CX; SUB DI,8FB8h |
Adds 7048h to DI. Excellent for large modular pointer movements; clobbers AX and CX. |
Open-Tail Emoji
| Emoji | UTF-8 bytes | Open tail | Common use |
|---|---|---|---|
| 🐸 | F0 9F 90 B8 |
MOV AX,imm16 needs two bytes |
Absorbs the beginning of ☺️ to create MOV AX,98E2h. |
| ☺️ | E2 98 BA EF B8 8F |
First two bytes can be an immediate; its final 8Fh needs a ModR/M |
After 🐸, supplies MOV AX,98E2h; MOV DX,B8EFh and opens a POP. |
| 🐰 | F0 9F 90 B0 |
MOV AL,imm8 needs one byte |
Consumes a following F0h, or consumes the E2h at the start of ☃️. |
| 🐹 | F0 9F 90 B9 |
MOV CX,imm16 needs two bytes |
Consumes a following F0 9F header to load CX=9FF0h. |
| 🐎 | F0 9F 90 8E |
MOV Sreg,r/m16 needs a ModR/M byte |
Followed by the E2h from ♐, gives the 8088-only alias MOV ES,DX. |
| 😿 | F0 9F 98 BF |
MOV DI,imm16 needs two bytes |
Consumes the next emoji's F0 9F header and resets DI=9FF0h. |
| 🍽️ | F0 9F 8D BD EF B8 8F |
POP r/m16 needs a ModR/M byte |
First adds B8EFh to DI; a following F0h completes undocumented POP AX, so SP += 2. |
| ☃️ | E2 98 83 EF B8 8F |
Begins and ends with bytes meant for neighbors | After 🐰 consumes its E2h, the middle performs SUB DI,-72; the trailing 8Fh needs a ModR/M. |
| 🗓️ | F0 9F 97 93 EF B8 8F |
Final B8 8F consumes the next F0h |
Saves the output pointer in BX while moving the byte accumulator into DI. It also executes OUT DX,AX. |
| ⭐ | E2 AD 90 |
Its first two bytes are LOOP -83 |
A three-byte loop tail when the loop body has been laid out at exactly the right displacement. |
Useful Gadgets
These are complete, useful sequences of multiple emoji.
BunnySad: 🐰😔 - Stack repair
This eight-byte sequence deliberately replaces the current stack pointer with FFF0h:
0000: F0 9F lahf
0002: 90 nop
0003: B0 F0 mov al,F0h
0005: 9F lahf
0006: 98 cbw
0007: 94 xchg sp,ax
The rabbit's final B0h consumes the sad face's leading F0h, loading AL=F0h. The second LAHF is followed by CBW, which turns that into AX=FFF0h; XCHG then installs it as the stack pointer.
This is how to recover after intentional POPs and keep asynchronous interrupt pushes near the top of the segment. It discards the current stack, so it is safe only when no return address or saved value is live. The old SP is left in AX.
BunnySnowParty 🐰☃️🐰😔 - Safe addition: DI += 48h
This 18-byte sequence advances DI by 48h (72 decimal), absorbs the snowman's dangling POP, and repairs the stack:
0000: F0 9F lahf
0002: 90 nop
0003: B0 E2 mov al,E2h
0005: 98 cbw
0006: 83 EF B8 sub di,FFB8h
0009: 8F F0 pop ax
000B: 9F lahf
000C: 90 nop
000D: B0 F0 mov al,F0h
000F: 9F lahf
0010: 98 cbw
0011: 94 xchg sp,ax
The first rabbit consumes the snowman's E2h; B8h is a signed -72 immediate, so SUB DI,-72 adds 72. The snowman's final 8Fh consumes the next rabbit's F0h as the undocumented POP AX encoding 8F F0. The final rabbit/sad-face pair restores SP=FFF0h.
This was one of the first gadgets found. The eye and plate gadgets are usually more efficient, but this one is included for completeness.
PlateGoat: 🍽️🐐 - Increment SP and advance DI
The plate needs a following byte to complete its final 8Fh. A padding goat is a convenient harmless tail.
0000: F0 9F lahf
0002: 8D BD EF B8 lea di,[di-4711h]
0006: 8F F0 pop ax
0008: 9F lahf
0009: 90 nop
000A: 90 nop
B8EFh is the 16-bit representation of -4711h, so the LEA performs DI += B8EFh. The plate's 8Fh consumes the goat's leading F0h as undocumented POP AX; the remaining 9F 90 90 is LAHF; NOP; NOP.
Plates advance SP by two as well as moving DI a significant distance. Plates are excellent for pointer arithmetic, but their stack effects must be accounted for.
CatGoat: 😿🐐 - Absolute DI reset
The crying cat opens MOV DI,imm16. A padding goat supplies the immediate.
0000: F0 9F lahf
0002: 98 cbw
0003: BF F0 9F mov di,9FF0h
0006: 90 nop
0007: 90 nop
This eight-byte gadget resets DI to 9FF0h without depending on its previous value. If another emoji replaces the goat, its leading F0 9F still becomes the immediate, but its remaining bytes execute as a tail and may further change DI.
CamelWrite: 🗓️ + arithmetic + 📗🐪 - Generate and write a byte
The generic form is 15 fixed bytes plus the chosen arithmetic sequence. Here is the simplest concrete example, 🗓️🐮📗🐪, using a cow to add one:
0000: F0 9F lahf
0002: 97 xchg di,ax
0003: 93 xchg bx,ax
0004: EF out dx,ax ; spurious OUT to B8EFh
0005: B8 8F F0 mov ax,F08Fh
0008: 9F lahf
0009: 90 nop
000A: AE scasb
000B: F0 9F lahf
000D: 93 xchg bx,ax
000E: 97 xchg di,ax
000F: F0 9F lahf
0011: 90 nop
0012: AA stosb
On entry, DI is the real output pointer and AX contains the arithmetic seed. 🗓️ moves the seed into DI and saves the output pointer in BX.
The middle emoji sequence performs arithmetic on this temporary DI; the cow above adds one.
📗 restores the output pointer and transfers the computed low byte into AL, and 🐪 writes it.
The calendar also performs OUT DX,AX. The common startup establishes DX=B8EFh, a generally unmapped port. Use with caution on real hardware!
FrogStrap: 🐸☺️😖🐰😗😯😯😯😯😯😯😯 - Common register bootstrap
This 50-byte sequence obtains a known zero from the normal COM entry stack and establishes a specific register state:
0000: F0 9F lahf
0002: 90 nop
0003: B8 E2 98 mov ax,98E2h
0006: BA EF B8 mov dx,B8EFh
0009: 8F F0 pop ax
000B: 9F lahf
000C: 98 cbw
000D: 96 xchg si,ax
000E: F0 9F lahf
0010: 90 nop
0011: B0 F0 mov al,F0h
0013: 9F lahf
0014: 98 cbw
0015: 97 xchg di,ax
0016: F0 9F lahf
0018: 98 cbw
0019: AF scasw
001A: F0 9F lahf
001C: 98 cbw
001D: AF scasw
001E: F0 9F lahf
0020: 98 cbw
0021: AF scasw
0022: F0 9F lahf
0024: 98 cbw
0025: AF scasw
0026: F0 9F lahf
0028: 98 cbw
0029: AF scasw
002A: F0 9F lahf
002C: 98 cbw
002D: AF scasw
002E: F0 9F lahf
0030: 98 cbw
0031: AF scasw
🐸☺️ loads DX=B8EFh and leaves a dangling 8Fh. The first byte of 😖 completes POP AX, obtaining the known zero at SS:FFFEh and wrapping SP to 0000h; the rest of 😖 moves that zero into SI.
🐰 consumes the leading F0h from 😗 to make AL=F0h, after which CBW; XCHG DI,AX establishes DI=FFF0h. Seven SCASWs advance it to FFFEh.
The final state is DX=B8EFh, SI=0000h, DI=FFFEh, and SP=0000h.
The value of B8EFh in DX is within the lower portion of CGA video memory if used as a segment.
An additional 😯 can roll DI over to 0000h. The value FFFEh left in DI has conceivable uses as a -2 constant.
🐸☺️🐰🐎♐ - Load the CGA segment
This 21-byte sequence exploits the original 8088's undocumented segment-register alias to establish ES=B8EFh:
0000: F0 9F lahf
0002: 90 nop
0003: B8 E2 98 mov ax,98E2h
0006: BA EF B8 mov dx,B8EFh
0009: 8F F0 pop ax
000B: 9F lahf
000C: 90 nop
000D: B0 F0 mov al,F0h
000F: 9F lahf
0010: 90 nop
0011: 8E E2 mov es,dx
0013: 99 cwd
0014: 90 nop
A Basic Loader
bits 16
org 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
The first lodsw reads the two-byte prefix of each 'data grapheme', discarding it. The second lodswthen reads the 16-bit tail, which is converted to an 8-bit value with the magic constant 8Fh. The key here is a feature of 8088's aadinstruction that allows it to take a non-decimal base as an immediate.lahf is constantly clobbering AH, so this 17-byte decoder must itself first be constructed in memory. This can be accomplished with the CamelWrite gadget described previously.A VGA Emoji Demo
Could you construct an emoji BIOS? An entire emoji operating system? Could we have EmojiDOOM?
I look forward to seeing whatever emoji-based horrors this post brings upon the world.