잠 깨라! 16b

발행: (2026년 5월 24일 AM 09:30 GMT+9)
12 분 소요

출처: Hacker News

2026년 5월 네덜란드 옴멘에서 열린 Outline Demoparty에서 공개

16바이트 x86 어셈블리에서 알고리즘 밀도를 탐구한 작품.


여러분, 안녕하세요. 저는 30년 전 초록색 단색 모니터가 달린 오래된 IBM PC에서 프로그래밍을 배웠고, 언제나 이 시스템용 프로그램을 만들고 싶었습니다. 지난 15년 동안 100개가 넘는 작은 인트로를 제작했죠. 최근엔 활동이 다소 뜸했지만, Plex의 환상적인 “Rainbow Surf”가 단 16바이트로 구현된 것을 보고 오래된 스케치를 다시 꺼내 작업을 시작하게 되었습니다.

이 프로그램은 평소와 다름없는 실험 과정에서 탄생했습니다. 저는 그래픽과 사운드를 위한 셀룰러 오토마톤을 가지고 놀면서 사이즈코딩 트릭을 발견했는데, 구체적으로는
a) add [bx+si],al 같은 다형성 어셈블리 명령(0x0000)과
b) 바이트를 절약하고 opcode를 재사용하기 위해 명령어 중간으로 점프하는 기법이었습니다. 수백 번의 작은 실험 중 이 코드가 소리만 들어도 눈에 띄었습니다.

남은 부분을 정리하고 “그 외”를 제거했을 때, 실제로 무슨 일이 일어나고 있는지 파악하기가 어려웠습니다. 많은 바이트를 골프한 뒤 남은 간단한 수식을 보고 머리를 쥐어뜯었죠. 이렇게 몇 바이트만으로 이렇게 깊은 설명이 필요할 줄은 몰랐습니다.

제가 2014년에 만든 원래 “M8trix”는 화면에 무작위 문자들을 흐리게 흩뿌렸습니다(8바이트, 그 다음엔 7바이트). 언제나 “소리를 좋게” 만들 수 있을까 고민했지만, “wakeup” 개발 순서상 사운드가 먼저였습니다. “보는 것이 듣는 것”이니 크게 중요하지는 않지만, “16바이트로 시어핀스키 사운드를 매트릭스 비처럼 바꾸다”라는 부제목이 딱 맞겠네요 =)

TL;DR: 매 타임스텝마다 시어핀스키 삼각형의 한 줄이
a) 스피커로 재생되고
b) 화면에 56칸 간격으로 그려집니다. 움직임은 감지할 수 있지만, 실제로는 8192 “픽셀 폭”이지만 문자 한 줄은 80바이트에 불과해 눈에 잘 보이지 않습니다. 훨씬 더 큰 화면에서는 삼각형을 볼 수 있겠죠. 혹은 “픽셀을 건너뛰지 않고” 한 번에 모두 그리면 역시 삼각형이 보일 겁니다.


다음은 16바이트짜리 x86 실모드 DOS 어셈블리입니다. 실행하면 비디오 메모리를 계산 공간으로 사용해 무한 시어핀스키 프랙탈을 그리면서 동시에 그 기하학을 스피커에 출력합니다.

int 10h          ; 2 bytes
mov bh, 0xb8     ; 2 bytes
mov ds, bx       ; 2 bytes
L:
lodsb            ; 1 byte
sub si, byte 57  ; 3 bytes
xor [si], al     ; 2 bytes
out 61h, al      ; 2 bytes
jmp short L      ; 2 bytes

1. 캔버스: 준비된 공허

코드는 표준 BIOS 인터럽트 int 10h로 시작합니다. 이는 비디오 모드 0을 설정해 40×25 텍스트 모드 그리드를 만들죠. 그 다음 데이터 세그먼트(ds)를 0xb800(VGA/CGA 텍스트 버퍼 주소)으로 지정합니다.

BIOS가 화면을 지울 때 메모리를 절대 0으로 채우지는 않습니다. 각 문자 위치는 두 바이트(ASCII 문자 + 색상 속성)로 구성됩니다. 2,000개의 슬롯 모두가 0x20(스페이스)과 0x07(검은 배경에 연한 회색)으로 초기화됩니다. 그래서 화면은 비어 보이지만 메모리는 이미 균일한 패턴으로 채워져 있습니다.

