본문으로 건너뛰기

© 2026 Molayo

HN분석2026. 05. 18. 20:02

16바이트 x86 어셈블리 코드로 매트릭스 패턴을 사운드로 변환하는 방법

요약

16바이트의 극도로 제한된 x86 어셈블리 코드를 사용하여 Sierpinski 프랙탈을 시각화하고 이를 오디오 데이터로 변환하는 알고리즘 밀도를 탐구합니다. 이 코드는 비디오 메모리를 계산 공간으로 활용하며, 이항 계수 수열과 Rule 60 세포 자동자의 원리를 통해 수학적 패턴을 생성합니다.

핵심 포인트

  • 16바이트라는 극소량의 코드로 프랙탈 생성 및 오디오 변환이 가능한 알고리즘 밀도 구현
  • x86 실모드 DOS 환경에서 비디오 메모리(0xB800)를 계산 공간으로 활용
  • 누적 접두사 합(running prefix sum) 원리를 이용한 이항 계수 수열 생성
  • XOR 연산을 통해 비트 평면을 분리하여 Stephen Wolfram의 Rule 60 세포 자동자 패턴 구현

2026년 5월 Outline Demoparty에서 공개된 Ommen, NL

x86 어셈블리 16바이트에 담긴 알고리즘 밀도 탐구.

demoscene에서는 극도의 제약 조건 내에서 무엇을 달성할 수 있는지 탐구하는 것이 보람 있는 기술적 도전입니다. 다음 x86 실모드 DOS 어셈블리 16바이트 코드는 알고리즘 밀도를 신중하게 보여주는 예시입니다. 이 코드가 실행되면 컴퓨터의 비디오 메모리를 계산 공간으로 활용하여 무한한 Sierpinski fractal을 그리고, 동시에 그 기하학적 형태를 오디오 데이터로 해석합니다.

int 10h ; 2 bytes
mov bh, 0xb8 ; 2 bytes
mov ds, bx ; 2 bytes
...

이 코드는 표준 BIOS 인터럽트인 int 10h로 시작합니다.

이는 Video Mode 0을 초기화하여 40x25 문자 모드 그리드를 설정합니다. 이후의 명령어들은 데이터 세그먼트(DS)를 VGA/CGA 텍스트 버퍼의 물리 메모리 주소인 0xB800에 지정합니다.

BIOS가 이 인터럽트 동안 화면을 지울 때, 단순히 절대 영(zero)으로 채우지는 않습니다. 문자 모드에서 모든 문자 공간은 두 바이트로 구성됩니다: ASCII 문자와 색상 속성입니다. BIOS는 2,000개의 모든 문자 슬롯을 균일하게 초기화합니다: ASCII 바이트는 0x20 (공백 문자)으로 설정되고, 색상 바이트는 0x07로 설정됩니다.

프랙탈의 순수 수학적 이해를 위해 변수를 임시로 분리해 보겠습니다. 기본값인 0x20 초기화 대신 완벽하게 0이 된 상태를 모델링할 것입니다. 또한, xor 대신 add를 사용하고, 누산기 AL에 값 2가 로드되었다고 가정하며 이 메모리를 16바이트 간격으로 전진합니다.

실모드 DOS 세그먼트는 정확히 65,536 바이트입니다. 반복마다 16바이트씩 이동하면 해당 세그먼트를 탐색하는 데 정확히 4,096 단계가 걸립니다 ($ ext{65536} / ext{16} = ext{4096}$). SI 레지스터가 0xFFFF를 지나면 깨끗하게 0x0000으로 순환됩니다.

루프가 진행됨에 따라, 누산기 (accumulator)의 현재 값을 메모리 셀에 더하고, 업데이트된 값을 다시 누산기로 읽어옵니다. 이는 효과적으로 누적 접두사 합 (running prefix sum)을 생성합니다. 4,096은 256(8비트 레지스터의 용량)의 배수이므로, 세그먼트가 순환할 때 수학적 올림 (carryover)이 일치하여, 매 전체 스윕 (sweep)이 끝날 때 AL이 2로 깔끔하게 재설정됩니다.

패스 (pass) $p$ 동안 세그먼트에 걸쳐 나타나는 $k$번째 수정된 셀의 값은 초기값인 2에 의해 스케일링된 이항 계수 (binomial coefficient) 수열을 따릅니다:

다음 표는 16개의 메모리 셀에 걸친 계산의 처음 16단계를 보여주며, 값이 행별로 어떻게 누적되는지 입증합니다:

