Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

에뮬레이션 기초

이 첫 번째 장에서는 에뮬레이션 개발의 개념과 Chip-8을 개괄합니다. 이후의 장들에서는 Rust로 코드를 구현하지만, Rust에 관심이 없거나 예제 없이 작업하는 편을 선호한다면 이 장에서 실제 시스템이 어떤 단계를 수행하는지, 우리가 직접 에뮬레이션해야 하는 것은 무엇인지, 그리고 여러 구성 요소가 어떻게 맞물려 돌아가는지를 소개받을 수 있습니다. 이 주제를 처음 접하는 분이라면, 앞으로 구현할 내용을 이해하는 데 필요한 정보를 이 장에서 얻을 수 있을 것입니다.

Chip-8 게임에는 무엇이 들어 있습니까?

간단한 질문부터 시작합시다. 제가 Chip-8 ROM1 파일을 하나 드린다면, 그 안에는 정확히 무엇이 들어 있습니까? 게임이 화면에서 실행되는 데 필요한 모든 게임 로직과 그래픽 에셋이 들어 있어야 하겠지만, 그 모든 것이 어떻게 배치되어 있습니까?

이것은 기본적이고 근본적인 질문입니다. 우리의 목표는 이 파일을 읽어서 그 프로그램을 실행하는 것이기 때문입니다. 자, 알아낼 방법은 하나뿐이니 Chip-8 게임을 텍스트 편집기(메모장, TextEdit 또는 선호하는 다른 편집기)로 열어 봅시다. 이 예제에서는 이 가이드에 포함된 roms/PONG2 게임을 사용합니다. 아주 특별한 편집기를 쓰는 것이 아니라면, 아마 그림 1과 비슷한 화면이 보일 것입니다.

별로 도움이 되어 보이지 않습니다. 사실, 파일이 손상된 것처럼 보입니다. 걱정하지 마십시오. 여러분의 게임은 (아마도) 멀쩡합니다. 모든 컴퓨터 프로그램은 근본적으로 파일에 저장된 숫자에 불과합니다. 이 숫자들은 거의 무엇이든 의미할 수 있으며, 컴퓨터는 문맥이 있어야만 그 값에 의미를 부여할 수 있습니다. 텍스트 편집기는 영어 같은 언어로 된 텍스트를 보여 주고 편집하도록 설계되었으므로, 자동으로 ASCII 같은 언어 중심의 문맥을 사용하려고 합니다.

이렇게 해서 우리의 Chip-8 파일이 평범한 영어로 작성되어 있지 않다는 사실을 알게 되었지만, 이는 아마 이미 짐작할 수 있었던 일일 것입니다. 그렇다면 실제로는 어떤 모습입니까? 다행히 파일의 원시 내용을 보여 주는 프로그램이 있으므로, 이른바 “헥스 에디터” 중 하나를 사용해 파일의 실제 내용을 표시해야 합니다.

그림 2에서 세로선 왼쪽에 있는 숫자들은 오프셋 값으로, 파일의 시작 지점에서 몇 바이트 떨어져 있는지를 나타냅니다. 오른쪽에는 파일에 저장된 실제 값이 있습니다. 오프셋과 데이터 값은 모두 16진수로 표시됩니다(앞으로 16진수를 꽤 자주 다루게 될 것입니다).

좋습니다. 이제 숫자가 보이니 한결 나아졌습니다. 이 숫자들이 영어 문자에 대응하지 않는다면, 무엇을 의미합니까? 이것들은 Chip-8 CPU를 위한 명령어입니다. 실제로 각 명령어는 2바이트이며, 그래서 스크린샷에서 두 개씩 묶어 놓았습니다.

CPU란 무엇입니까?

익숙하지 않은 분들을 위해 CPU가 정확히 어떤 기능을 제공하는지 잠시 설명하겠습니다. 우리의 목적에서 CPU는 계산을 합니다. 그것이 전부입니다. 여기서 “계산을 한다“라고 말할 때는 덧셈, 뺄셈, 두 수가 같은지 다른지 확인하는 것과 같은 일반적인 항목들이 포함됩니다. 또한 코드의 다른 부분으로 점프하거나 숫자를 가져오고 저장하는 것처럼 게임을 실행하는 데 필요한 추가 연산도 있습니다. 게임 파일은 전부 CPU가 수행할 수학 연산으로 이루어져 있습니다.

