에뮬레이터와 가상 머신
게임 에뮬레이터는 옛 게임기를 통째로 소프트웨어로 흉내 내고, 가상 머신은 지금 쓰는 컴퓨터를 하드웨어의 도움으로 여러 대처럼 나눠 쓴다. 작은 CPU 인터프리터를 직접 돌려 보는 것에서 시작해 메모리 맵·타이밍·JIT, 하이퍼바이저·트랩·2단계 주소 변환, 원격 데스크톱, 언어 가상 머신, 법적 쟁점까지 따라간다.
목차 (14)
한 문장 답: 무엇을 흉내 내는가
에뮬레이터는 "다른 기계"를 소프트웨어로 흉내 낸다. NES의 CPU는 6502 계열이고 PC는 x86-64나 Arm이라 기계어가 서로 통하지 않는다. 그래서 에뮬레이터는 게임의 명령을 하나씩 읽어 그 뜻대로 자기 변수를 바꾸고, 화면·소리·컨트롤러까지 모두 프로그램으로 만들어 낸다. 가상 머신(VM)은 "같은 종류의 기계"를 나눠 쓴다. x86-64 서버 위의 x86-64 게스트 OS는 대부분의 명령을 CPU가 그대로 실행하고, 하드웨어를 직접 건드리는 특권 동작만 하이퍼바이저가 가로채 처리한다. 그래서 VM은 거의 제 속도가 나고, 에뮬레이터는 흉내 내는 기계보다 훨씬 빠른 컴퓨터가 필요하다.
경계가 늘 깔끔하지는 않다. QEMU는 혼자 돌면 Arm 보드를 x86 PC에서 흉내 내는 에뮬레이터이고, KVM과 함께 돌면 같은 종류의 CPU를 쓰는 VM이 된다. 비슷한 말로 묶이는 것들을 한 표에 놓으면 다음과 같다.
| 종류 | 무엇을 흉내 내나 | 어떻게 | 명령 집합 | 예 |
|---|---|---|---|---|
| 에뮬레이터 | 다른 기계 전체(CPU·화면·소리·입력) | 소프트웨어로 명령을 해석하거나 번역 | 달라도 된다 | Mesen(NES), Dolphin(게임큐브·Wii), QEMU 전체 시스템 모드 |
| 가상 머신 | 같은 종류의 컴퓨터 한 대 | CPU가 게스트 명령을 직접 실행, 특권 동작만 하이퍼바이저가 처리 | 같아야 한다 | KVM, Hyper-V, VMware ESXi, VirtualBox |
| 컨테이너 | 흉내 내지 않는다 — 격리된 프로세스 | 호스트 커널을 함께 쓰고 namespace·cgroup으로 나눈다 | 같아야 한다 | Docker, Podman, Kubernetes 파드 |
| 언어 VM | 실재하지 않는 가상의 기계 | 바이트코드를 해석하거나 JIT으로 번역 | 상관없다 | JVM, .NET CLR, WebAssembly 런타임, CPython |
| 호환 계층 | 다른 OS의 API나 다른 CPU의 앱 하나 | 시스템 콜·API를 바꿔 주거나(Wine) 앱의 기계어를 번역(Rosetta 2) | Wine은 같아야, Rosetta 2는 달라도 | Wine·Proton, Rosetta 2, WSL 1 |
컴퓨터를 흉내 낸다는 것
게임기도 컴퓨터다. 기계어를 실행하는 CPU, CPU가 주소로 읽고 쓰는 메모리(RAM과 카트리지 ROM), 화면을 그리는 그래픽 칩(NES의 PPU), 소리를 내는 오디오 칩(APU), 컨트롤러 같은 입출력 장치가 있고, 이 모두가 하나의 수정 발진기에서 나온 시계에 맞춰 움직인다. 게임은 이 부품들이 정확히 어떻게 반응하는지에 기대어 만들어졌다.
소프트웨어로 보면 이 모든 것이 결국 숫자다. 레지스터는 변수 몇 개, RAM은 바이트 배열, ROM은 파일에서 읽어 온 읽기 전용 배열, 그래픽 칩은 내부 레지스터와 비디오 메모리 배열이다. 그러니 에뮬레이터의 뼈대는 단순하다. 기계의 상태를 담은 배열 묶음과, 그 상태를 명령 하나씩 바꾸는 루프다. 루프를 한 번 돌 때마다 흐른 시간만큼 그래픽·소리 칩도 진행시키면 한 대의 게임기가 된다.
에뮬레이터 만들기 1: CPU 인터프리터
CPU 흉내의 핵심은 fetch → decode → execute 세 단계다. 프로그램 카운터(PC)가 가리키는 메모리에서 명령 바이트를 읽고(fetch), 그 비트 모양으로 어떤 명령인지 가려내고(decode, 보통 거대한 switch), 명령의 뜻대로 레지스터·메모리·PC를 바꾼다(execute). 실제 CPU가 회로로 하는 일을 코드 몇십 줄이 똑같이 한다.
첫 에뮬레이터로 흔히 고르는 것이 CHIP-8이다. 1970년대 후반 COSMAC VIP 같은 취미용 컴퓨터에서 게임을 쉽게 만들려고 고안한 가상 기계라, 실제 칩이 아니면서도 구조가 진짜 CPU와 같다. 메모리 4 KiB, 8비트 범용 레지스터 16개(V0~VF, VF는 자리올림 플래그 겸용), 주소 레지스터 I, 64 × 32 흑백 화면, 60 Hz로 줄어드는 타이머 두 개, 2바이트짜리 명령 서른몇 개가 전부다. 프로그램은 0x200번지부터 올라간다. 아래 그림은 그 가운데 일곱 가지 명령만 구현한 인터프리터로 작은 프로그램을 실제로 돌린 기록이다.
프로그램 9개 명령(18바이트)을 0x200에 올렸다. 레지스터는 모두 0이고 PC는 0x200이다. 이제 fetch → decode → execute를 되풀이한다.
몇 가지가 눈에 띈다. ADD V0, 255로 1을 뺀 것은 8비트 레지스터가 256에서 0으로 감기기 때문이다. 에뮬레이터는 이런 넘침까지 원래 기계와 똑같이 해야 한다. SE는 조건이 맞으면 다음 명령을 건너뛰는 식으로 분기하고, 반복문은 JP로 되돌아가 만든다. 실제 CHIP-8 에뮬레이터는 여기에 화면에 스프라이트를 XOR로 그리는 DXYN, 키 입력, 타이머를 더하면 완성이다. NES의 6502는 명령이 150개쯤(공식 명령 56종 × 주소 지정 방식)으로 늘고 플래그 규칙이 까다로울 뿐, 루프 구조는 같다.
원래 기계의 명세를 아는 것이 출발점이다. CHIP-8은 공개 문서가 있지만 상용 게임기는 대개 공개 문서가 없어, 사람들이 실제 기계로 실험해 동작을 알아내고 위키에 정리해 왔다. 남이 만든 바이너리를 읽는 일반적인 방법은 리버스 엔지니어링 노트, 기계어와 컴파일러의 관계는 컴파일러 노트에 있다.
메모리 맵과 장치
CPU 입장에서 세상은 주소뿐이다. 6502는 16비트 주소를 내보내 64 KiB를 가리킬 수 있는데, NES 기판은 그 주소를 보고 어느 부품이 응답할지 정한다. $0000~$07FF는 내부 RAM 2 KiB, $2000~$2007은 PPU 레지스터 8개, $4000~$4017은 APU와 컨트롤러, $8000~$FFFF는 카트리지 ROM이다. 기판이 주소 선 일부를 무시하기 때문에 RAM은 $0800~$1FFF에 세 번 더 비쳐 보이고(거울, mirror), PPU 레지스터 8개는 $3FFF까지 8바이트마다 되풀이된다.
이처럼 장치 레지스터를 메모리 주소처럼 읽고 쓰게 하는 방식을 메모리 맵 I/O라 한다. $2007에 바이트를 쓰면 RAM에 저장되는 것이 아니라 PPU가 비디오 메모리에 그 값을 적고 내부 주소를 하나 올린다. 그래서 에뮬레이터의 메모리 읽기·쓰기 함수는 단순한 배열 접근이 아니라 주소를 보고 장치 함수로 보내는 분배기다. 마이크로컨트롤러가 GPIO·UART 레지스터를 똑같이 주소로 다루는 모습은 임베디드 노트에 있다.
카트리지는 단순한 ROM이 아니다. 32 KiB보다 큰 게임은 카트리지 안의 매퍼(뱅크 전환 칩)가 ROM의 어느 조각을 $8000 창에 보여 줄지 바꾸고, 어떤 카트리지는 세이브용 RAM과 전지를 품고 있다. NES 에뮬레이터는 매퍼 종류마다 이 동작을 따로 구현해야 하며, 그 종류가 수백 가지다. 게임 롬이 어떤 칩에 어떻게 담겼는지는 저장장치 › 기억장치의 종류에서 다룬다.
그래픽·소리·입력
그래픽: NES의 PPU는 프레임 버퍼를 통째로 두지 않고, 주사선을 그리는 순간순간 배경 타일과 스프라이트를 골라 픽셀 색을 낸다. 에뮬레이터는 이 과정을 흉내 내 256 × 240 픽셀 배열을 채우고, 한 프레임이 끝나면 그 배열을 GPU 텍스처로 올려 창 크기에 맞게 확대한다. 게임이 화면을 그리는 도중에 스크롤 값을 바꾸는 기법(화면 위쪽은 고정, 아래쪽만 스크롤)이 흔해서, 줄 단위나 점 단위로 그려야 맞는 게임이 많다.
소리: APU는 펄스파 두 개, 삼각파, 노이즈, 샘플 재생 채널 다섯 개를 CPU 클록에 맞춰 돌린다. 에뮬레이터는 사이클마다 채널 출력을 계산해 섞고, 이 초당 약 179만 개의 값을 호스트 사운드 카드가 받는 48 kHz로 리샘플링해 오디오 버퍼에 쌓는다. 그냥 솎아 내면 높은 주파수가 접혀 들어와 듣기 싫은 소리가 나므로 저역 통과 필터를 먼저 거친다. 샘플링·에일리어싱·필터의 원리는 신호 처리 노트에 있다.
입력: 게임은 $4016을 읽을 때마다 컨트롤러 버튼 상태를 한 비트씩 받는다. 에뮬레이터는 호스트의 키보드·게임패드 상태를 8비트 버튼 값으로 만들어 두고, 게임이 $4016을 읽으면 다음 비트를 돌려준다.
소리는 화면보다 까다롭다. 화면은 한 프레임쯤 늦거나 한 번 겹쳐 보여도 잘 모르지만, 오디오 버퍼가 비면 바로 "틱" 소리가 난다. 그래서 많은 에뮬레이터가 오디오 버퍼가 차는 속도에 맞춰 에뮬레이션 속도를 미세하게 조절한다.
타이밍과 동기화
부품들은 같은 시계를 나눠 쓴다. NTSC NES는 21.477 MHz 마스터 클록을 12로 나눠 CPU(1.79 MHz), 4로 나눠 PPU를 돌리므로 CPU 1사이클 동안 PPU는 점 3개를 그린다. 한 프레임은 262줄 × 341점이니 CPU로 치면 약 29,780.5사이클이다. 에뮬레이터의 기본 동기화는 이 비율을 지키는 것이다. CPU 명령 하나를 실행하고 걸린 사이클을 세어, 그 세 배만큼 PPU를, 같은 만큼 APU를 진행시킨다.
문제는 명령 하나 단위로도 거칠 때가 있다는 것이다. 게임이 PPU 상태 레지스터를 읽는 바로 그 사이클에 PPU가 어디를 그리고 있었는지에 따라 결과가 달라지고, 일부 게임은 그런 우연한 타이밍에 기대어 동작한다. 그래서 정확도를 높이려면 사이클마다, 심지어 사이클 안의 읽기·쓰기 순서까지 맞추는 사이클 정확(cycle-accurate) 에뮬레이션을 하게 된다. 반대편 끝은 고수준 에뮬레이션(HLE)이다. 하드웨어를 흉내 내는 대신 "이 함수는 결국 이 일을 한다"는 수준에서 결과만 만들어 낸다. 예컨대 BIOS의 파일 읽기 루틴을 명령 단위로 돌리지 않고 호스트 파일을 바로 읽어 준다. 하드웨어를 회로 수준까지 흉내 내는 쪽은 저수준 에뮬레이션(LLE)이라 부른다.
정확도와 속도는 맞바꾸는 관계다. SNES 에뮬레이터 bsnes(뒤에 higan)의 개발자는 2011년 글에서, 1990년대 에뮬레이터는 25 MHz급 PC로도 돌았지만 하드웨어를 충실히 재현하는 에뮬레이터는 3 GHz가 필요하다고 설명했다. 빠른 에뮬레이터에서는 괜찮아 보이던 게임이 몇 시간 뒤 특정 장면에서 멈추는 식의 문제가 정확도에서 갈린다는 것이다. 오래된 8·16비트 기계는 지금 컴퓨터가 충분히 빨라 정확도를 고르고, 최신 기계일수록 HLE와 아래의 JIT로 속도를 확보한다.
빠르게 하기: 동적 재컴파일(JIT)
인터프리터는 같은 명령을 백 번 실행하면 백 번 해석한다. 게임큐브·Wii(약 486~729 MHz PowerPC)나 그 이후 기계를 흉내 내려면 이 낭비를 견딜 수 없다. 그래서 동적 재컴파일(dynamic recompilation, JIT)을 쓴다. 게스트 코드를 분기 명령까지의 블록으로 잘라, 처음 만났을 때 그 블록 전체를 호스트 기계어로 번역해 캐시에 넣고, 다음부터는 번역본을 바로 실행한다. 블록끼리 직접 점프하도록 이어 붙이면(블록 체이닝) 디스패치 비용도 줄어든다.
대가도 있다. 번역 자체가 시간을 먹으니 한 번만 도는 코드에는 손해이고, 게임이 자기 코드를 고쳐 쓰면(자기 수정 코드) 낡은 번역본을 버려야 한다. QEMU의 번역기 TCG는 번역한 코드가 있는 페이지를 쓰기 금지로 표시해 두었다가, 쓰기가 일어나면 그 페이지의 번역을 무효로 만든다. 사이클 단위 타이밍도 블록 단위로 뭉개지므로, 정확도가 중요한 부분은 인터프리터로 돌리는 절충을 한다.
같은 기법이 게임 밖에서도 쓰인다. 게임큐브·Wii 에뮬레이터 Dolphin은 PowerPC 코드를 x86-64와 AArch64로 JIT 번역한다. Apple의 Rosetta 2는 x86-64 Mac 앱을 Apple 실리콘(Arm)에서 돌리기 위해 실행 전에 앱을 통째로 번역해 두고(AOT), 앱이 실행 중에 만들어 내는 JIT 코드는 그때그때 번역한다. Apple은 Rosetta를 macOS 27까지 범용으로 제공하고 이후에는 옛 게임용 일부만 남기겠다고 밝혔다. 번역기도 결국 컴파일러라서, 중간 표현·레지스터 할당·최적화 같은 말이 그대로 나온다. 그 기초는 컴파일러 노트에 있다.
가상 머신이란
가상 머신은 질문이 다르다. "다른 기계를 어떻게 흉내 내나"가 아니라 "같은 기계 한 대를 어떻게 여러 OS가 서로 모르게 나눠 쓰나"다. 이 일을 하는 소프트웨어가 하이퍼바이저(VMM, 가상 머신 모니터)다. 하드웨어 바로 위에서 도는 타입 1(베어메탈)은 데이터센터용이고, 보통 OS 위의 앱으로 도는 타입 2(호스트형)는 개인 PC에서 다른 OS를 띄울 때 쓴다. 생각 자체는 1960~70년대 IBM 메인프레임(CP-67, VM/370)에서 나왔다.
자주 나오는 질문이 "KVM은 타입 1인가 2인가"다. KVM은 평범한 Linux 위에 올리는 커널 모듈이라 겉모습은 타입 2 같지만, 올리는 순간 호스트 커널 자체가 하드웨어를 직접 나누는 하이퍼바이저가 된다. 그래서 하이퍼바이저 분류에서도 KVM과 FreeBSD의 bhyve를 "호스트 OS를 사실상 타입 1로 바꾸는" 예로 들며 두 종류의 경계가 늘 분명하지는 않다고 적는다. 분류 이름보다 게스트의 특권 동작을 누가 어디서 처리하느냐(아래 트랩 앤 에뮬레이트)를 보는 편이 정확하다.
트랩 앤 에뮬레이트: 게스트 OS의 커널은 자기가 가장 높은 권한이라고 믿지만, 실제로는 한 단계 낮은 권한에서 돌린다. 그러면 페이지 테이블을 바꾸거나 인터럽트를 끄는 특권 명령을 실행하는 순간 CPU가 막고 하이퍼바이저로 트랩된다(운영체제 노트의 시스템 콜·예외와 같은 길이다). 하이퍼바이저는 그 명령이 하려던 일을 그 VM의 가상 상태에 대해 흉내 내고 게스트로 돌려보낸다. 나머지 평범한 명령은 CPU가 그대로 실행한다. 1974년 Popek과 Goldberg는 이것이 통하는 조건을 정리했다. 시스템 상태를 바꾸거나 그 상태에 따라 결과가 달라지는 민감한 명령이 모두 사용자 모드에서 트랩되는 특권 명령이기만 하면 된다.
x86의 문제와 바이너리 변환: 옛 x86은 이 조건을 어겼다. 민감하지만 특권이 아닌 명령이 18개 있었는데, 예컨대 POPF는 사용자 모드에서 실행하면 인터럽트 플래그를 바꾸지 못하고 트랩도 없이 조용히 무시된다. 게스트 커널이 인터럽트를 켰다고 믿는데 하이퍼바이저는 알 길이 없다. VMware는 1999년 무렵 게스트 커널 코드를 실행 직전에 훑어 문제 명령을 안전한 코드로 바꿔 넣는 바이너리 변환으로 이를 돌파했고, Xen은 게스트 커널 소스를 고쳐 그런 명령 대신 하이퍼바이저를 직접 부르게 하는 반가상화(paravirtualization)를 택했다. 앞 절의 JIT 번역과 같은 기술이 같은 종류의 CPU 위에서 쓰인 셈이다.
하드웨어 가상화: 2005~2006년 Intel VT-x와 AMD-V가 나오며 CPU에 "게스트 모드"가 생겼다. 게스트 커널은 게스트 모드의 링 0에서 정상 권한으로 돌고, 하이퍼바이저가 미리 정해 둔 사건(특정 레지스터 접근, 입출력, 인터럽트 등)이 일어날 때만 CPU가 VM 탈출(VM exit)로 하이퍼바이저에 넘긴다. 이후 x86 하이퍼바이저는 대부분 이 기능을 쓴다. KVM은 Linux 커널에 이 기능을 다루는 모듈을 넣어 커널 자체를 하이퍼바이저로 만들고, 장치 흉내는 QEMU 같은 사용자 공간 프로그램에 맡긴다.
메모리 가상화: 게스트 OS도 자기 페이지 테이블로 가상 주소를 "물리 주소"로 바꾸지만, 그 물리 주소는 사실 하이퍼바이저가 준 가짜다. 처음에는 하이퍼바이저가 게스트 표와 자기 표를 합친 섀도 페이지 테이블을 몰래 만들어 CPU에 물렸다. 게스트가 표를 고칠 때마다 트랩을 걸어 섀도를 맞춰야 해서 비쌌다. 그래서 CPU가 두 단계를 하드웨어로 걷는 2단계 주소 변환(Intel EPT, AMD NPT)이 들어왔다. 대신 TLB가 빗나가면 걸음 수가 곱으로 는다. 게스트 표 한 단계를 읽을 때마다 그 표가 있는 게스트 물리 주소를 다시 호스트 표 네 단계로 바꿔야 하기 때문이다. 4단계 × 4단계면 최대 24번이다.
장치 가상화: 게스트에 디스크·랜카드를 주는 방법은 셋이다. 진짜 있는 장치(인텔 e1000 랜카드 등)의 레지스터를 그대로 흉내 내면 어떤 OS든 기존 드라이버로 돌지만, 레지스터를 건드릴 때마다 VM 탈출이 일어나 느리다. 이건 앞의 게임기 에뮬레이터와 똑같은 장치 흉내다. virtio는 처음부터 가상화용으로 약속한 장치라, 게스트 드라이버가 공유 메모리의 링(virtqueue)에 요청을 모아 두고 한 번만 알린다. 가장 빠른 길은 패스스루로, 진짜 장치(또는 SR-IOV로 쪼갠 가상 기능)를 VM에 직접 넘기고 IOMMU가 그 장치의 DMA가 다른 VM 메모리에 닿지 못하게 막는다. 대신 그 VM은 장치에 묶여 다른 서버로 옮기기 어려워진다.
VM의 쓰임
- 서버 통합과 클라우드: 사용률이 낮은 서버 여러 대를 VM으로 한 대에 모으면 하드웨어·전기·공간이 준다. 클라우드의 가상 서버 한 대가 바로 이런 VM 하나이며, 고객은 하드웨어를 보지 않고 VM 크기만 고른다. 자세한 것은 클라우드 노트에 있다.
- 스냅숏과 마이그레이션: VM의 상태는 결국 메모리 내용과 디스크 파일이라 통째로 저장하고 되돌릴 수 있다. 켜진 VM을 다른 서버로 옮기는 라이브 마이그레이션은 VM이 도는 동안 메모리를 복사하고, 그사이 바뀐 페이지만 다시 보내기를 되풀이하다가, 남은 양이 작아지면 아주 잠깐 멈춰 마지막 조각을 옮긴다.
- 격리와 보안 샌드박스: 게스트는 자기만의 커널을 가지므로 하나가 뚫려도 다른 VM으로 번지기 어렵다. 그래서 악성코드를 일부러 실행해 행동을 관찰하는 동적 분석을 VM에서 하고, 스냅숏으로 깨끗한 상태로 되돌린다(리버스 엔지니어링 노트). 반대로 악성코드가 VM 안인지 알아채고 얌전히 구는 일도 있어, 분석 환경은 VM 흔적을 숨기려 애쓴다.
- 개발과 테스트: 다른 OS·다른 버전에서 프로그램을 시험하거나, 망가져도 되는 실습 환경을 만들 때 쓴다.
- 메모리 오버커밋과 같은 페이지 합치기: VM들은 받은 메모리를 다 쓰지 않는 때가 많아, 호스트는 실제 RAM보다 많은 메모리를 VM들에 나눠 주기도 한다(오버커밋). 같은 OS를 띄운 VM끼리는 내용이 똑같은 페이지가 많은데, Linux의 KSM(Kernel Samepage Merging)은 원래 KVM을 위해 만든 기능으로 이런 페이지를 찾아 쓰기 금지된 한 장으로 합치고, 누가 고치려 하면 그때 따로 복사한다(copy-on-write). 다만 모든 VM이 한꺼번에 메모리를 채우면 호스트가 스왑에 빠지므로 여유를 두고 쓴다.
컨테이너와의 차이: 컨테이너는 VM이 아니다. 하드웨어도 커널도 흉내 내지 않고, 호스트 커널 하나를 모든 컨테이너가 함께 쓰며 namespace·cgroup으로 보이는 것과 쓸 수 있는 양만 나눈다. 그래서 훨씬 가볍고 빨리 뜨지만, 커널이 공유되므로 격리는 VM보다 얇다. 두 장점을 섞으려고 컨테이너마다 아주 작은 VM을 붙이는 방식(Firecracker 마이크로VM, Kata Containers)도 쓰인다. 자세한 구조는 컨테이너 노트에 있다.
원격 데스크톱: 화면은 보내고 입력은 받는다
원격 화면 조작은 흉내가 아니라 중계다. 프로그램은 원격 컴퓨터에서 진짜로 돌고, 내 컴퓨터는 그 화면을 받아 보여 주며 내 마우스·키보드 입력을 돌려보낼 뿐이다. 원격 컴퓨터는 ① OS의 캡처 API로 화면을 받고 ② 이전 프레임과 달라진 곳을 찾아 ③ 압축해 보낸다. 내 컴퓨터는 ④ 풀어서 그리고 ⑤ 입력 이벤트를 보내며, 원격 컴퓨터는 ⑥ 그 입력을 OS에 넣는다. 앱은 진짜 입력을 받은 줄 알고 화면을 다시 그리고, 고리가 다시 돈다.
원격 데스크톱은 이 고리를 계속 돈다. 원격 컴퓨터는 화면을 보내고 내 컴퓨터는 입력을 보낸다. 게임 스트리밍·VDI·화상 회의의 화면 공유도 같은 고리다.
대부분의 비용은 화면 쪽에 있다. 1080p 60 fps 화면을 픽셀 그대로 보내면 3.98 Gbps라 보통 인터넷으로는 어림없다. 다행히 사무용 화면은 한 프레임에서 바뀌는 곳이 아주 적고, 게임처럼 다 바뀌는 화면은 영상 코덱이 프레임 사이의 비슷함을 이용해 수백 분의 일로 줄인다. 반대로 입력 이벤트는 몇십 바이트라 대역폭보다 지연이 문제다. 화면이 몇십 ms 늦게 따라오면 마우스가 끈적하게 느껴진다.
프로토콜마다 화면을 보내는 방식이 다르다.
- VNC(RFB): 가장 단순하다. 원격 프레임 버퍼에서 바뀐 픽셀 사각형을 보내고(필요하면 가벼운 압축), 받는 쪽은 그것을 제자리에 붙인다. 창 시스템을 몰라도 되어 어떤 OS에서든 구현할 수 있다(RFC 6143).
- RDP: Windows의 원격 데스크톱 프로토콜. 화면을 그리는 명령과 캐시한 비트맵을 보내는 "그래픽 원격화"에서 출발해 지금은 영상 코덱 모드도 쓴다. 클립보드·드라이브·프린터·오디오를 가상 채널로 함께 실어 나르는 것이 강점이다.
- 원격 플레이·게임 스트리밍: Parsec이나 Moonlight(클라이언트)·Sunshine(호스트) 같은 도구는 GPU의 하드웨어 인코더로 화면 전체를 H.264·HEVC·AV1 영상으로 보내고, 버퍼를 최소로 줄여 지연을 낮춘다. 클라우드 게이밍도 같은 원리로, 게임이 데이터센터의 GPU 서버에서 돌 뿐이다.
- 브라우저 기반: 설치 없이 브라우저에서 원격 화면을 보는 서비스는 대개 WebRTC로 영상과 데이터 채널을 주고받는다. 이 사이트의 P2P 화상 통화도 같은 WebRTC 위에서 카메라 영상을 보낸다.
NAT 넘기: 집이나 회사의 컴퓨터는 대개 공유기 뒤에 있어 밖에서 바로 연결할 수 없다. 그래서 양쪽이 먼저 바깥 서버에 접속해 서로의 공인 주소를 알아낸 뒤 동시에 연결을 시도해 공유기에 구멍을 내거나(홀 펀칭), 그래도 안 되면 중계 서버를 거쳐 데이터를 주고받는다. WebRTC의 STUN·TURN이 이 두 역할이다. 원리는 네트워크 노트에 있다.
VDI와 클라우드 PC: 데이터센터의 VM에 데스크톱 OS를 띄우고 이 원격 화면 프로토콜로 접속하게 하면 가상 데스크톱(VDI)이 된다. 직원은 어떤 기기로든 같은 책상 환경에 들어가고, 데이터는 데이터센터 밖으로 나가지 않는다. 앞 절의 VM(나눠 쓰기·스냅숏·마이그레이션)과 이 절의 원격 화면이 합쳐진 모습이다.
보안: 원격 접속은 그대로 남이 내 컴퓨터를 조작할 수 있는 문이다.
- RDP·VNC 포트를 인터넷에 직접 열어 두지 않는다. 자동화된 비밀번호 대입과 원격 접속 서비스의 취약점(2019년 BlueKeep 같은 인증 전 취약점)이 늘 노린다. VPN이나 원격 데스크톱 게이트웨이 뒤에 두고, 다중 인증(MFA)과 네트워크 수준 인증을 켜고, 업데이트를 미루지 않는다. 바탕 개념은 보안 › 네트워크 보안과 제로 트러스트에 있다.
- 원격 지원 사칭 사기를 조심한다. "컴퓨터가 감염됐다", "환불해 주겠다"며 먼저 연락해 원격 지원 프로그램을 설치하게 하는 수법이 흔하다. 정상적인 회사는 먼저 연락해 원격 접속을 요구하지 않는다. 내가 먼저 찾아간 곳이 아니면 원격 접속 권한을 주지 않고, 이미 줬다면 연결을 끊고 은행과 비밀번호부터 정리한다. 사람을 노리는 공격은 보안 › 사람과 운영에서 다룬다.
언어 가상 머신: 같은 말의 다른 뜻
"가상 머신"이라는 말은 전혀 다른 것에도 쓰인다. JVM, .NET의 CLR, WebAssembly, CPython의 바이트코드 해석기는 실제로 존재한 적 없는 가상의 기계를 정의한다. 컴파일러는 소스를 이 가상 기계의 명령(바이트코드)으로 번역하고, 기계마다 설치된 런타임이 그 바이트코드를 해석하거나 JIT으로 진짜 기계어로 바꿔 실행한다. 배포하는 쪽은 x86용·Arm용을 따로 만들 필요 없이 한 벌만 내면 된다.
구조로 보면 앞의 CHIP-8 인터프리터와 놀랍도록 같다. fetch → decode → execute 루프가 돌고, 자주 도는 코드는 JIT으로 번역한다. 다른 점은 흉내 낼 진짜 기계가 없다는 것이다. 그래서 명세를 설계할 자유가 있어, JVM과 WebAssembly는 레지스터 대신 스택 기계로 정의하고, 타입 검사와 메모리 경계 검사를 명세에 넣어 신뢰할 수 없는 코드를 안전하게 돌리게 했다. 스택 기계 해석기의 동작은 컴파일러 노트의 "인터프리터, 바이트코드 VM, JIT" 절에서 볼 수 있다. 정리하면 시스템 VM(하이퍼바이저가 만드는 것)은 있는 기계를 나누고, 언어 VM은 없는 기계를 만든다.
법과 윤리
이 절은 미국 판례를 중심으로 한 개요이며 법률 조언이 아니다. 나라마다, 사안마다 판단이 다르다.
에뮬레이터 자체는 대개 합법으로 본다. 2000년 미국 제9순회항소법원은 Sony v. Connectix 사건에서, Connectix가 PlayStation 에뮬레이터(Virtual Game Station)를 만들며 분석 목적으로 BIOS를 복사·역어셈블한 중간 복제가 공정 이용이라고 판단했다. 같은 해 Sony v. Bleem 사건에서는 PlayStation 에뮬레이터 Bleem!의 광고가 실제 게임 화면을 비교에 쓴 것을 공정 이용으로 봤다(이 사건은 광고 화면에 관한 것이고 에뮬레이션 자체의 적법성을 따진 것은 아니다).
반면 BIOS·펌웨어와 게임 롬은 콘솔·게임 회사의 저작물이다. 에뮬레이터 코드를 독자적으로 짜는 것과, 남의 BIOS나 게임을 복사해 퍼뜨리는 것은 전혀 다른 문제다. 또 많은 나라가 복제 방지 장치(기술적 보호조치)를 푸는 일 자체를 저작권 침해와 별도로 금지한다. 2024년 닌텐도 스위치 에뮬레이터 Yuzu의 개발사는 닌텐도가 이 조항(DMCA의 우회 금지)을 들어 소송을 내자 240만 달러를 지급하고 개발을 접는 데 합의했다. 판결이 아니라 합의라 선례는 아니지만, 암호 키와 보호 우회가 쟁점이 된다는 것을 보여 준다.
합법적으로 즐길 길도 넓다. 공개 도메인이나 제작자가 무료로 푼 홈브루 게임, 직접 만든 CHIP-8·NES 테스트 프로그램은 마음껏 돌려도 된다. 게임 회사가 직접 내는 복각판과 구독 서비스의 클래식 게임 모음도 내부적으로는 에뮬레이터다. 에뮬레이터는 단종된 하드웨어 위에서만 돌던 게임을 보존하는 수단이기도 해서, 박물관·도서관의 디지털 보존 작업에도 쓰인다.
흔한 오해
- "에뮬레이터와 가상 머신은 같은 말이다" — 에뮬레이터는 다른 기계를 명령 하나하나 흉내 내고, VM은 같은 CPU에서 명령 대부분을 그대로 실행한다(맨 위 표). VM이 거의 제 속도를 내는 이유다.
- "컨테이너는 가벼운 VM이다" — 컨테이너는 커널을 흉내 내지 않고 호스트 커널을 함께 쓰는 프로세스 묶음이다. 가벼운 대신 커널 취약점 하나가 모든 컨테이너에 걸린다(위 컨테이너와의 차이).
- "VM 안에서 돌리면 무엇이든 안전하다" — 하이퍼바이저와 흉내 낸 장치도 코드라 버그가 있으면 게스트가 호스트로 빠져나올 수 있다. 공유 폴더·클립보드·패스스루 장치는 그만큼 경계를 넓힌다. 하이퍼바이저를 최신으로 두고, 의심스러운 파일을 다룰 VM은 이런 연결을 끈다.
- "Rosetta 2는 명령을 하나씩 흉내 내는 에뮬레이터다" — 실행 전에 앱을 통째로 Arm 기계어로 번역해 두는 번역기라, 명령마다 해석하는 인터프리터와 다르다(위 동적 재컴파일 절).
핵심 자료
가상 메모리·트랩·시스템 콜의 바탕은 운영체제 노트, VM 위에 쌓은 서비스는 클라우드 노트, 커널을 나눠 쓰는 격리는 컨테이너 노트에 있다.
| 자료 | 종류 | 요지 | 링크 |
|---|---|---|---|
| Cowgod's Chip-8 Technical Reference | 명세 | CHIP-8의 메모리·레지스터·명령 전부를 한 쪽에 정리했다. 첫 에뮬레이터를 만들 때 가장 많이 펴 보는 문서. | devernay.free.fr |
| NESdev Wiki | 커뮤니티 문서 | NES의 CPU 메모리 맵, PPU·APU 레지스터, 사이클 단위 타이밍을 실험으로 확인해 모은 위키. 이 노트의 NES 수치는 여기서 가져왔다. | nesdev.org |
| Translator Internals (TCG) | 공식 문서 | QEMU가 게스트 코드를 번역 블록으로 잘라 호스트 기계어로 바꾸고, 블록을 잇고, 자기 수정 코드를 감지하는 방법. | qemu.org |
| Popek and Goldberg virtualization requirements | 백과사전 | 1974년 논문의 정리(민감한 명령 ⊆ 특권 명령이면 트랩 앤 에뮬레이트로 가상화 가능)와 x86이 이를 어긴 사례. | Wikipedia |
| Intel 64 and IA-32 Architectures Software Developer Manuals | 명세 | 3권 C편이 VMX(VT-x) 명령, VM 진입·탈출, EPT를 다룬다. 하드웨어 가상화의 원전. | Intel |
| KVM | 공식 문서 | Linux 커널을 하이퍼바이저로 만드는 KVM의 API와 메모리·인터럽트 가상화 문서 모음. | kernel.org |
| Virtual I/O Device (VIRTIO) Version 1.2 | 표준 | 반가상화 장치의 공용 규격. 게스트와 호스트가 공유 메모리의 virtqueue로 요청을 주고받는 방식을 정한다. | OASIS |
| RFC 6143: The Remote Framebuffer Protocol | 표준 | VNC가 쓰는 RFB 프로토콜. 화면을 픽셀 사각형으로 보내고 키·마우스 이벤트를 받는 가장 단순한 원격 화면 구조. | RFC Editor |
| WebAssembly Core Specification | 표준 | 스택 기계로 정의한 가상 명령 집합. 실제 CPU 없이 의미를 정의한 "가상 머신 명세"의 좋은 예. | W3C |
- Cowgod's Chip-8 Technical Reference — devernay.free.fr
- NESdev Wiki — nesdev.org
- Translator Internals (TCG) — qemu.org
- Popek and Goldberg virtualization requirements — Wikipedia
- Intel 64 and IA-32 Architectures Software Developer Manuals — Intel
- KVM — kernel.org
- Virtual I/O Device (VIRTIO) Version 1.2 — OASIS
- RFC 6143: The Remote Framebuffer Protocol — RFC Editor
- WebAssembly Core Specification — W3C
- Cycle reference chart — NESdev Wiki
- PPU — NESdev Wiki
- APU — NESdev Wiki
- CHIP-8 — Wikipedia
- Accuracy takes power: one man’s 3GHz quest to build a perfect SNES emulator — Ars Technica
- About the Rosetta translation environment — Apple Developer
- Xen and the Art of Virtualization(SOSP 2003) — University of Cambridge
- Hypervisor — Wikipedia
- Kernel Samepage Merging — kernel.org
- Live Migration of Virtual Machines(NSDI 2005) — USENIX
- [MS-RDPBCGR] Remote Desktop Protocol: Basic Connectivity and Graphics Remoting — Microsoft Learn
- Remote Desktop Services overview — Microsoft Learn
- Moonlight Game Streaming
- Sunshine — GitHub
- WebRTC
- How To Spot, Avoid, and Report Tech Support Scams — FTC
- The Java Virtual Machine Specification, Java SE 21 — Oracle
- Common Language Runtime (CLR) overview — Microsoft Learn
- dis — Disassembler for Python bytecode — Python 문서
- Sony Computer Entertainment, Inc. v. Connectix Corp. — Wikipedia
- Sony Computer Entertainment America v. Bleem(2000) 요약 — U.S. Copyright Office
- Makers of Switch emulator Yuzu quickly settle with Nintendo for $2.4 million — Engadget