- 6o6은 보호 기능이 약한 NMOS 6502 위에서 다시 6502를 소프트웨어로 실행해, 오래된 8비트 시스템에 제어 가능한 가상 실행 계층을 더하는 프로젝트임
- 게스트 코드의 명령 실행과 메모리 접근을 중간에서 통제해 주소 재매핑, 불법 읽기/쓰기 차단, jam opcode 트랩 같은 기능을 제공함
- 핵심은 호스트 6502의 ALU를 그대로 빌려 쓰는 방식으로, 게스트 레지스터와 플래그를 호스트에 올려 같은 명령을 실행한 뒤 결과를 다시 저장함
- 검증에는 Klaus Dormann의 6502 functional test suite와 lib6502 기반 테스트 환경이 쓰였고, 최적화된 구성은 1,602,516,769개 명령으로 비최적화 구성보다 36.5% 적은 명령을 실행함
- 함께 공개된 The Incredible KIMplement 1.0과 여러 예제는 KIM-1 에뮬레이션, 중첩 가상화, 작업 전환, geoRAM 기반 외부 메모리 시스템까지 6o6의 활용 범위를 보여줌
6o6과 KIMplement가 공개한 것
- The Incredible KIMplement 1.0은 1KB, 1MHz MOS/Commodore KIM-1 6502 단일 보드 컴퓨터를 에뮬레이션함
- 확장 없는 Commodore 64에서 동작함
- KIM의 내장 TTY를 지원하고, 실제 컴퓨터의 시리얼 포트를 통한 접근도 가능함
- 주소 공간은 16K로 확장됨
- 6o6은 “6502-on-6502”의 약자로, 6502 CPU 위에서 실행되는 완전한 소프트웨어 NMOS 6502 가상 CPU임
- 게스트 코드 실행을 제어함
- 문서화되지 않은 opcode와 jam opcode를 트랩함
- 모든 메모리 접근을 추상화함
- 주소 재매핑, 불법 읽기/쓰기 가로채기, 가상 메모리 기반 실행을 지원함
- Commodore 64와 Apple IIe에서 게스트
hello world를 실행하고, 다시 6o6 안에서 6o6을 실행하는 중첩 가상화도 동작함- stage 1은 거의 즉시 실행됨
- stage 2는 더 느림
- stage 3은 매우 느리지만 동작함
6502에 가상화가 필요한 이유
- 초기 개인용 컴퓨터에서는 보통 하나의 프로그램이 전체 기계를 제어했고, 프로그램이 잘못 동작하면 재부팅으로 해결할 수 있었음
- 다중 사용자나 다중 작업 환경에서는 잘못된 코드가 다른 주소 공간을 훼손하거나, 위험한 명령을 실행하거나, 자원을 독점하는 문제가 생김
- NMOS 6502는 약 4,000개 미만의 트랜지스터로 구성된 단순한 CPU라 보호 기능에 한계가 있음
- 많은 역사적 NMOS 6502 시스템은 zero page나 프로세서 스택 위치를 자유롭게 옮기기 어려웠음
- 임의 위치로 코드 주소를 재매핑해 fixup 없이 실행하는 기능이 없었음
- 특정 메모리 위치 접근을 포괄적으로 금지할 수 없었음
- 문서화되지 않은 jam 또는
KILopcode가 실행되면 프로세서가 완전히 멈출 수 있었음
- 일부 문제는 하드웨어로 완화할 수 있음
- NMI를 주기적으로 발생시키면 인터럽트 플래그를 설정해 시스템을 독점하려는 프로세스를 외부에서 멈출 수 있음
- 일부 6502 멀티태스킹 커널은 이런 방식으로 선점형 작업 전환을 구현함
- Eastern House Software의 “Trap65” 같은 in-circuit emulator는 잘못된 opcode를 트랩 가능한 BRK로 바꿀 수 있었지만, 비싸고 복잡한 버스 조작에는 한계가 있었음
6o6의 실행 방식
- 단순 인터프리터 방식도 6502에서는 실용적일 수 있음
- 6502는 레지스터 수가 적음
- 명령은 56개이고 주소 지정 모드도 많지 않음
- 프로세서 상태 추적이 비교적 쉬움
- 6o6의 “가상화”는 호스트 6502의 ALU를 게스트 내부 연산에 사용한다는 점에 있음
- 게스트 accumulator와 플래그를 호스트 CPU에 로드함
- 게스트가 실행할 동일한 명령을 호스트에서 실행함
- 결과와 플래그를 저장하고, 호스트 상태를 정리함
- 직접 산술과 플래그 처리를 재구현하지 않아도 되는 점이 이 방식의 장점임
- decimal mode, 즉 BCD 산술도 자연스럽게 동작함
- 실제 6502가 계산하므로 결과가 6502와 같음
- 메모리 값을 읽거나 레지스터를 전송할 때도 음수 플래그와 zero 플래그 처리를 위해 같은 방식을 사용함
- 구현에는 자기 수정 코드가 사용되므로 ROM에 넣으려면 별도 주의가 필요함
VM, 하네스, 커널 구조
- 6o6 VM은 전체 시스템이 아니라 엔진 역할을 맡음
- 실행 환경은 세 부분으로 나뉨
- VM: 실제 6502 위에서 동작하는 하드웨어 비의존적 가상 CPU
- 하네스: 게스트 메모리와 관리 대상 하드웨어에 대한 인터페이스
- 커널: VM을 호출하고 예외와 게스트 상태를 처리하는 제어 루프
- 하네스는 표준화된 jump table로 바이너리 인터페이스를 제공함
- 특정 주소의 load/store를 구현함
- instruction fetch를 처리함
- 하드웨어 스택과 스택 포인터를 유지함
- VM은 페이지 크기나 메모리 페이징 존재 여부를 가정하지 않음
- 하네스는 단순 덧셈·비트 시프트 기반 주소 변환부터 페이지드 가상 메모리까지 구현할 수 있음
- page fault를 외부로 넘기지 않고 하네스가 load/store 과정에서 paging in/out을 수행할 수 있음
- 보호 예외도 하네스가 발생시킬 수 있음
- 커널은 VM 실행을 시작하고, VM이 반환한 상태 코드를 해석함
- 하네스나 6o6 자체에서 발생한 예외를 처리함
- 게스트 레지스터와 PC를 검사하거나 수정함
- 특정 서비스 루틴을 네이티브로 처리할 수 있음
- VM 호출 사이에는 게스트 CPU가 “정지” 상태이므로 상태 캡처나 컨텍스트 스위칭이 가능함
- VM은 가상 IRQ, NMI, reset을 직접 발생시키지 않음
- 이런 이벤트 발생 시점은 커널이 결정함
- BRK는 지원되지만, VM은 스택을 설정한 뒤 새 PC로 이동하지 않고 예외를 반환함
KIMplement에서의 6o6 활용
- KIMplement의 하네스는 표준 6502 스택과 KIM-4 확장 장치를 가상화함
$0000-$17ff: 읽기/쓰기 메모리$1800-$1fff: ROM$2000-$3fff: RAM$4000-$fff7: 매핑되지 않은 쓰기 불가 공간$1ff8-$1fff는 벡터용으로$fff8-$ffff에 mirror됨
- 호스트 Commodore 64는 하위 16K를
$4000-$7fff에 저장하고, 나머지는 하네스가 합성함 - KIMplement 커널은 RRIOT 에뮬레이션, LED 표시, TTY 서비스, stop과 Single-Step Switch용 NMI 삽입, KIM-1 ROM monitor 일부 트랩을 처리함
- VM 실행 후 KIMplement 커널은 6o6의 PC를 검사해 현재 루틴을 가로챌지 결정함
- TTY와 일부 기능이 이 방식으로 구현됨
성능 최적화
- 6o6과 하네스 사이의 호출은 큰 병목이 될 수 있음
- 단순한 명령도 최소 한 번의 fetch가 필요함
- 간접 주소 지정은 더 많은 메모리 접근을 만들 수 있음
- KIMplement 0.2에서는 메모리 load 일부를 전처리기 매크로로 인라인화함
- 임의 가상 주소용 load 루틴과 zero page 최적화 load 루틴을 VM에 직접 연결함
- 속도는 크게 개선됐지만 VM 크기는 커짐
- store는 빈도가 낮고 더 복잡해 서브루틴 호출로 유지함
- 최신 반복에서는 인라인 매크로가 program counter에 접근하는 방식의 비효율도 정리해 instruction fetch를 더 개선함
- KIMplement 0.3에서는 “extra helpings”라는 원시적인 instruction fusion을 추가함
- 메모리를 건드리지 않는 명령은 커널로 즉시 돌아갈 필요가 없음
- 즉시값 명령, accumulator 중심 명령, 대부분의 implied 명령, 분기하지 않은 branch가 대상임
- load/store, PC 비순차 변경, 예외가 발생하면 VM은 명령 묶기 시도를 멈춤
- extra helpings는 VM 자체를 빠르게 만들지는 않음
- KIMplement처럼 PC 위치에 따라 기능을 제한하면 약간 느릴 수도 있음
- 대신 커널이 관찰할 변화가 없는 명령마다 불필요하게 실행되지 않아 전체 시스템의 다른 부분이 빨라짐
- PC를 정밀하게 제어해야 하는 애플리케이션에는 방해가 될 수 있어 단계적 또는 완전 비활성화 옵션이 있음
검증과 테스트 결과
- 검증에는 Klaus Dormann의 functional test suite가 사용됨
- 제공된 바이너리는 하드웨어에 대한 가정을 하지 않음
- 성공 종료는 특정 위치의 무한 루프로 신호를 보냄
- 테스트는 Ian Piumarta의 lib6502 CPU emulator를 사용해 셸에서 직접 실행되도록 구성됨
- lib6502는 decimal mode의 edge case 때문에 처음에는 실패했고, 패치 후 통과함
- Klaus가 제공하는 바이너리는 전체 64K라서 기본 6502 주소 공간 안에 6o6과 함께 둘 수 없었음
- lib6502에 32K bank-switched 최소 시스템 패치를 추가함
$7000-$efff영역을 사용하고, 테스트 바이너리의 앞 32K와 뒤 32K를 서로 다른 bank에 둠
- 세 가지 구성으로 테스트함
- extra helpings 없음, inline fetch macro 없음
- inline fetch macro 있음, extra helpings 없음
- inline fetch macro와 extra helpings 모두 있음
- 세 구성 모두 Klaus suite를 통과함
- 명령 수 결과는 다음과 같음
- 6o6 없는 lib6502: 30,646,178개 명령
- 최적화 없는 6o6: 2,188,322,914개 명령
- inline fetch macro 적용: 1,713,350,225개 명령
- inline fetch macro와 extra helpings 적용: 1,602,516,769개 명령
- 가장 빠른 6o6 구성은 가장 덜 최적화된 구성보다 36.5% 적은 명령을 실행함
- 가장 빠른 구성은 게스트 명령 하나당 평균 52.3개 명령을 실행함
- 이 수치에는 하네스, 커널, 6o6 실행이 모두 포함됨
- 명령마다 cycle count가 다르므로 속도 배율로 해석하면 안 됨
포함된 예제
- hello world 예제는 같은 프로그램을 먼저 네이티브 CPU에서 실행하고, 이후 6o6을 통해 실행함
- Commodore 64에서는
$ffd2, Apple II에서는$fded의 문자 출력 루틴으로 매핑됨 - 커널은 PC가 문자 출력 루틴을 가리키는 것을 감지하면 게스트 accumulator를 가져와 네이티브 ROM 루틴을 호출하고, 스택에서 return address를 꺼내 루프로 돌아감
- Commodore 64에서는
- inception 예제는 같은 하네스와 커널로 6o6이 자기 자신을 payload로 실행함
- 각 stage는 고유한 zero page와 stack을 가짐
- 현재 6o6은 자기 수정 코드를 사용하므로 stage마다 VM 복사본이 필요함
- stage 3에서는 메모리 대부분이 VM 3개 사본에 쓰이며, inline fetch macro 포함 VM은 개당 10KB 이상임
- 중첩 실행에서
CHROUT호출은 stage 3에서 stage 2, stage 1을 거쳐 마지막에 네이티브 루틴으로 전달됨 - payload 종료는 RTS 명령을 “kick”처럼 사용함
- 시작 시 스택에 return address가 없기 때문에 RTS는 stack underflow를 유발함
- 하네스가 이를 예외로 보고하면 커널이 정상 종료로 처리함
- 깊은 stage에서도 같은 방식으로 상위 커널까지 전파됨
- Apple II에서는 실행 후
CALL 2051, Commodore 64에서는RUN으로 다시 실행할 수 있음- Apple II 버전은
$9000위의 resident DOS 영역까지 사용하므로 실행 후 재부팅을 권장함
- Apple II 버전은
작업 전환 예제
- tasks 예제는 두 독립 작업 사이를 오가는 작은 task-switching 커널임
- 각 작업은 자기 zero page, stack, 작은 코드 주소 영역을 가지며, 서로와 VM의 존재를 알지 못함
- 하나의 작업은 알파벳을 표시하고, 다른 작업은 숫자를 표시함
- 숫자는 시각적으로 구분하기 위해 reverse video로 표시됨
- 키를 누를 때마다 작업이 전환됨
- 두 작업은 zero page의 같은 위치를 상태 저장에 쓰지만, zero page가 독립되어 있어 각자 마지막 위치에서 이어서 실행됨
- 컨텍스트 스위칭에 필요한 것은 현재 작업 정보와 각 작업의 A, X, Y, P, S, PC 상태 저장 영역임
- 하네스는 “on CPU” 작업을 보고 zero page, stack, 실행 코드의 물리 주소를 선택함
- 커널은 전환 시 다른 상태를 저장·로드하고 다른 작업을 “on processor”로 표시함
geoRAM 기반 64K 외부 메모리 예제
- vmgr 예제는 Commodore 64 전용이며, 시스템 자체 RAM을 쓰지 않는 64K 주소 공간을 외부 메모리로 제공함
- geoRAM은 Commodore 공식 REU와 다른 paged RAM 장치임
- REU는 DMA 중심이며, MOS 8726 REC를 사용해 main memory와 읽기·쓰기·교환 작업을 수행함
- geoRAM은 I/O 범위
$de00의 256바이트 window page로 메모리를 매핑함 - 제어 레지스터는
$dffe,$dfff에 있음 - 현대 호환 clone은 최대 4MB 용량도 있음
- VICE는 geoRAM 에뮬레이션을 지원함
- 예제는 RC2014 Z80 kit computer용으로 제공됐던 6502 processor module의 ROM을 사용함
- ROM에는 monitor와 Lee Davison의 EhBASIC이 들어 있음
- 사용된 ROM은 GitHub에 있는 pre-built ROM이며, 6551 버전을 사용함
- 하네스는 guest ROM 영역인
$c100부터의 write를 무시함- 16K 미만 쓰기는 빠른 경로를 사용함
- 그 이상은 mask와 shift로 geoRAM bank를 조정함
- 현재 geoRAM page를 캐시해 같은 page 접근에서는 설정 작업을 건너뜀
- 커널과 main program은 이 예제에서 하나로 합쳐져 있음
- geoRAM 존재와 동작을 확인함
- ROM 이미지를 geoRAM에 복사함
- BRK는 monitor로 되돌림
- illegal instruction, user-defined instruction trap 등은 BRK처럼 처리함
- RC2014 ROM의 serial vector를 가로채 간단한 터미널을 에뮬레이션함
- PETSCII와 터미널 문자 변환을 수행함
- 작은 커서를 유지함
- 게스트 레지스터와 플래그를 결과에 맞게 조정함
CTRL-SHIFT-Commodore로 에뮬레이트된 시스템을 reset할 수 있고 메모리는 유지됨
- EhBASIC cold start에서 메모리 크기를 직접 입력하지 않으면 C64와 geoRAM 조합에서 32768바이트 free를 찾는 데 약 1분이 걸림
- ROM은
$8000hard cap으로 빌드되어 실제로는 더 있어도 32768바이트로 제한됨 $8000-$c0ff에는 다른 것을 둘 수 있음- EhBASIC은 소문자 command나 keyword를 받지 않아 모두 대문자로 입력해야 함
- ROM은
- 실제 Commodore 128DCR과 512K geoRAM cartridge에서도 동작함
- 부동소수점 연산도 정상 동작함
- bad instruction은 제어된 방식으로 즉시 가로채짐
- 256바이트 window를 제외하면 화면의 시스템은 6502 자체 주소 공간에서 실행되지 않음
- 512K geoRAM에서도 64K 6502 시스템 task 8개를 별도로 둘 수 있음
향후 개선과 활용처
- 6o6을 ROM에서 실행할 수 있게 만드는 개선은 가능하지만, 리팩터링이 필요하고 더 느려질 수 있어 선택지에 가까움
- 65816 에뮬레이션은 범위 밖으로 보지만, NMOS 시스템에서 CMOS 명령을 에뮬레이트하는 것은 가능할 수 있음
- ALU를 사용하므로 NMOS 6502가 CMOS 65C02를 에뮬레이트해도 플래그는 NMOS 방식으로 설정됨
- 반대 방향도 마찬가지임
- 주소 지정은 현재 NMOS CPU 방식으로 작성되어 있음
- inline memory macro 방식에는 peephole optimization 기회가 있음
- 실제 assembly 전에 “post-preprocessor” pass를 둘 수 있음
- 도구 체인이 더 복잡해지므로 일반적인 이득을 확인해야 함
- 다운로드한 코드를 현재 작업을 망치지 않고 실행하는 용도가 6o6의 명시적 활용처 중 하나임
- Gopher client 일부로 사용해 다운로드한 것을 동적으로 실행하는 아이디어가 있음
- 새 6502 시스템을 직접 설계한다면 필요한 기능을 하드웨어로 구현하는 쪽이 더 빠를 수 있음
- NMOS CPU에서 보호 장치가 적거나 추가 실리콘을 최소화하려는 경우, 6o6은 유연하고 적응 가능한 대안이 됨
배포와 라이선스
- The Incredible KIMplement는 홈페이지와 GitHub에서 제공됨
- 6o6은 GitHub에서 제공되며, 글에서 다룬 네 가지 예제도 포함됨
- KIMplement 1.0 업데이트는 공개용 정리와 작은 버그 수정 중심임
- KIMplement에는 Dave Hassler가 제공한 Tiny PILOT도 포함됨
- Tiny PILOT은 1979년 MICRO magazine에 Nicholas Vrtis가 작성한 구현에 Bob Applegate와 Dave Hassler의 패치가 더해진 것임
- Dave Hassler는 Carol Shaw와 Harry Stewart의 1980 Atari PILOT 구현에서 ELIZA도 포팅함
- KIMplement와 6o6은 모두 Floodgap Free Software License로 배포됨