Вход в Зоопарк ручных компьютеров
Вход > Палеонтологический музей > Маленькие истории > Copilot: проект и разработка
--

Грег Хьюджилл greg@hewgill.com
Copilot: проект и разработка

Целью этого проекта, как нетрудно догадаться, была разработка программы, эмулирующей Pilot. Разработка велась без документации, кроме справочника по набору команд DragonBall, который использовался в устройстве, которое стало потом известно как Palm Pilot. Palm Pilot был ручным ПК с ЦП на базе 68000, сенсорным ЖК-экраном, 512 Кбайт ПЗУ, последовательный порт и несколько кнопок на корпусе. Я собирался попробовать эмулировать его программно. Чтобы рассказать этот рассказ как следует, я должен начать с первого знакомства с Pilot.

Открытие Pilot
Я обнаружил Pilot впервые на рабочем столе одного коллеги, где-то в июне 1996. Он описывал мне его как машинку, подобную Newton, но компактнее. Он показывал Graffiti и предложил попробовать, дав Pilot и карточку, на которой были изображены символы Graffiti. Я был поражен. Несколько дней спустя я приобрел Pilot 5000 в местном магазине, и, как разработчик программ, начал искать информацию о средствах разработки для этой небольшой драгоценности.

Немного информации нашлось на Web-узле Pilot, где, правда, не было инструментальных средств для Windows. Все, что я обнаружил, было Metrowerks CodeWarrior for Pilot, который работал только на Macintosh. У меня не было Macintosh. Итак, я купил единственную программу, которая имела какое-то отношение к разработке для Pilot — Conduit Software Development Kit (я до сих пор не пробовал сделать свой кондуит).

К счастью, существовал другой разработчик, который имел Pilot. Но не имел Macintosh. У Даррина Массены был Web-сайт (www.massena.com/darrin/pilot) и он разработал программу, которую лучше всего можно назвать «инструмент вскрытия». Она называлась PilotHack, представляла собой программу, которая работала как монитор аппаратных средств ПК, для которой ОС была совсем не обязательной. PilotHack показывал каждый байт ОЗУ и ПЗУ в шестнадцатеричном виде и ASCII. Эта программа до сих пор находится на моем Pilot сегодня.

Даррину удалось скомпоновать несколько маленьких программ для Pilot, используя Microsoft Visual C 4.0 Cross-Development Edition for Macintosh. Несмотря на огромное количество проблем, компилятор генерировал двоичный код программы, который мог загружаться в Pilot. Он также скомпоновал PILA, PILot Assembler. PILA работал в Windows и создавал исполнимые файлы Pilot из исходных текстов на ассемблере 68000.

Наконец, Даррин опубликовал статью «Разработка программ для Pilot в среде Windows», где описывал, как должен был бы выглядеть современный Windows SDK для Pilot. Этот SDK включал редактор, компилятор, ассемблер, редактор диалогов, компоновщик, средства управления проектом, отладчик, эмулятор, образцы, заголовочные файлы, документацию, руководства и среда разработки для Windows, которая могла бы это скомпоновать. Понимающий, что для единственного разработчика создание такого SDK было бы непосильной задачей, он предложил другим разработчикам присоединиться к нему.

Когда я прочитал список Даррина, я решил, что мне будет интересно разработать эмулятор. В конце концов, у меня был некоторый опыт с эмуляторами (я когда-то написал эмулятор Apple ][ ), я немного знал ассемблер 68000 и потенциальный результат, если проект имел бы успех, был просто слишком большим, чтобы упустить такую возможность.

Первые шаги
Идея Copilot родилась. В то время программа не называлась Copilot, я назвал ее так значительно позже; в то время она разрабатывалась как Pilotsim. Перед Pilotsim у него не было имени, это было просто модифицированный эмулятор UAE.

Первое, что нужно сделать, когда вы разрабатываете эмулятор — раздобыть код, который собираетесь исполнять. В случае Пилота, это были 512 Кбайт ПЗУ, которые содержали ядро Pilot, ОС, библиотечные функции, прикладные программы. Даррин добавил в PilotHack функцию загрузки произвольных 16-Кбайт блоков данных в процессе HotSync, запустив ее 32 раза и перемещая 16-Кбайт окно по адресному пространству ПЗУ, я подготовил образ ПЗУ на ПК. С 512-Кбайт файлом PILOT.ROM в руках, можно было найти первую команду и начать эмуляцию.

