В 1984 году в журнале Scientific American появилась статья Эндрю Дьюдни (Andrew Dewdney) с описанием игры Core Wars. Статья начиналась следующими словами:
«Две программы в своей естественной среде обитания – компьютерной памяти – гоняются друг за другом от адреса к адресу. Иногда они выслеживают врага; иногда закладывают батареи цифровых бомб; иногда они копируют сами себя в другое место памяти, чтобы избежать опасности или останавливаются, чтобы поправить нанесенный противником ущерб. Эту игру я называю Core Wars…
Программы эти, конечно, написаны людьми, но, как только битва начинается, их создателям ничего не остается как только беспомощно наблюдать, как плод его многочасового труда живет или погибает на экране.»
Игра моментально привлекла внимание энтузиастов. По простоте правил она немного напоминает «Жизнь» (о которой в другой раз), а по степени вовлеченности участников в игру намного превосходит ее, потому что требует гораздо большего, чем применение метода проб и ошибок.
Дьюдни рассказывает, что идея создания игры появилась у него после того, как он услышал следующую историю, относящуюся, вероятно, скорее к миру фольклора, чем к реальности:
В некоей корпорации работал программист, который шутя, или руководствуясь недобрыми намерениями, написал программу под названием Creeper, единственным назначением которой было размножаться путем многократного перезаписывания самой себя с компьютера на компьютер, пользуясь корпоративной сетью. Поведение это вскоре вызвало такие проблемы в компании, что все силы сотрудников были брошены на безуспешную борьбу со зловредной программой вручную. Creeper уже начал было одерживать победу, пока кто-то не придумал «тушить пожар огнем». Была создана программа под названием Reaper, которая должна была гоняться за Creeper’ом по сети, беспощадно уничтожая его и, после того, как копий последнего больше не останется, уничтожить саму себя.
История эта видимо является одним из первых свидетельств возникновения компьютерных вирусов и их противников – антивирусов. Да и вся игра Core Wars теснейшим образом связана с вирусами – она закладывает фундамент для обширной и запутанной дисциплины, которую можно назвать “компьютерной вирусологией”. Мы подробно разберем правила игры и несколько программ для того, чтобы читатель чувствовал себя более уверенно, дойдя до главы о настоящих компьютерных вирусах.
Но дело не ограничивается одними вирусами. Игра Core Wars – это великолепный пример[1] как ассемблера – машинного языка, так и виртуальной машины, с которой нам также предстоит встретиться в будущем.
На простом примере вымышленного компьютера MARS вы познакомитесь с несколькими фундаментальными понятиями программирования.
Итак, битва разворачивается внутри вымышленной или, так называемой «виртуальной»[2], машины под названием MARS (Memory Array Redcode Simulator). Redcode – это название машинного языка (или «ассемблера») MARSа, происходящее, вероятно, от сокращения фразы «Reduced code».
Core – это память виртуального компьютера MARS. Название core происходит от того, что в старых машинах память основывалась на магнитных сердечниках (core); отсюда же происходит и название игры – «войны в памяти». Память эта изначально пуста, кроме тех мест, где находятся программы-противники. Программа состоит из команд (или «инструкций») машинного языка Redcode (с которым мы вскоре познакомимся). Одна ячейка памяти содержит одну инструкцию. Виртуальный компьютер MARS поочередно выполняет по одной инструкции каждой из враждующих программ. Как только MARS встречает число на месте, где должна быть выполняемая инструкция одной из программ, процесс прекращается и эта программа объявляется побежденной.
Цель каждого из противников «закидать» врага числовыми бомбами как можно более хитроумным способом, оставаясь при этом неуязвимым. Красота игры – в простоте «машинного» языка, с которым мы сейчас познакомимся, а потом рассмотрим несколько простых примеров.
Для того, чтобы понять инструкции языка Redcode, нам необходимо сначала разобраться в нескольких способах, которыми в компьютерных языках низкого уровня (таких, как настоящий ассемблер или вымышленный Redcode) обращаются к ячейкам компьютерной памяти.
Казалось бы - все просто. Всё, что нам нужно, это давать инструкциям адрес ячейки памяти в качестве аргумента. Например: инструкция ADD 31415, 27182 сложит два числа, находящихся в ячейках памяти по адресам 31415 и 27182. Однако, такой способ исключительно неудобен, и более того - даже вреден! Такой способ адресации (то есть способ, которым обозначаются ячейки памяти в инструкциях) предполагает, что программа вместе со своими данными всегда должна располагаться в строго определённом месте памяти компьютера, что делает её неуклюжей, немобильной и неповоротливой. А что, если другая программа, которая будет работать параллельно с этой, тоже написана таким образом, что ей понадобится обращаться к тем же самым ячейкам памяти по адресам 31415 и 27182 ?! Произойдёт потеря данных и той и другой программы.
Такой способ адресации называется абсолютным, и используется в очень редких случаях. В большинстве случаев применяется способ адресации, называемый относительным. Как следует из его названия, адреса ячеек памяти при таком способе высчитываются относительно какого-либо установленного базисного адреса. Так, например, в компьютере MARS (и следовательно в Redcode) у ячеек памяти вообще нет абсолютных адресов , что, конечно, отличает его от настоящих компьютеров, но облегчает игровые задачи. Все адреса, использующиеся инструкциями Redcode, - относительны. Базисным адресом в этом случае является адрес самой инструкции. Например, если инструкция сложения выглядит как ADD -1,1 - это означает, что будут сложены числа, находящиеся в предыдущей и в следующей за ADD ячейке памяти, так как ячейка, в которой находится ADD имеет адрес 0.
Этот процесс показан на рисунке.
Здесь также необходимо отметить, что результат операции сложения будет положен в ячейку, относительный адрес который обозначен вторым аргументом инструкции, то есть в следующую за ADD ячейку в нашем случае.
Следующий пример работы относительной адресации послужит нам ещё и как важный урок в "общекомпьютерном" смысле. Что это за урок станет понятно после того, как мы познакомимся с инструкцией Redcode MOV.
Эта инструкция (одна из самых важных в игре) перемещает данные внутри памяти MARS'а, а именно: содержимое ячейки памяти, адрес которой задан первым аргументом MOV копируется в ячейку, адрес которой задан вторым аргументом MOV [3].
Рассмотрим пример, который преподнесёт нам небольшой сюрприз. Что делает инструкция MOV 0,3 ? Так как все адреса относительны, содержимое ячейки по адресу ноль (то есть ячейки, содержащей саму инструкцию MOV) будет скопировано в ячейку по относительному адресу 3 (то есть в ячейку, отстоящую от нулевой на две ячейки вперёд). Но что же будет скопировано ? Конечно же, сама инструкция MOV! Этот процесс показан на рисунке.
Из этого простого примера можно сделать очень важный и далеко идущий вывод: в языке Redcode инструкции программы и её данные (такие, как, например, числа) не отличаются друг от друга. Мы только что скопировали не просто число из одной ячейки в другую - нет, мы скопировали целую инструкцию. Более того, эта инструкция скопировала саму себя! Почему это представляет для нас такой интерес, мы увидим очень скоро. А пока, давайте запомним этот вывод - он нам ещё пригодится в главе, рассказывающей о Лиспе.
Однако способы адресации не исчерпываются только относительной адресацией. Более сложный способ носит название косвенной адресации и заключается вот в чём. Вместо адреса ячейки памяти, в которой находится аргумент инструкции Redcode эта инструкция получает адрес ячейки, в которой находится адрес её аргумента. Понять это головокружительное определение можно только на примере. Для начала несколько обозначений.
Косвенная адресация обозначается с помощью знака @. Так, вместо того, чтобы писать MOV 0,3 мы должны будем написать MOV 0,@3 (объяснения следуют). Кроме того, для понимания следующего примера, вам необходимо знать, что числа, находящиеся в ячейках памяти MARS'а обозначаются с помощью так называемой псевдоинструкции DAT. Эта инструкция сама по себе ничего не выполняет, а просто указывает на то, что в ячейке находится какое-то число.
На следующем рисунке представлено действие инструкции MOV 0,@3.
Инструкция MOV в этом примере начинает поиск ячейки назначения с относительного адреса 3, то есть с ячейки, отстоящей от текущей на 2 шага. Там записано число 5 (с помощью псевдоинструкции DAT 5). Это число и будет настоящим (и тоже, конечно, относительным) адресом назначения инструкции MOV, по которому она и скопирует саму себя.
Читатель может быть в недоумении - зачем нужен такой сложный, запутанный механизм адресации ? Терпение! Скоро мы увидим пример, в котором косвенная адресация придётся как нельзя кстати.
Нам осталось рассмотреть ещё один способ адресации, а точнее - способ задания аргумента инструкции. Этот способ называется немедленным. Название подсказывает, что в этом случае инструкция получает не адрес ячейки своего аргумента, а само число. Обозначается такая передача числа с помощью значка #. Так, например, ADD #10,-3 значит, что число 10 должно быть прибавлено к числу, находящемуся в ячейке, отстоящей на две ячейки назад от инструкции ADD. MOV #999, 1 означает, что число 999 должно быть скопировано в следующую за MOV ячейку:
В следующей части мы познакомимся с полным набором инструкций Redcode и подробно разберём работу одной программы.
[1] Слово «великолепный» в данном случае не означает достоинств самого этого языка, а только указывает на то, что простота этого игрушечного ассемблера позволяет познакомиться с основами программирования «низкого уровня» людям, даже далеким от программирования.
[2] Насколько плодотворно понятие «виртуальной машины» (и совсем не в игрушечных целях) нам еще предстоит убедиться в главе о Java.
[3] При этом важно помнить, что содержимое ячейки, из которой копируются данные не изменяется.