Chip-8이 수행할 수 있는 모든 수학 연산에는 각각 대응하는 숫자가 있으며, 이를 옵코드라고 부릅니다. Chip-8 옵코드의 전체 목록은 이 페이지에서 볼 수 있습니다. 에뮬레이터가 다음 명령어를 수행할 때가 될 때마다(이를 틱 또는 사이클이라고도 합니다), 에뮬레이터는 게임 ROM에서 다음 옵코드를 가져와 옵코드 표에 지정된 연산을 무엇이든 수행합니다. 덧셈, 뺄셈, 화면에 그려진 내용 갱신 등 무엇이든 말입니다.

매개변수는 어떻게 할까요? “이제 더할 차례다“라고 말하는 것만으로는 충분하지 않습니다. 실제로 더할 두 수가 필요하고, 계산이 끝났을 때 합을 넣어 둘 장소도 필요합니다. 다른 시스템은 이를 다르게 처리하지만, 연산당 2바이트는 표현할 수 있는 수가 매우 많아서 Chip-8이 실제로 수행할 수 있는 35가지 연산보다 훨씬 많습니다. 남는 자릿수는 옵코드에 추가 정보를 담는 데 사용됩니다. 이 정보의 정확한 배치는 옵코드마다 다릅니다. 옵코드 표에서는 리터럴 16진수를 나타내기 위해 N을 사용합니다. 한 자리 리터럴 값에는 N, 두 자리에는 NN, 세 자리에는 NNN을 사용하며, X나 Y로 레지스터를 지정하기도 합니다.

레지스터란 무엇입니까?

이제 다음 주제로 넘어가겠습니다. 도대체 레지스터란 무엇입니까? 레지스터는 명령어에서 사용하기 위해 1바이트를 저장하도록 특별히 지정된 장소입니다. RAM과 꽤 비슷하게 들릴 수 있으며, 어떤 면에서는 실제로 그렇습니다. RAM은 데이터를 저장하는 크고 주소 지정이 가능한 장소인 반면, 레지스터는 보통 개수가 적고, 모두 이름이 붙어 있으며, 옵코드를 실행할 때 직접 사용됩니다. 많은 컴퓨터에서는 RAM에 있는 값을 사용하려면 먼저 그 값을 레지스터로 복사해야 합니다.

Chip-8에는 프로그래머가 자유롭게 사용할 수 있는 레지스터가 16개 있으며, 이름은 V0부터 VF(16진수로 0-15)까지입니다. 레지스터가 어떻게 동작하는지 보여 주는 예로, 옵코드 표에 있는 덧셈 연산 중 하나를 살펴봅시다. 두 레지스터의 값을 더하는 연산 VX += VY가 있으며, 이는 8XY4로 인코딩됩니다. 8XY4는 옵코드를 패턴 매칭하는 데 사용됩니다. 현재 옵코드가 8로 시작하고 4로 끝난다면 이것이 일치하는 연산입니다. 그다음 가운데 두 자리는 어떤 레지스터를 사용할지 지정합니다. 옵코드가 8124라고 합시다. 8로 시작하고 4로 끝나므로 올바른 연산을 찾은 것입니다. 이 명령어에서는 나머지 두 자리에 해당하는 V1과 V2에 저장된 값을 사용합니다. V1에 5가 저장되어 있고 V2에 10이 저장되어 있다고 하면, 이 연산은 두 값을 더해 V1에 있던 값을 대체하므로 이제 V1에는 15가 들어 있습니다.