나는 많은 “노이즈” 혹은 “CA” 사운드 인트로를 만들었지만, 이 작품만큼 눈에 띄는 건 없었다. 정말 예상 밖이었다! 여기서 핵심은 “화면을 지울 때 메모리가 어떻게 초기화되는가”와 “실제 보이는 메모리 앞뒤에 무엇이 있는가”이다. “순수” 사운드도 매력적이지만(몇 바이트만 더 추가하면 모든 시스템에서 동일하게 들리게 할 수 있다) 이 미묘한 차이가 아직 완전히 이해되지 않아도 사운드를 더욱 돋보이게 만든다.

2. 엔진: 가산 접두합

이와 같은 교차·공감각은 다른 작은 인트로에서 본 것보다 훨씬 깊습니다. 오히려 RNG 없이 “카오스 게임”을 위한 반복 함수 시스템보다 더 많은 수학적 비밀과 관계를 드러낸다고 말하고 싶습니다. 이번에는 여러분이 듣는 소리의 수학을 근본적으로 이해하도록 하겠습니다. 단순히 “여기서 연산을 하면 흥미로운 소리가 난다”는 수준이 아니라 말이죠.

수학적으로 정리하면: 0x20 대신 0으로 초기화된 상태를 가정하고, xor 대신 add를 사용하며, 한 번에 16바이트씩 전진한다고 가정합니다. 누산기 al2에서 시작합니다.

DOS 세그먼트는 정확히 65,536바이트이며, 16바이트씩 이동하면 4,096 스텝((65536 / 16 = 4096))만에 세그먼트를 한 바퀴 돕니다. 그때 si0x0000으로 깔끔히 래핑됩니다.

셀 사이의 값을 더하면 부분합이 만들어집니다. 4,096은 256(8비트 레지스터 크기)의 배수이므로, 세그먼트가 래핑될 때 캐리오버가 완벽히 맞아떨어져 al이 매 스윕 시작 시 2로 재설정됩니다.

값은 2배 스케일된 이항수열을 따릅니다:

$$A^{(p)}[k] \equiv 2 \binom{k+p}{p-1} \pmod{256}$$

아래는 첫 16스텝이 행별로 누적되는 모습을 보여줍니다.

PassCell

3. 결정화: XOR와 시어핀스키 시프트

이제 조합론으로 돌아갑니다. 모듈로 2 연산을 할 때, 시어핀스키 삼각형이 나타나는 특별한 법칙이 있습니다. 이 비트가 스피커에 전달되고, 다른 비트들은 무시됩니다.

비트플레인을 분리하기 위해 XOR가 사용됩니다. 비트의 무캐리 합은 바로 XOR이기 때문이죠.

코드가 2(이진 00000010)로 시작하므로 비트 1만 0x000x02 사이에서 토글됩니다. 이는 초등 셀룰러 오토마톤의 rule 60과 정확히 일치합니다:

$$Cell^{(p)}[k] = Cell^{(p-1)}[k] \oplus Cell^{(p)}[k-1]$$

루카스 정리는 이 식이 덧셈표의 비트 1과 일치함을 보장합니다. 직접 확인해 보세요(‘2’는 비트 1이 설정됐음을 의미).

PassCell

4. 머신의 목소리: 데이터를 오디오로 변환

핵심 트릭은 out 61h, al입니다.

포트 61h는 PC 스피커와 연결됩니다. 비트 1이 스피커 콘을 밖으로(1) 혹은 안으로(0) 움직이게 합니다. 코드는 XOR로 프랙탈을 계산해 메모리에 기록하고, 그 바이트를 바로 스피커 포트에 전달합니다.

프랙탈에서 나온 1과 0은 자연스럽게 펄스 폭과 주파수가 변하는 사각파를 생성합니다.

라인 단위로 재생하면 자체 유사성을 가진, 거의 템포에 독립적인 바이트비트가 만들어집니다.

게다가 텍스트만 스피커에 출력되는 것이 아니라, 64KB 세그먼트 전체(그 안에 그림자 비디오 ROM BIOS 코드도 포함)도 함께 출력됩니다. 이것이 기대했던 시어핀스키 라인 기반 직사각형 파형 바이트비트와는 크게 다른, 펑키하고 거친 사운드의 비밀 재료입니다.

5. 56바이트 스텝: 옥타브 이동과 대각선 전단

M8trix 효과를 재현하려면 셀 자체를 화면에 흩뿌려야 합니다. 이때 사운드 버퍼가 너무 커지지 않으면서 문자들이 고르게 퍼지게 해야 하죠.

그래서 코드는 16이 아니라 -56바이트씩 이동합니다

0 조회
Back to Blog

관련 글

더 보기 »