Чтобы запустить двоичный код для ЦП, отличного от того, на котором сейчас работает ПК, вам нужна программа, которая называется эмулятором. В моем случае, целью было выполнить инструкции процессора 68000 на ЦП серии x86. Я мог разработать эмулятор процессора серии 68000, несомненно, правда. Эта работа уже была сделана. Без долгих поисков в Интернет, я наткнулся на UAE, Un*x Amiga Emulator. Amiga использует ЦП того же семейства, 68000, что и Pilot, поэтому эмуляторы Amiga и Pilot в этом смысле, имеют что-то общее. Я извлек эмулятор ЦП из UAE, «вырезав» всю поддержку для заказных графических микросхем и других аппаратных средств, которые не присутствовали в Pilot. Шмидт Бернд, автор UAE, любезно позволил мне использовать исходные тексты ядра ЦП 68000 в Copilot.

Процессор MC68328 DragonBall, использованный в Pilot, имеет очень гибкую архитектуру памяти. В нем есть много конфигурационных регистров, которые задают многие его параметры, например, 8-бит или 16-битовый размер машинного слова, адрес банка памяти и флаги защиты от записи. Почти все эти регистры инициализируются в процессе начальной загрузки и не изменяются. Вместо эмуляции регистров, первый Copilot «знал» о корректной конфигурации памяти Pilot и не реагировал на параметры конфигурации фактической памяти, которые пыталось задать ОС PalmOS из ПЗУ.

После начальной загрузки, ПЗУ Pilot начинает инициализировать аппаратные средства. В одной из подпрограмм инициализации, код в ПЗУ ждал некоторое время истечения таймаута. В этом месте, я не знал точно, что он пытался инициализировать. Тщательная трассировка кода показала, что она ожидала в цикле какого-то внешнего события. Покопавшись с руководстве по DragonBall, я обнаружил, что ОС ждала один из аппаратных таймеров. Поскольку моя эмуляция просто «съедала» значения, которые были заданы регистрам в процессе инициализации, без попыток корректной их эмуляции, код ждал события, которое никогда бы не произошло.

Разработка нормального таймера была одной из многих функций, которые я изучил по руководству DragonBall. В целом, процедура разработки оказалась примерно такова:

  • трассируем код, пока программа не потерпит крах (обычно это проявляется, когда что-то должно было произойти в аппаратных средствах);
  • исследуем, какие регистры были задействованы и ищем для чего они используются в DragonBall;
  • читаем руководство по Dragonball, чтобы понять, что пытался сделать код из ПЗУ и как аппаратные средства реагируют на действия ПЗУ;
  • переписываем исходные тексты, чтобы корректно эмулировать аппаратные средства.

Обратите внимание мой комментарий «корректно» [в оригинале, «точно так, как необходимо», прим. пер.] эмулируем Dragonball. Эмуляции Dragonball в Copilot далеко до полной эмуляции Dragonball. В целом, корректно работают только те регистры, которые необходимы для правильной работы ПЗУ Pilot. В некоторых случаях, реализована поддержка только одного или двух управляющих битов. Для всех не задействованных регистров Dragonball, заданы точки прерывания в эмуляторе.

Я использовал их в процессе разработки, чтобы обнаруживать, когда ПЗУ пытается работать с регистром, который я еще не эмулировал (например, это та причина, почему Copilot выдает сообщение об ошибке с исключением 03H при запуске ПЗУ с PalmOS 2.0 на версиях Copilot до 2.0, PalmOS 2.0 задействует несколько регистров, для управления подсветкой и для этих регистров не было эмуляции в более ранних версиях Copilot, исключение 03H — аппаратная точка прерывания для ЦП Intel).

Вплоть до этой точки, единственный метод обнаруживать, что код ПЗУ что-то делает — трассировка инструкции за инструкцией с помощью отладчика. Не было никакого вывода на экран. Так что следующая задача выглядела как извлечение образа экрана Pilot из области видеоОЗУ и отображение его на экране Windows. Copilot развивался в многопоточную программу: процесс эмуляции ЦП работал, исполняя инструкции, и не мог проверять наличие изменений в области видеопамяти. Был создан новый поток, который проверял изменения в видеопамяти и копировал их на экран Windows, когда содержимое видеоОЗУ изменялось.