Pass \ Cell

이 십진수 값들 내부에는 더 깊은 패턴이 존재합니다. 이진 덧셈 (binary addition)을 수행할 때, 비트 평면 (bit-planes)은 인접한 위치로 올림 (carry)됩니다. 그러나 산술적 올림을 버리고 엄격하게 모듈로 2 (modulo 2) 연산을 수행하면, 배타적 논리합 (Exclusive OR, XOR) 연산이 남게 됩니다.

add 대신 xor를 사용함으로써, 알고리즘은 비트 평면을 분리합니다. 모델링된 시작값이 2 (이진수 00000010)이므로, 오직 비트 1 (Bit 1)만이 이 특정 계산에 의해 영향을 받습니다. 연쇄적인 십진수들은 0x000x02 사이의 순수한 토글 (toggle)이 됩니다. 이 진행 과정은 Stephen Wolfram의 기본 세포 자동자 (elementary cellular automata) 중 Rule 60과 완벽하게 일치합니다:

Lucas의 정리에 따르면, 이 XOR 관계는 덧셈 표의 비트 1 상태와 일치함이 수학적으로 보장됩니다. 우리는 이진 전파 (binary propagation)를 시각화하여 이를 검증할 수 있습니다 (여기서 '2'는 비트 1이 설정되었음을 나타냄):

Pass \ Cell

명령어 out 61h, al에는 놀라울 정도로 우아한 디테일이 담겨 있습니다. 포트 61h는 내부 PC 스피커와 인터페이스합니다. 이 포트의 **비트 1 (Bit 1)**은 1로 설정되면 스피커 콘을 바깥으로 밀어내고, 0으로 설정되면 다시 되돌림으로써 스피커 콘을 직접 제어합니다. 우리의 루틴은 XOR를 통해 프랙탈을 계산하고, 메모리를 업데이트하며, 즉시 해당 바이트를 스피커 포트로 전송합니다.

알고리즘이 Bit 1을 특정하여 분리하고 토글하기 때문에, Sierpinski 삼각형 (Sierpinski triangle)의 기하학적 구조는 스피커 콘 (speaker cone)을 위한 직접적인 명령 세트 역할을 합니다. CPU의 실행 속도가 기능적인 샘플 레이트 (sample rate)를 결정합니다.

프랙탈에 의해 생성된 1과 0의 패턴은 펄스 폭 (pulse width)과 주파수 (frequency)가 자연스럽게 변화하는 뚜렷한 사각파 (square waves)를 만들어냅니다. 다음 그래프들은 위 표의 상태들을 오디오 파형 (audio waveforms)으로 시각화한 것입니다:

프랙탈이 교차하는 행을 생성할 때(예: Pass 2), 더 높은 주파수의 사각파를 출력합니다. 구조가 더 큰 0의 블록을 도입할 때(예: Pass 4와 같이 삼각형 내부의 빈 영역), 스피커 콘은 정지 상태를 유지하며 리드미컬한 일시 정지(pauses)를 만들어냅니다. 결과적으로 생성된 오디오는 수학적 구조의 직접적인 소리 표현이 됩니다.

실제 코드로 돌아가 보면, 코드가 16씩 단계적으로 이동하지 않는다는 점을 알 수 있습니다. lodsb에 의한 증가분과 결합된 sub si, byte 57 명령은 반복당 -56 바이트의 순 이동을 초래합니다. 즉, 루틴은 메모리를 역방향으로 탐색합니다.

이러한 조정은 출력의 청각적 주파수와 시각적 레이아웃을 모두 변화시킵니다.

16바이트 단계는 정확히 4,096번의 반복으로 세그먼트 스윕 (segment sweep)을 완료하지만, 56은 65,536을 나누어떨어지게 하지 않습니다 ($\gcd(56, 65536) = 8$). 루프는 8의 배수인 오프셋 (offsets)만 방문하며, 모든 사용 가능한 주소를 모두 훑기 위해서는 8,192번의 반복이 필요하고, 0x0000으로 돌아오기 전까지 세그먼트를 7번 회전하게 됩니다.

8,192는 256으로 나누어떨어지므로, Sierpinski 수열의 수학적 연속성은 유지됩니다. 그러나 매크로 사이클 (macro-cycle)이 4,096단계가 아닌 8,192단계로 길어졌기 때문에, 한 번의 패스 (pass)를 완료하는 데 두 배의 CPU 사이클이 소요됩니다. 이는 시스템의 기본 주파수를 절반으로 줄여, 청각적 리듬을 한 옥타브 (one octave) 낮추며, 결과적으로 더 느리고 깊은 톤을 만들어냅니다.

