Часть 1: введение
17 апреля 2012 года Джордан Мекнер опубликовал исходный код Prince of Persia.
Даже несмотря на то, что это версия для Apple II, написанная на ассемблере процессора 6502, было очень приятно погрузиться в код этой легендарной игры. Как обычно, меня ждало множество программных интересностей.
Очевидно слабая для программирования игр среда Apple II на самом деле была фундаментом несравнимых ни с чем инноваций и творчества: самомодифицирующийся код, внутренний загрузчик, умный формат гибких дисков и смещение таблиц поиска. В каждом своём модуле Prince Of Persia хранит сокровища инженерной мысли.
Чтение исходников позволило мне не только узнать больше о процессе разработки игр в 80-х, но и снова вызвало чувство признательности за те вещи, которые сегодня считаются естественными.
Как обычно, я вёл подробные записи и на их основе создал эту статью. Надеюсь, она вдохновит других на чтение исходного кода и усовершенствование своих навыков разработки.
Благодарности: я хочу поблагодарить Miles.J с 6502.org и Роланда Густафссона (Roland Gustafsson) (автора RWTS18) за то, что они поделились со мной своими знаниями.
С чего начать?
Исходный код доступен в репозитории на GitHub и его можно скачать одной командой:
git clone git://github.com/jmechner/Prince-of-Persia-Apple-II.git
Интересная часть находится в папке /Prince-of-Persia-Apple-II/01 POP Source/Source/
, которая содержит игровой движок, составленный из множества файлов .S
.
Это первое, чего не было у программистов в те времена: высокоуровневых языков с качественными компиляторами. Чтобы добиться хорошей скорости, разработчикам приходилось работать непосредственно с «железом» на языке ассемблера 6502. Именно из него и состоят эти файлы .S
.
Согласно книге Джордана Мекнера Making of Prince of Persia, в PoP использовался ассемблер Merlin.
Одна из хороших возможностей Merlin — директива ORG
, позволяющая сообщить ассемблеру, где инструкции будут загружены в ОЗУ: в PoP ORG
находится в начале каждого файла.
Интересный факт: директивы ORG
на самом деле были всего лишь указаниями. В Apple II не было ни операционной системы, ни компоновщика или загрузчика: разработчик сам должен был каким-то образом ухитриться перенести инструкции с гибкого диска в нужное место.
Второй важный момент, который нужно понять для восприятия PoP в целом, — способ обмена информацией между модулями. Поскольку в то время не было межфайлового компоновщика (и конечного исполняемого файла, только фрагменты), модули должны обмениваться данными, выполняя переход в пустоту… в которой должны были находиться другие модули.
Пример этого мы можем видеть в загрузчике BOOT.S
в строке 138:
jmp $ee00
И это довольно загадочно.
Чтобы проследовать за потоком инструкций, нам нужно выяснить, что находится в $ee00
. С помощью небольшой команды grep можно увидеть org = $ee00
в HIRES.S
и перенос.
Так как компоновщиков не было, разработчикам 80-х нужно было знать схему памяти движка и понимать ограничения ОЗУ. Такое сейчас встречается довольно редко, потому что большинство профессионалов полагается на сборщики мусора или использование векторов с автоматическим изменением размера.
Ассемблер 6502
Я расскажу о нём, чтобы представить общую картину и объяснить, как выполняться переход между подсистемами. Для понимания содержимого каждого модуля нужно немного познакомиться с центральным процессором. К счастью, 6502 — это простой 16-битный процессор всего с тремя регистрами, способный адресовать 64 КБ ОЗУ (переключение банков расширяло этот объём до 128 КБ), без функций вычислений с плавающей запятой и без сегментации, имеющий всего 56 инструкций.
Рисунок показывает, насколько просто всё было:
- Серые блоки: 64 КБ ОЗУ, в том числе 256 байт, выделенные под стек.
- Синие блоки: 3 регистра — X,A,Y (8 бит).
- Зелёный блок: один 16-битный программный счётчик.
- Красный блок: один 8-битный указатель стека.
- Жёлтый блок: несколько флагов STATUS.
Большинство операций предназначено для загрузки и хранения регистра, регистры X и Y имели простые инструкции, а накопитель (A) немного более сложен, но в целом довольно прямолинеен и хорошо задокументирован.
Две отличные книги, которые, с сожалению сегодня сняты с печати: Apple II Reference Manual и Inside the Apple IIe.
Начальная точка
В 80-е не было IDE и исходных файлов C с методом main
, поэтому невозможно сказать, где начиналась программа. Чтобы понять, с чего начать чтение, нам нужно знать, как загружался Apple II:
- Чтение первого байта X сектора 0 на дорожке 0 с гибкого диска.
- Загрузка X секторов с дорожки 0 в ОЗУ по адресу
$800
. - Начало выполнения кода по адресу
$800
.
Чтобы найти начальную точку, нам нужно определить, какому модулю указано запускаться в $800
с помощью поиска директивы ORG
:
fabiensanglard$ find . -name "*.S" -exec grep -H "org = $800" {} ;
./01 POP Source/Source/BOOT.S:org = $800
./02 POP Disk Routines/CP.525/POPBOOT0.S:org = $800
./03 Disk Protection/POPBOOT0.S:org = $800
./04 Support/MakeDisk/DRAZ/DRAZ.S:org = $800
./04 Support/MakeDisk/S/BOOT.S:org = $800
Итак, начальная точка находится в BOOT.S
: этот файл, как мы увидим позже, содержит загрузчик PoP.
Рекомендуемое чтение
Джордан Мекнер опубликовал свои журналы (их можно приобрести как книгу или pdf). Это очень точное описание того, что должны пройти разработчики игр при создании игры: сомнения, давление, отчаяние, надежда и восторг.
Чтобы разобраться больше с технической частью, изучите заметки разработчика, предназначенные для разработчиков на Atari/Amiga/PC (работа над этими портами не очень задалась, но я не буду умалять значение журнала «Making Of»).
Несколько видео легендарного процесса ротоскопирования и знакомства с Тиной Леду (Tina LaDeau) (Принцессой), упомянутой в дневнике Джордана.
Примечание: Джордан Мекнер участвовал в AMA на Reddit в январе 2013 года, его можно прочитать здесь.
Часть 2: загрузчик
Для экономии ОЗУ и вычислительных ресурсов разработчики игр на Apple II не использовали операционную систему, поставлявшуюся с компьютером. Поэтому им приходилось писать собственный загрузчик: небольшую программу, которая загружала движок игры с гибкого диска в ОЗУ.
Очень помогали в этом процедуры, находящиеся в ПЗУ Apple II: RWTS16 был набором инструкций, который управлял гибким диском и позволял считывать/записывать 16 секторов по 256 байт на дорожку из 35 дорожек. В сумме это составляло 140 КБ на диск.
Но в играх Brøderbund не использовался RWTS16. Они нашли формат получше: RWTS18, обеспечивавший больший объём данных и проявивший себя как надёжный механизм защиты от копирования.
Традиционный загрузчик
Прежде чем сосредотачиваться на различиях, перечислю то, что обычно делается в индустрии видеоигр:
При запуске компьютера взаимодействуют три компонента:
- Гибкий диск.
- Процедура загрузки в ПЗУ Apple.
- Процедуры считывания гибкого диска (RWTS16) в ПЗУ Apple.
При запуске Apple II:
- Компьютер устанавливал свой указатель инструкций на инструкции загрузки в ПЗУ Apple
- Процедуры загрузки Apple использовали RWTS16 для загрузки секторов с гибкого диска в ОЗУ по адресу
$800
. - Эти секторы содержали загрузчик игры.
- Процедуры загрузки Apple II затем переходили к
$800
.
С этого момента загрузчик игры отвечал за загрузку движка игры с гибкого диска в ОЗУ:
- С помощью процедур RWTS16 Apple II...
- Он переносил движок игры с гибкого диска в ОЗУ...
- А затем переходил к движку.
RWTS16
RWTS16 был форматом гибких дисков, поставлявшимся с Apple II. Он подробно описан в блестящей книге "Beneath Apple DOS". Если вкратце, то он состоял из набора процедур в ПЗУ, управлявших дисковым приводом для записи и чтения 16 секторов на дорожку.
Секторы по 256 байт записывались на дорожку следующим образом:
Поскольку приводам не хватало точности, между секторами необходимо было оставлять огромные зазоры, чтобы обеспечить стирание/перезапить без наложений:
Как видно на рисунке ниже, головка привода могла выполнить запись немного до или после «идеального» положения. Зазоры компенсировали эти неточности:
RWTS18
RWTS18 был форматом гибких дисков, созданным Роландом Густафссоном для Brøderbund. Он совершенно отличался от RWTS16 тем, что не только обеспечивал больший объём хранения, но ещё и значительно усложнял жизнь хакерам.
Повышение ёмкости RWTS18 обеспечивалось важным наблюдением: при разработке игр в основном использовалось чтение данных, а не запись. RWTS18 почти полностью отказался от концепции секторов: он записывал дорожки целиком и считывал сектора гораздо большего размера.
Такая схема позволила избавиться от большинства зазоров, которые RWTS16 приходилось располагать между секторами: между секторами находились только XXX байт самосинхронизации.
В результате RWTS18 обеспечивал хранение 768 байт в каждом из 6 секторов и 157 КБ на диск вместо 140 КБ, доступных у RWTS16.
Но повышенная ёмкость была не единственным преимуществом RWTS18. Он также создавал механизмы защиты от копирования:
- Все изготовленные диски имели физические различия: данные кодировались по-разному благодаря таблице полубайтов, загружаемой RWTS18 в процессе выполнения.
- Разные Prolog и Epilog секторов сбивали с толку программы копирования дисков.
Примечание: RWTS18 не отказался от секторов полностью по двум причинам:
- Максимальная латентность начала считывания дорожки составляла 1/6 оборота диска.
- Разработчикам всё равно нужно было немного фрагментарности (полная дорожка может содержать слишком много данных).
Примечание: Роланд Густафссон добавил свой комментарий:
По спецификации при считывании диска 18 секторов могли переноситься в 18 разных страниц ОЗУ, а не только быть просто последовательными считываниями 18 секторов ($1200 байт) в любом получившемся порядке. Этот способ сейчас используется для жёстких дисков, кажется, это называется «считыванием вразброс» (scatter-read) (а мне тогда это просто показалось отличной идеей!).
Использованный мной самомодифицирующийся код позволял выполнять 4 цикличных считывания вместо 6, экономя 2 цикла на чтение/запись. Кроме того, он позволял «отбрасывать» операции чтения, когда не обязательно нужны были каждый сектор/страница. Существовал API, управлявший всеми нужными вариациями. Циклы чтения и записи внутренних дорожек были чрезвычайно плотными. Фактически, процедуры записи должны были предварительно обрабатывать данные немного заранее, чтобы выполнить запись за один оборот. Если не ошибаюсь, запас был всего в 20%.
Интересный факт: Роланд Густафссон был настолько влюблён в Apple II, что сделал для своего автомобиля номерной знак "D5 AA 96
" (это соответствует трёхбайтному маркеру пролога RWTS16.
Интересный факт: исходный код RWTS18 был написан с помощью ассемблера LISA, имевшего размеченный формат файлов, а не просто текстовые файлы. Роланд планирует опубликовать исходный код, как только сможет извлечь его. Если вы знаете, как это сделать, помогите ему.
Интересный факт: RWTS18 было так сложно расшифровать, что возникли странные легенды: раньше считали, что для создания дисков RWTS18 необходимы медленные дисковые приводы.
Интересный факт: хакерам всё-таки удалось взломать Prince Of Persia… но для дальнейшего распространения им пришлось выполнить обратную разработку игры и записать её на три диска, отформатированного RWTS16. Оригинальная версия PoP продавалась на двух дисках с RWTS18. Даже эмуляторы Apple II могут запускать только взломанную версию Prince Of Persia!
Загрузчик Prince Of Persia
Теперь у нас есть вся информация, чтобы начать разбираться с загрузчиком PoP:
PoP начинала загружаться обычным образом: первая дорожка была отформатирована RWTS16, поэтому процедуры загрузки Apple II загружали загрузчик PoP в ОЗУ.
Затем загрузчик PoP с помощью RWTS16 загружал процедуры RWTS18 с оставшейся части дорожки 0.
Потом загрузчик использовал RWTS18 для загрузки движка игры с других дорожек, отформатированных RWTS18.
Загрузчик переходил к игровому движку, который тоже использовал процедуры RWTS18 для загрузки ресурсов игры.
Интервью с Роландом Густафссоном
Роланд согласился ответить на несколько вопросов, вот наше интервью:
Фабьен Санглар: Привет, Роланд, можешь немного рассказать о себе: когда ты родился, где вырос, любил ли школу и о том, как ты стал программистом?
Роланд Густафссон: Я родился в Швеции, но всю жизнь прожил в Калифорнии. Мне не очень нравилось в школе, однако я выпустился из старших классов и в то время уже программировал и неплохо жил в свои 17 лет. (Вот почему я не пошёл в колледж: в университете преподавание компьютерных наук никак не было связано с новыми интересными машинами, которые назывались «персональными компьютерами». Это было в конце 70-х.)
ФС: Как получилось, что 80-х ты стал работать на Brøderbund? Как долго ты там работал? Как к тебе пришла идея RWTS18, почему ты его написал? Над чем ещё ты работал в Brøderbund, кроме RWTS18?
РГ: Я никогда не был сотрудником Brøderbund, не имел настоящей работы, всегда был фрилансером.
Идея RW18 родилась из необходимости. У нас были продукты Bank Street (после Bank Street Writer), которые не вмещались на стандартный 16-секторный диск. Раньше мне уже приходилось делать фокусы, поэтому меня спросили, можно ли придумать что-нибудь для хранения большего объёма данных. Я годами работал над защитой от копирования, нестандартно мыслил и полностью понимал возможности привода Disk II. Немного подумав, я пришёл к наиболее эффективному способу, который пришёл в голову. Это случилось в самолёте на пути в Японию, поэтому у меня было много времени на бумагу и карандаш. Мой друг Кори Косак (Corey Kosak) был уверен, что нашёл способ хранить 19 секторов (теоретический предел), но в нём использовались такие затратные математические вычисления, что слабый 6502 с ними не справился бы. Математика для управления полубайтами использует base 64, при этом можно было хранить больше данных, если бы была возможность простой работы с base 78 или чем-то подобным, не помню. Математика немного вводила в ступор. А работать с base 64, само собой, двоичным системам было очень просто.
ФС: Как вам работалось с Brøderbund (мне интересны расписание, зарплата, такие же страстные люди, как вы)? Можете сравнить тот опыт с современными условиями работы?
РГ: У меня не было настоящей работы, я её не хотел, не люблю офисы.
ФС: Вы вообще работали над Prince of Persia? Помогали ли вы разработчикам игры или закончили работу на том, что передали исходники на ассемблере и руководство?
РГ: Я работал непосредственно с Джорданом Мекнером над реализацией RW18 и защиты от копирования. У нас часто были интересные моменты, когда мы придумывали окольные пути усложнения копирования. В некоторых случаях игра, казалось, работала, но в результате в неё невозможно становилось играть. В случае большинства других игр я принимался за работу после их завершения и добавлял защиту от копирования. Исключением стала программа RW18, которую нужно было реализовать на этапе разработки! Я написал простое руководство, чтобы её можно было легко использовать.
ФС: В книге «Hackers: Heroes of the Computer Revolution» («Хакеры: герои компьютерной революции») есть глава, посвящённая борьбе Spiradisc и Sierra On-Line против пиратства. Оглядываясь назад, можете ли вы назвать RWTS18 эффективным способом защиты Brøderbund от пиратства игр? Изучали ли вы механизмы защиты от копирования других издателей (Spiradisc)? Если да, то что вы о них думаете?
РГ: Единственная защита от копирования, которую я внимательно изучил — это кассетная версия Microsoft Flight Simulator. Тогда я осознал, что существует защита от копирования. С того момента я разрабатывал свою систему защиты и редко изучал то, что делают другие. Я сам пришёл к концепции защиты спиральным диском и использовал её во многих игра Brøderbund и Gebelli. Я не знал, додумался ли кто-нибудь ещё до техники с 18 секторами, но слышал много лет спустя, что кто-то скопировал или сам дошёл до этого способа. Но я по-прежнему в этом не уверен. Точно знаю, что RW18 изобретён мной.
ФС: Продолжим тему борьбы с программным пиратством: следили ли вы за техниками ваших противников для усовершенствования RWTS18? Противники иногда отдают дань мастерству тех, с кем борются: вас когда-нибудь впечатляло то, что способны были сделать пираты (например, пиратская копия PoP на трёх дисках RWTS16)?
РГ: Да, я как минимум изучал Locksmith, Nibbles Away и другое ПО для копирования, а также читал темы о «железе». Я добавил код для защиты от аппаратного копирования, которое было настолько хорошо сделано, что Apple не могла догадаться, как защититься от него даже в тогда ещё не объявленном и тайном Apple //e. Поэтому они принесли этот компьютер ко мне домой и я заставил защиту работать. Я скрывал Apple //e довольно долгое время, пока он не был объявлен и выпущен.
Честно говоря, я считаю, что мысль о создании трёхдисковой версии для двухдисковой игры с RW18 была глупой и не очень впечатляющей. Им было лучше создать систему копирования, полностью воссоздававшую этот формат RW18 и просто делать точные копии! Игра была бы намного лучше, быстрее и т.д…
ФС: Какой хак вы считаете величайшим за всю историю разработки ПО?
РГ: Ничего не приходит в голову. Могу сказать, что когда кто-нибудь находит удивительно умное решение сложной проблемы, требующее изобретательского
ФС: Какую игру вы любите больше всего?
РГ: Хоть я и не играл уже очень давно, могу назвать оригинальную версию Tetris для GameBoy «сетевой» игры, когда игроки сражаются друг против друга. Я очень упорно играл в неё в Brøderbund.
ФС: Есть ли у вас образец для подражания, личный герой в компьютерной индустрии?
РГ: То, что Стив Возняк настоял на публикации исходного кода ПЗУ оригинального Apple II, стало чрезвычайно важным для моего обучения. Однажды я встретил его на встрече пользователей Apple в Сан-Франциско, мне удалось поговорить с ним и получить подробную информацию о системе Disk II, которая позволила мне заняться защитой от копирования.
ФС: Каким моментом вы гордитесь больше всего как инженер ПО?
РГ: Всегда, когда мне удаётся превзойти ожидания, «достать кролика из шляпы», это бывают потрясающие моменты. Я по-прежнему считаю RW18 одним из моих лучших трюков.
ФС: Над чем вы работаете сейчас?
РГ: Я технический директор Oceanhouse Media, мы занимаемся разработкой мобильных приложений. Apple совершила настоящий прорыв с iOS и разработкой приложений! Мы ведём разработку и для других мобильных рынков, и нам многое ещё предстоит совершить. Да, я по-прежнему большой фанат Apple.
Рекомендуемое чтение
"Beneath Apple DOS" — это отличная книга, описывающая внутреннее устройство компьютеров Apple II. Она очень полезна для понимания RWTS16 и трансляции 6+2.
В Hackers: Heroes of the Computer Revolution есть глава о Sierra On-Line и о сложных взаимоотношениях с их вундеркиндом-антихакером, разработавшим Spiradisc — Марком Дюшано (Mark Duchaineau).
Теперь, когда у нас есть все нужные знания, мы можем на самом деле погрузиться в код и разобраться, где находится каждый из файлов в невероятном хаосе, который представляет собой структура ОЗУ Apple II.
Часть 3: подробнее о загрузчике
Подробно прокомментированную версию исходного кода можно найти на GitHub.
Вот схема этой печально известной ОЗУ, которая будет уточняться ниже:
BOOT.S
BOOT.S
начинается с вывода $01
в поток инструкций (строка:21). Это заставляет встроенное ПО Apple автоматически загрузить один сектор с дорожки 0 в ОЗУ по адресу $800
и перейти туда:
После установки нескольких программных переключателей BOOT.S
использует RWTS16 для загрузки серии секторов в ОЗУ с помощью таблицы смещения (строка:79). Выяснилось, что в этих секторах содержит остальную часть процедур BOOT.S
и RWTS18:
После перемещения процедур RWTS18 в другой банк по адресу $D000
(для удобства использования), он использует RWTS18 для загрузки целой дорожки 1 по адресу $E000
(строка:136) и переходит к загадочному $ee00
.
HIRES.S
Мы понятия не имеем, что находилось на дорожке 1 и должны ли бы эти данные находиться по адресу $ee00
.
Но можно использовать grep, чтобы выяснить это:
fabiensanglard$ find . -name "*.S" -exec grep -H "org = $ee00" {} ;
./01 POP Source/Source/HIRES.S:org = $ee00
./01 POP Source/Source/MOVER.S:org = $ee00
Немного изучив код, мы выяснили, что нам интересен HIRES.S
.
HIRES.S
в начале содержит таблицу переходов, которая переносит нас в ещё один загадочный адрес: $f880
(строка:13).
MASTER.S
И мы снова не знаем, что должно находиться по адресу $f880
.
Но можно опять применить grep, чтобы это выяснить:
fabiensanglard$ find . -name "*.S" -exec grep -H "org = $f880" {} ;
./01 POP Source/Source/MASTER.S:org = $f880
./04 Support/MakeDisk/S/MASTER.S:org = $f880
./04 Support/MakeDisk/S/MASTER525.S:org = $f880
Итак, по адресу $f880
находится содержимое MASTER.S
. Мы можем отредактировать таблицу распределения памяти:
MASTER.S
имеет следующее содержимое:
jmp FIRSTBOOT
jmp LOADLEVEL
jmp RELOAD
jmp LoadStage2
jmp RELOAD
jmp ATTRACTMODE
jmp CUTPRINCESS
jmp SAVEGAME
jmp LOADGAME
jmp DOSTARTGAME
jmp EPILOG
jmp LOADALTSET
На этом этапе движок игры загружен в ОЗУ и готов к загрузке легендарного экрана заставки DOUBLE-HIGH:
[Прим. пер.: на этом статья несколько преждевременно завершается. Автор спрашивал в комментариях, интересен ли будет дальнейший анализ, получил множество положительных ответов, но почему-то дальше не продвинулся.]
Автор: PatientZero