Проблема ввода
Наконец, после многих часов трассировки, кодирования, отладки, чтения руководство по Dragonball, снова трассировки и, несомненно, изрядной толики везения, на экране моего ПК появилась надпись Welcome to Pilot. Первый видимый успех. Отступать было некуда.

Конечно, мое возбуждение быстро прошло, когда я понял что я так и смотрю на Welcome, эмулятор не переходит к процедуре, которую после этого запускает реальный Pilot -программе калибровки экрана. Как оказалось, была пара проблем. Из-за ошибки в процедуре эмуляции таймера, код в ПЗУ работал исключительно медленно, что порождало ошибку, связанную с таймаутами. Когда я наконец разобрался с ними, появился экран калибровки пера.

Но калибровка пера представляла большую проблему. Эмулятор, как можно догадаться, не предпринимал никаких попыток использовать ввод от мыши. Я предполагал, что когда пользователь касался пером экрана Pilot, то в очередь событий вставлялось событие PenDown, однако, нужно было узнать, как это происходило. Поскольку Copilot был аппаратным эмулятором, он «знал» про аппаратные регистры, но не про очередь событий ОС. Я начал исследовать процесс обработки прикосновений к экрану.

В ЦП Dragonball нет специального регистра для передачи информации о том. что экрана коснулось перо. У меня не было документации о аппаратных средствах Pilot. Единственная источник, который я имел — код ПЗУ, но 512 Кбайт — это очень много кода, для того, чтобы искать отдельные инструкции. Вместо чтения кода ПЗУ, я выбрал путь экспериментальных изысканий.

Используя PILA, я разработал небольшую программу на ассемблере, которая работала на моем реальном Pilot. Эта программа перехватывала векторы прерывания ЦП и показывала информацию о том, какое прерывание произошло и где. Поскольку она представляла собой обработчик прерывания, она не могла использовать обычные функции ОС для вывода символов. Кроме того, я хотел отобразить больше информации о прерывании, чем могло бы поместиться на экране в текстовом режиме. Посему, программа выводила информацию, используя специальные растровые коды. Чтобы просматривать их, я буквально считал пикселы с помощью увеличительного стекла. Используя эту технику, я обнаружил прерывание, которое позволяло обрабатывать события пера.

Это была половина работы. Понять, как передаются координаты, было более сложно. После много более продолжительных трассировок, я обнаружил, что интерфейс пера использует блок управления последовательными устройствам (SPIM) Dragonball. Координаты по X и по Y считывались последовательно: сначала командный байт регистра SPIM, затем — координаты пера в регистре данных SPIM.

Мне пришлось потратить примерно полторы недели между моментом, когда я увидел экран калибровки пера впервые и смог действительно «щелкнуть» на нем. Это было, вероятно, наиболее единственным заметным барьером в разработке Copilot. Но, я был вознагражден. Как только я решил проблему с пером, остальные компоненты Pilot прекрасно работали на эмуляторе. Я прошел экраны настройки, запускал программы, просмотрел пункты списка дел (ToDo), которые записывались в ToDo при инициализации Pilot, даже вводил символы Graffiti мышью. Поскольку ввод Graffiti с точки зрения Pilot — это просто перемещение пера в определенной области, то не нужно было никаких специальных разработок, чтобы они работали. Это был великий миг в истории Copilot.

Я, правда, знал, что это был только «склон горы». Было еще много работы, включая улучшение отладчика, поддержку последовательного порта и нажатий на кнопки.

Отладчик Copilot
Отладчик, который я использовал в Copilot вплоть до этого момента, был тем же примитивным отладчиком, как и в UAE. У него были проблемы, связанные с дизассемблированием некоторых инструкций, не было средств работы с символьной информацией, контрольными точками и его, в общем-то, нельзя было использовать для работы, кроме как для простых задач. Даррин Массена предложил сделать новый отладчик и разработал мощную программу, которая позволяла работать со многими функциями Pilot. Дополнительно к стандартным функциям отладчика, таким, как скажем, контрольные точки, символьные адреса, трассировка стека, наиболее важной функцией была команда bp -na, которую реализовал Даррин. Контрольная точка, которая инициализируется командой bp, контролирует доступ к абсолютному адресу. Это важно, если вы заранее знаете, по какому адресу начинается ваш код, но это не работает для динамически загружаемых программ. Найти где начинается именно ваш код сложно. При использовании же команды -ne, эмулятор перехватывал процедуры запуска программ и инициализировал отладчик при запуске прикладной программы. Таким образом, вы просто запускаете свою программу, и отладчик останавливается на первой инструкции.