이 지점에서 루틴의 이중 목적(dual-purpose) 특성이 진정으로 빛을 발합니다. 알고리즘은 ASCII 문자 바이트에 직접 쓰는데, 여기서 비트 1(Bit 1)은 스피커 콘(speaker cone)을 토글하는 역할을 전담합니다. 동시에 나머지 7개 비트는 일련의 의사 난수(pseudo-random) ASCII 글리프(glyphs)로 변이하며, 제작물의 혼돈스러운 시각적 질감을 제공합니다. 놀랍게도, 이 혼합 데이터 바이트 전체를 역사적으로 다양한 저수준 메인보드 제어를 관리해 온 시스템 포트 61h로 직접 전송해도 시스템은 중단되지 않습니다. 표준 DOS 환경과 현대의 에뮬레이터(emulators)에서 이 추가 비트들을 포트로 밀어 넣는 것은 사실상 무해합니다. 이러한 우아한 우연 덕분에 시각적 문자 데이터는 하드웨어 충돌을 일으키지 않고 오디오 신호로서 안전하게 이중 역할을 수행할 수 있습니다.

또한 -56바이트 단계(step)가 어떻게 40문자(80바이트)의 화면 너비에 매핑되는지도 관찰할 수 있습니다.

시퀀스는 단계당 위로 1행, 오른쪽으로 12열씩 진행됩니다. 방문하는 고유 열의 수를 결정하기 위해 최대 공약수(Greatest Common Divisor, GCD)를 계산합니다: ( \gcd(12, 40) = 4 ).

산술적 계산을 통해 루프가 정확히 열의 4분의 1(( 40 / 4 = 10 ))을 방문함을 보장합니다. 결과적으로 프랙탈(fractal)은 연속된 이미지로 렌더링되지 않습니다. 대신, 공간적 오프셋(spatial offset)이 이를 대각선으로 전단(shear)하여 화면을 타고 올라가는 10개의 균일한 간격의 수직 열로 만듭니다.

이 루틴이 클래식한 단색 녹색 모니터에서 실행되는 모습을 상상해 본다면, 패턴의 기하학적 밀도를 반영하는 전자음과 함께 깜빡이며 상승하는 글리프들이 10개의 뚜렷한 기둥 형태로 나타나는 것을 관찰할 수 있을 것입니다.

하지만 하드웨어의 초기 상태와 관련된 실질적인 현실을 인정하는 것도 중요합니다. 이론적 모델은 완벽하게 초기화된 예측 가능한 환경을 가정합니다. 실제로 BIOS 인터럽트(interrupt) 10h 이후에는

, 서로 다른 시스템 구성, 다양한 VGA BIOS 구현, 그리고 현대적인 에뮬레이터(DOSBox 또는 PCem 등)는 RAM의 상위 영역에 약간씩 다른 아티팩트(artifacts)나 기본값을 남길 수 있습니다. 우리의 알고리즘은 메모리에 이미 존재하는 무엇인가를 지속적으로 읽고 XOR 연산을 수행하기 때문에, 이러한 기존 비트(bits)들과 직접적으로 상호작용하게 됩니다.

그 결과, 그 효과는 환경에 매우 민감하게 반응합니다. 표시되는 시각적 문자들과 사운드의 음색(timbre)은 코드를 실행하는 특정 기기나 에뮬레이터에 따라 눈에 띄게 달라질 수 있습니다.

모든 가능한 하드웨어 구성에서 동일하고 균일한 출력을 보장하려면, 전체 메모리 세그먼트(segment)를 명시적으로 지우거나 미리 설정하는 짧은 설정 루틴(setup routine)을 추가하기만 하면 됩니다. 이렇게 하면 이론적인 완벽함을 보장할 수 있겠지만, 자연스럽게 엄격한 16바이트 제한이 허용하는 것보다 약간 더 큰 코드 크기(footprint)를 요구하게 될 것입니다. 궁극적으로, 약간의 코드 공간만 더 있다면 이러한 메모리 환경 중 어떤 것이든 다른 시스템에서 재현할 수 있습니다. 하지만 기기의 자연스러운 상태가 지닌 미묘한 예측 불가능성을 받아들이는 것 또한 이러한 극도로 제한된 제약 조건 내에서 작업하는 묘미 중 하나입니다. 읽어주셔서 감사합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0