Chip-8에는 레지스터가 몇 개 더 있지만, 이들은 매우 특정한 용도로 쓰입니다. 그중 가장 중요한 것 하나는 프로그램 카운터(PC)로, Chip-8에서는 16비트 값을 저장할 수 있습니다. 지금까지 “현재 옵코드“를 막연하게 언급해 왔는데, 우리가 어디까지 왔는지는 어떻게 추적할까요? 우리 에뮬레이터는 프로그램을 실행하는 컴퓨터를 시뮬레이션합니다. 에뮬레이터는 게임의 처음에서 시작하여 옵코드에서 옵코드로 이동하며, 지시받은 대로 명령어를 실행해야 합니다. PC는 현재 처리 중인 게임 명령어의 인덱스를 담고 있습니다. 따라서 PC는 게임의 첫 번째 바이트에서 시작하고, 그다음 세 번째 바이트로 이동하며(모든 옵코드는 2바이트라는 점을 기억하십시오), 이런 식으로 계속 진행합니다. 어떤 명령어는 PC에게 게임의 다른 곳으로 이동하라고 지시할 수도 있지만, 기본적으로 PC는 옵코드 단위로 한 칸씩 앞으로 나아갈 뿐입니다.

RAM이란 무엇입니까?

V 레지스터가 16개 있기는 하지만, Chip-8 같은 단순한 시스템이라 해도 한 번에 16개보다 많은 숫자를 저장할 수 있으면 정말 좋을 것입니다. 바로 여기서 랜덤 액세스 메모리(RAM)가 등장합니다. 여러분의 컴퓨터를 통해 이미 익숙하시겠지만, RAM은 CPU가 원하는 대로 사용할 수 있는 커다란 숫자 배열입니다. Chip-8은 물리적인 시스템이 아니므로, 갖추어야 할 표준 RAM 용량이 정해져 있지 않습니다. 하지만 에뮬레이션 개발자들은 대체로 4096바이트(4 KB)로 합의했으므로, 우리 시스템은 RAM에 8비트 숫자를 최대 4096개까지 저장할 수 있으며, 이는 대부분의 게임이 사용하는 양보다 훨씬 많습니다.

이제 중요한 세부 사항을 살펴볼 차례입니다. Chip-8 CPU는 RAM을 원하는 대로 자유롭게 읽고 쓸 수 있습니다. 그러나 게임의 ROM에는 직접 접근할 수 없습니다. “읽기 전용 메모리“라는 이름을 보면 덮어쓸 수 없으리라는 것은 짐작할 수 있지만, CPU가 ROM을 읽을 수도 없다는 말입니까? CPU는 게임 ROM을 읽을 수 있어야 하지만, 게임이 시작될 때 게임 전체를 RAM2에 복사하는 방식으로 이를 간접적으로 해결합니다. 아주 적은 양의 데이터를 읽기 위해 게임 파일을 몇 번이고 다시 여는 것은 상당히 느리고 비효율적입니다. 그 대신 가능한 한 많은 데이터를 RAM에 복사해 두어 더 빠르게 사용할 수 있도록 하려는 것입니다. 또 하나 주의할 점은, 다소 혼란스럽게도 ROM 데이터가 RAM의 맨 처음에 적재되지 않는다는 것입니다. 대신 512바이트(0x200)만큼 오프셋을 두고 적재됩니다. 즉 게임의 첫 번째 바이트는 시작 시 RAM 주소 0x200에 적재됩니다. ROM의 두 번째 바이트는 0x201에 적재되는 식입니다.

Chip-8은 왜 그냥 게임을 RAM의 처음에 저장하고 끝내지 않을까요? Chip-8이 설계되던 당시에는 컴퓨터의 RAM 용량이 훨씬 제한적이었고, 한 번에 하나의 프로그램만 실행할 수 있었습니다. 처음 0x200바이트는 Chip-8 프로그램 자체가 실행되는 공간으로 할당되었습니다. 따라서 게임이 여전히 그 개념에 맞추어 개발되므로, 현대의 Chip-8 에뮬레이터도 이 점을 염두에 두어야 합니다. 사실 우리 시스템도 그 빈 공간을 조금 사용하게 되는데, 이에 대해서는 나중에 다루겠습니다.


  1. ’ROM’은 “읽기 전용 메모리(Read-only memory)“를 뜻합니다. 제조 시점에 고정적으로 기록되며 컴퓨터 시스템이 수정할 수 없는 종류의 메모리입니다. 이 가이드에서는 “ROM 파일“과 “게임 파일“을 같은 의미로 혼용합니다. ↩

  2. 따라서 Chip-8 게임의 최대 크기는 4 KB이며, 이보다 크면 메모리에 전부 불러올 수 없습니다. ↩