Кроме того, реализована функция работы с таблицами идентификаторов, которая автоматически загружается отладчиком. Практически каждая программа для PalmOS имеет символьную отладочную информацию в двоичном «образе» программы. Идентификаторы следуют сразу за последней инструкцией RTS каждой подпрограммы, хранятся в области памяти, которая состоит из байта длины (в котором первый бит установлен в 1), за ней следует название подпрограммы (тот же метод используется в Macintosh для MacsBug). Когда загружается новая программа, отладчик автоматически распознает отладочную информацию об идентификаторах и регистрирует их в общей таблице идентификаторов отладчика. Это неимоверно упрощает отладку.

Завершение работы
Пока Даррин разрабатывал отладчик, я работал над эмуляцией последовательного порта. Если вы хотели запустить программу, которая не была записана в ПЗУ, то было необходимо перенести ее на эмулятор с помощью процедуры HotSync с ПК. К счастью, поддержка последовательного порта в ЦП Dragonball довольно проста — ввод/вывод контролируется единственным байтом. Это легко было эмулировать с помощью API работы с последовательным портом Win32 и как только порт заработал, я успешно синхронизировал эмулятор и ПК, соединив порты ПК нуль-модемным кабелем. Copilot общался одним портом, HotSync — с другим, вместе — прекрасно загружали ПО в Pilot.

Другая особенность, которая пока не была эмулирована — восемь кнопок (четыре кнопки запуска программ, вверх и вниз, включение и кнопка HotSync на крэдле). Я изучил их работу примерно так же, как исследовал работу пера: сделал несколько программ и протрассировал прерывания, выводя информацию в растр на экране, который выдавал информацию о том, что происходит. Как оказалось, кнопки практически полностью «отображаются» на контакты INTx ЦП.

Одна из особенностей ЦП Dragonball — возможность переключения в режим «сна». Это метод понижения энергопотребления, который используется в Pilot когда он выключен. Конечно, питание никогда полностью не отсутствует, в противном случае Pilot не отвечал бы на прикладные кнопки и не выдавал бы предупреждения. Вместо этого, ОС из ПЗУ переключает процессор в режим сна, однако, задает специальные биты в регистрах конфигурации, которые определяют, на какие прерывания ЦП будет реагировать, включаясь. В Pilot эти биты заданы практически для всех кнопок, кроме «вверх» и «вниз». Нажатие на любую кнопку пробуждает ЦП, который немедленно обрабатывает прерывание (чтобы обрабатывать события предупреждений, Dragonball также периодически пробуждается на некоторое время, чтобы проверить список активированных предупреждений).

Примерно в это время я опубликовал предварительную версию Copilot beta 1.0 на моем Web-сайте. Я послал объявление в пару списков рассылки и конференций, посвященных Pilot, и почти немедленно начали отзываться пользователи, которые благодарили за эту программу. Многие отнеслись к нему скептически, как и я, когда начинал проект создания эмулятора для аппаратных средств, которые были никак не документированы, однако скоро Copilot стал любимым средством для разработчиков и конечных пользователей.

Загрузка программ
Copilot был все еще не закончен — загрузка программа оставалась неудобной процедурой. HotSync был единственным методом, который позволял это сделать, что действительно замедляло цикл «отладка/компиляция/запуск». Надо было придумать более простой метод. Я попробовал разные подходы, чтобы решить эту проблему. Первая идея сводилась к имитации действий HotSync, когда данные «подсовывались» в порт, чтобы заставить Pilot реагировать как при синхронизации. Я попытался анализировать поток данных HotSync, но без документации (у меня не было PalmOS SDK в то время), реализовать это не удалось.

Однако, исследуя поток данных HotSync, я изучил процедуру его работы. Файл БД PalmOS представляет собой набор «ветвей», в которых могут содержаться код, данные, пиктограммы и проч. В процессе HotSync, программа переносится на Pilot, одна запись за один раз. Когда HotSync завершается, база данных прикладной программы содержит все данные, необходимые для ее запуска. Итак, все что надо было сделать — реализовать эту операцию, не используя HotSync. Я прочитал о функции DmCreateDatabase, изучил процедуру занесения новых записей в БД с помощью DmNewRecord, различия между обработчиками, указателями, и локальными идентификаторами и мог начать разработку. К сожалению, я не мог непосредственно вызвать DmCreateDatabase из программы для Win32, да и ЦП Intel как-то не может работать с кодом для Dragonball. Нужна была функция, которая позволяла бы работать с БД на эмуляторе.

Основная идея в моем решении проблемы — обработчик прерывания на эмуляторе, независимо от того какую инструкцию исполнял в это время ЦП, изменял следующую инструкцию, переназначая ее на вызов API, стек, помещая в него параметры функции, переносил данные в память, доступную PalmOS, и продолжал цикл эмуляции ЦП, запуская функцию. После того, как она завершалась, эмулятор перехватывал результат (в регистрах A0 и D0), восстанавливала регистры, стек и любые буфера в памяти. В конце восстанавливалась нормальная работа программы.

Были несколько важных моментов. Я был не уверен, что функции работы с БД PalmOS были реентерабельны, поэтому я не мог прерывать работу ЦП, когда был активен любой иной вызов API PalmOS. Поэтому пришлось добавить функцию инициализации контрольной точки в обработчик системного прерывания TRAP $F, который обозначал, что активна функция API. Когда выдается команда загрузки программы, Copilot инициализирует эту контрольную точку, затем ждет прерывания. Пока Pilot включен, вызовы PalmOS API завершаются очень быстро, пауза при ожидании минимальна. Функция загрузки программы прерывает цикл эмуляции ЦП, загружает данные и снова запускает цикл работы процессора.

Как только данные перенесены в память, работа ЦП продолжается, и вызов функции API завершается. Copilot знает, когда вызов API завершается из-за команды прерывания (код операции $4AFC). Этот код операции появляется в потоке инструкций сразу за кодом инструкции TRAP $F. Когда вызов API завершается, ЦП снова останавливается в контрольной точке и активирует программу загрузки данных. В этой точке результаты работы функции сохраняются из регистров (D0 или A0), и восстанавливается выполнение изначального потока управления.

В результате, все эти махинации были «свернуты» в небольшой класс Си , который я назвал FakeCall, который позволял обращаться к функциям PalmOS из программы для Win32:

FakeCall fc;
fc.PushLong(dbid); // dbID
fc.PushWord(0); // cardNo
fc.Call(sysTrapDmDeleteDatabase);
err = fc.GetResultD0();

Этот пример удаляет базу данных, если она уже существует. Я затем сделал функцию открытия базы данных, создания новых записей, копирования записей и закрытия базы данных. Этот код был довольно сложным, поскольку ему нужно было считывать файл PRC из файла на диске, распределять его по записям и передавать каждую запись отдельно. У меня не было документации о формате файла PRC в то время, поэтому работало это весьма нестабильно (по правде, в рассказе я много пропускаю).

По разным причинам, это работало не так корректно, как хотелось бы. Особенно много проблемы было с процессом записи информации, причины могли быть разными — ошибки в моей программе, неправильное использование функций API работы с БД PalmOS или какие-то иные. Я начал просматривать документацию по работе с БД и увидел функцию DmCreateDatabase-FromImage (я не знаю, почему не заметил ее раньше). Это функция делала именно то, что я пытался делать вручную в программе для Win32, можно было просто задать указатель на двоичный образ файла PRC, запустить функцию, и она создавала базу данных со всеми соответствующими записями. Это было именно то, что нужно.

Я выкинул весь безобразный код, который разбирал файл по записям и переделал подпрограммы с использованием функции DmCreateDatabaseFromImage. Это работало стабильно, но была одна проблема. При вызове DmCreate-DatabaseFromImage, один из параметров — указатель на двоичный массив памяти с образом программы. Я занимал кусок памяти с помощью функции MemPtrNew, копировал туда данные и освобождал его, после того, как программа была инсталлирована. Правда, MemPtrNew распределяла память из динамической кучи. Каждый разработчик для PalmOS знает, что динамическая куча очень невелика по размеру (на моем Pilot — примерно 14 Кбайт). Следовательно, максимальный размер программы составлял 12 Кбайт (какое-то количество динамической памяти было необходимо для работы программы). Этого было мало.

Функция DmCreateDatabaseFromImage в качестве одного из параметров требует указатель на двоичный образ программы. Я размещал указатель в динамической куче, выделил область памяти, которую использовал только для загрузки программ, после чего запуск загрузчика позволял перенести этот образ в память CoPilot. Я добавил в подсистему эмуляции ЦП поддержку загрузки программ из 64-Кбайт области в ОЗУ, расположенной по абсолютному адресу 0x10000, и задал стандартный шестибайтовый заголовок блока памяти в куче, чтобы превратить ее в законную с точки зрения ОС область памяти, затем загружал образ программы в это пространство и передавал указатель на нее функции DmCreateDatabaseFromImage. Это работало прекрасно. Программы до 64 Кбайт загружались легко.

Минус такого подхода заключался в ограничении на 64-Кбайт, но преодолеть его без значительных изменений в загрузчике программ было невозможно. DmCreateDatabaseFromImage требует правильный указатель на область динамической памяти (правильность определяется по заголовку блока), В PalmOS 1.x и 2.x максимальный размер блока памяти, который можно выделить составляет 64 Кбайт, поскольку для хранения размера блока используется поле структуры заголовка блока длиной в 16 бит. Для того, чтобы исправить эту проблему, требовалось применить другой метод.

Дальнейшая разработка
В конце октября 1996 г, CoPilot был очень стабильной программой, хотя оставался бета-версией. Я начал работу над другими проектами и разработка CoPilot замедлилась. Когда появился PalmPilot с PalmOS 2.0 (весной 1997 г.), то были некоторые проблемы с совместимостью, например, связанная с тем, что у PalmPilot был увеличено до 1 Мбайт ПЗУ, против 512 Кбайт в предыдущих моделях. Увеличить ПЗУ, с которым работал CoPilot, было просто. Однако, я столкнулся со странной проблемой, связанной с производительностью, которая проявлялась, когда пользователь работал с пером. Я не мог этого объяснить.

Суть проблемы состояла в том, что события от пера передавались с задержкой. Это приводило к тому, что CoPilot замедлялся, плохо реагировал на «перо». Я говорил с некоторыми разработчиками из Palm Computing, с разработчиками, которые портировали CoPilot на другие платформы. Никто не мог объяснить причину. Я так и не смог найти решение, но сделал некоторые изменения в эмуляторе, которые позволили уменьшить ее воздействие. Если кто-нибудь из вас знает, почему это происходит, то буду рад, если вы мне напишете.

Из-за проблемы с пером, я отложил следующий выпуск CoPilot, надеясь, что найду решение. В июне 1997 г. я наконец опубликовал обновленную версию, которая позволяла работать с ПЗУ от PalmOS 2.0. В ней я отключил отладчик, поскольку функция bp -na была неработоспособна с новыми версиями ПЗУ (потребители с не-английским ПЗУ в действительности уже отмечали эту проблему и ранее).

CoPilot больше не был главным моим проектом и не развивался в течение какого-то времени. Другой разработчик, Хейт Ханникут, предложил продолжить разработку, но подготовив несколько версий (с несколькими новым функциями) тоже прекратил работу.

Появление Palm Computing
В начале 1998 г. я узнал, что компания Palm Computing продолжает разработку CoPilot, причем, силами штатного разработчика (Кейт Роллин) и планирует выпуск новой версии эмулятора. Это была прекрасная новость. У меня не было времени продолжать разработку CoPilot, но это был слишком ценный инструмент, чтобы пренебрегать им. Конечно, Palm Computing могла превратить CoPilot в действительно мощный продукт. Думаю. Скоро мы увидим несколько интересных разработок Palm Computing, связанных с CoPilot.

Результат
Разработка CoPilot была очень интересным проектом. Кажется почти невозможно сначала, но, постепенно решая проблемы, программа обретала очертания и наконец заработала. Я вряд ли забуду его. Я счастлив, что Palm Computing оценила мощь CoPilot и сделает его ценным дополнением к инструментальным средствам для PalmOS и надеюсь, что он принесет пользу тем, кто буде разрабатывать новые программы для PalmOS.

Перепечатано из журнала Handheld Systems, май/июнь 1998
© 1998 Creative Digital Publishing Inc. Все права сохраняются.

РекламаRambler's

Allbest.ru

RB2 Network

RB2 Network
--

Просим при воспроизведении материалов этого сайта, делать ссылку на Зоопарк ручных компьютеров
Copyright © 1999-2000 Зоопарк ручных компьютеров