[Перевод] Я отреверсил детскую камеру дочери и запустил на ней Doom: 2 МБ ОЗУ, свободно 891 КБ


Началось всё с праздного вопроса: что за прошивка стоит в этой камере? Закончилось двумя днями и несколькими поздними ночами реверс-инжиниринга совершенно недокументированной SoC (системы на кристалле). За это время появились SDK, локальный эмулятор, собственный компилятор и полноценный порт Doom, который работает на камере со всего 891 КБ свободной ОЗУ. Камерой она при этом быть не перестала.
Сразу уточню: это не Doom ради галочки. Управление разложено так, что в игру реально можно играть. Работают все звуки, есть чиптюновая музыка, можно сохраняться и загружаться, и всё это идёт примерно на 30 fps. Doom у вас в кармане.
Делали ли такое раньше? Проще спросить, куда Doom ещё не портировали, но похожих примеров я нашёл только два. Первый — видео Ильи Курдюкова: там чип Spreadtrum SC6531 с 4–8 МБ ОЗУ, и автор не рассказывает, как это сделано. Второй — видео LGR, где внутри по сути целый компьютер, опять же с большим запасом памяти и полноценным SDK. Наша задача оказалась намного сложнее.
Всё сделанное я подробно задокументировал на GitHub: там лежат разбор прошивки и устройства чипа, утилита для снятия прошивки с вашей камеры через USB, SDK, патчи компилятора и порт Doom. Можно повторить всё самому или применить те же приёмы к чему-нибудь похожему. Эта статья рассказывает, как мы к этому пришли, а репозиторий поможет прийти туда же вам.
Камера

Вот она. На коробке ничего полезного: ни производителя, ни адреса, ни названия, и в интерфейсе тоже пусто. Похоже, её сделали такой специально, чтобы продавать под любым брендом и чтобы никому не пришлось за неё отвечать. Единственный опознавательный знак — модель X9S в инструкции.
USB-идентификаторы тоже ничего не дали. В интернете эта штука почти не существовала, если не считать объявлений на AliExpress.
Потом я посмотрел на один из снимков и нашёл зацепку в EXIF: производителем значился HX-Tech. Это привело меня к статье Sprite_tm (Йерун Домбург) Kid-cam firmware modding. У него была немного другая камера с совсем другой прошивкой, а менять он хотел самую малость. Его работа дала нам хорошую отправную точку (например, какой процессор ожидать), но этого не хватало. Камеру предстояло вскрывать, иначе дальше не продвинуться.
Что внутри
Вскрывать было боязно: дочка эту камеру обожает, и ломать её мне очень не хотелось. Но я уже слишком втянулся. Я достал паяльник и отпаял аккумулятор, чтобы спокойно работать с платой.

Внутри обнаружилась маркировка чипа, HX3302B (больше нигде никаких упоминаний о нём я не нашёл), и восьминогая микросхема флеш-памяти. Всё упиралось в чтение этой микросхемы, и мне повезло: под рукой был подходящий инструмент, программатор XGecu T48, которым я регулярно пользуюсь при ремонте материнских плат. Вы бы удивились, насколько часто на платах старше 20 лет встречается порча данных.

У меня есть клипса, которая надевается на микросхему сверху, чтобы не выпаивать её с платы, но ставить такие клипсы мучительно, и держатся они плохо. Здесь пришлось прижимать её обеими руками, а кнопку мыши нажимать мизинцем. Попыток ушло несколько, и поверил я в отсутствие ошибок чтения только тогда, когда три дампа совпали.
С этого дампа и началась настоящая работа.
В дело вступает Claude
Незадолго до этого я долго занимался декомпиляцией Indiana Jones and the Infernal Machine с точным совпадением по байтам, помогая проекту OpenJones. Поэтому я хорошо представлял, насколько силён ИИ в декомпиляции и реверс-инжиниринге, но также знал его слабые места и как выжать из него максимум.
Этот опыт пригодился: вскоре мы с Claude уже запустили Ghidra и разбирали прошивку на части. Помог Sprite, который уже определил процессор и опубликовал для него описание процессора для Ghidra. Мы быстро заметили, что при всём сходстве чипов прошивка у нас совсем другая: файл обновления, например, назывался Xly100s.bin, у Sprite это был hx330x_sdk.bin.
Первая прошитая правка
Из статьи Sprite мы уже знали, что такие камеры умеют обновляться с файла на SD-карте. Читая код прошивки, мы первым делом выяснили, как именно идёт запись, и главное, что обновление частичное: запись начинается с начала флеш-памяти и продолжается ровно на длину файла. Подсунете файл на 1 МБ, и прошьётся первый мегабайт, а остальные три останутся нетронутыми. Это было важно: мы могли вносить маленькие точечные правки и переписывать только нужный кусок, не трогая всё, что лежит дальше.
Благодаря T48 у нас всегда оставался аварийный путь назад: оригинальную прошивку можно было залить напрямую. Но возня с клипсой была кропотливой, и мы хотели её избежать. Обновляться с SD-карты было гораздо проще.
Кроме того, хотя в заголовке есть контрольная сумма, камера никогда не проверяет её правильность. Теоретически для запуска перепрошивки хватало сдвинуть метку времени в заголовке на секунду. Судя по коду, проверка обновления выполняется при каждом включении камеры.
Так что начали мы вообще без реальных изменений: только первые несколько килобайт флеш-памяти с изменённой меткой времени. Цель была запустить перепрошивку, увидеть экран обновления и убедиться, что мы на верном пути. Поскольку менялась только метка времени, безопаснее перепрошивки не придумаешь.
Мы ждали, что на экране версии появится новая метка, но этого не случилось, и, насколько мы могли судить, вообще ничего не произошло. Поэтому довольно долго мы искали какие-то секретные условия запуска.
Заодно я обнаружил, что если на экране батареи удерживать затвор 5 секунд, камера переходит в особый тестовый режим с надписью TEST ON. Мы нашли эту строку в прошивке и заменили её на HACK ON. Файл получился куда больше, примерно 770 КБ, и наконец появилась полоска прогресса. А когда мы снова зашли в тестовый режим, надпись была на месте:

Хак действительно состоялся, а вот секретные условия мы искали зря. Прежние обновления работали, просто были такими маленькими, что полоска прогресса не успевала появиться, а заметной разницы наши правки не давали.
EXIF спешит на помощь
Способ прошивки у нас появился, и пришло время первой настоящей правки кода. Мы выбрали трюк с EXIF, который Sprite изящно применил на своей камере: он давал ценные сведения об устройстве ещё до серьёзных изменений.
Если вы с этим не сталкивались, EXIF — это блок сведений в самом начале файла снимка: какая камера снимала, когда, с какими настройками (на телефоне обычно ещё и где). В JPEG под него можно зарезервировать до 64 КБ, а эта камера использует примерно 730 байт, около 1%, так что свободного места хватает, и никто туда не смотрит.
Идея Sprite заключалась в том, чтобы использовать это место как свалку для дампов памяти. Меняешь код, записывающий метаданные снимка, так, чтобы вместо них он писал кусок памяти самой камеры, а потом читаешь его с SD-карты прямо из фотографии.
Сам по себе этот трюк позволяет снять до 64 КБ за снимок, и с основной ОЗУ камеры он сработал с первого раза: один снимок, один большой кусок. Но с той памятью, которая интересовала нас больше всего, с маленькой внутренней стартовой памятью, ничего не вышло. Снимок пишет на SD-карту аппаратный блок, который этой памяти не видит, поэтому вернулись одни нули. Пришлось заставить собственный процессор камеры копировать память в снимок, но места хватало только на 4 КБ за раз, так что мы сделали процесс пошаговым. Счётчик в памяти камеры сдвигал окно после каждого снимка, и каждый следующий кадр захватывал очередные 4 КБ. Сделал 16 снимков и получил все 64 КБ прямо с SD-карты.
Это дало нам много. Внутренняя память камеры байт в байт совпала с началом нашего дампа прошивки, как и часть основной ОЗУ, а значит, эти части дампа были верны. Выяснилось также, что код, который вызывает прошивка и который мы считали спрятанным в ПЗУ, на самом деле просто часть стартового кода во флеш-памяти, так что всё, что выполняет камера, у нас в дампе уже было. (Заодно вскрылась и наша ошибка: «ПЗУ», которое мы всё пытались прочитать, возвращало одни нули, потому что мы неправильно вычисляли адреса. Именно дамп памяти наконец помог это исправить.) А перебирая память по мегабайту за раз, мы выяснили, сколько в камере ОЗУ: на запросе третьего мегабайта она перезагрузилась. Значит, два мегабайта. Но на этом возможности трюка заканчивались. Он дал нам карту памяти камеры, но не показал, что на самом деле делает код, так что для настоящего понимания прошивки пришлось браться за сам код.
Первое исправление
Способ прошивки и карта памяти были, оставалось решить, что реверсить первым. Один кандидат напрашивался сам, и исправить его было особенно приятно, потому что он всегда меня раздражал. В настройках камеры один из пунктов меню называется Systerm Volume. Казалось бы, исправить опечатку легко: находишь текст, меняешь, готово. Вот только в прошивке этого текста как текста нет. Поиск ничего не находит ни в одной кодировке.

Оказалось, что в камере есть собственный растровый шрифт с несколькими сотнями глифов (латиница, кириллица, тайский, китайский, корейский, арабский…), а каждая строка меню хранится как список номеров глифов для каждого из 18 поддерживаемых языков. Нам пришлось разобрать формат этих таблиц, написать скрипт, который рисовал строки шрифтом самой камеры, чтобы мы видели, на что смотрим, и потом найти нужную английскую строку. Мы стали на шаг ближе к пониманию кода, а надоедливая опечатка исчезла.
Теперь тесты
Первое исправление прибавило уверенности, и мы взялись разбирать прошивку всерьёз, просто чтобы понять, как она устроена. Быстро нашлась область, откуда запускаются игры, и она показалась самым безопасным местом для кода покрупнее. Этот код запускается только из меню игр, проверка обновления срабатывает раньше, сразу при включении камеры. Как бы мы всё ни сломали, путь назад всегда оставался.
Мы пока писали на ассемблере и сделали серию маленьких программ и бенчмарков, чтобы собрать данные: нажатия кнопок, сколько ОЗУ на самом деле свободно, как всё размещено в памяти, с какой скоростью читается и пишется ОЗУ, насколько быстр процессор и как работать с дисплеем.

Когда результаты были собраны, мы наконец поняли, с чем имеем дело:
Процессор: в лучшем случае ~120 MIPS (миллионов простых инструкций в секунду), или ~82, если ветвление встречается каждые несколько инструкций.
Кеш L1: 8 КБ для инструкций и 8 КБ для данных, чтение и запись ~164 МБ/с.
ОЗУ: 2 МБ, чтение ~65 МБ/с, запись ~37 МБ/с. Для наших целей свободно всего 891 КБ.
Флеш-память: 4 МБ (та самая микросхема с прошивкой), чтение всего ~4,4 МБ/с. Код, который помещается в кеш инструкций, работает из флеш-памяти так же быстро, как из ОЗУ, а код, который не помещается, примерно в 18 раз медленнее.
Дисплей: 320×240, 256 цветов, палитру можно менять.
Сравнение не совсем прямое, но получается что-то близкое к 486 DX4/100 с его примерно 100 MIPS, а скорость памяти примерно как у раннего Pentium.
Иными словами, перед нами было нечто вроде быстрого 486 с памятью скорости Pentium и с запасом ОЗУ меньше мегабайта.
В этот момент я уже спрашивал себя, какова вообще моя конечная цель с этим устройством. Но, глядя на цифры, я хмыкнул и задал вопрос: как думаешь, а Doom мы запустим? Характеристики очень напоминали машины, на которых он в своё время прекрасно шёл, только без полных 4 МБ ОЗУ, которые ему были нужны. У нас было вдвое меньше, и большая часть уже занята.
Портируем Doom
Мы начали с doomgeneric, версии Doom, специально рассчитанной на лёгкий перенос на всякие дурацкие устройства вроде нашей камеры. Вы предоставляете несколько функций (нарисовать кадр, прочитать клавишу, сообщить время), а остальное она делает сама.
Поскольку свой компилятор и эмулятор у нас уже были, перенести код на нашу разновидность OpenRISC оказалось довольно просто. Настоящая проблема заключалась в том, как мало у нас памяти. Doom рассчитан на машину с 4 МБ, в камере их всего 2, и свободно из них после того, как прошивка забирает свою долю, только 891 КБ. В них должны уместиться две разные вещи: код игры и её рабочая память, то есть текстуры, уровни и звуки, которые она подгружает и тасует во время игры.
Ищем ещё ОЗУ
Рабочая память — проблема покрупнее: Doom непрерывно выделяет из большого пула, пока идёт игра. Поэтому мы отправились искать ещё. Мы написали простую программу-заглушку, которая делала всё, что понадобится игре (проигрывала звуки, читала клавиши, рисовала на экране, читала с SD-карты), и замерили, какие области памяти камеры менялись во время её работы. Всё, чего прошивка ни разу не коснулась, становилось кандидатом на заимствование.
К сожалению, то, что выглядело безопасным, часто таким не было: запись туда перезагружала камеру. Пришлось долго ходить туда-обратно, пока мы не выжали всё до последнего байта и не довели объём, доступный Doom, примерно до 1,4 МБ, пусть и множеством разрозненных кусков.
Это большой шаг вперёд по сравнению с 891 КБ, но до 4 МБ, на которые писался Doom, ещё очень далеко. Требовались новые ухищрения.

Билл Гейтс почти наверняка никогда такого не говорил, но к месту пришлось.
Фрагментация всё усугубляла. Отчасти дело было в самой памяти: те 1,4 МБ состояли из множества отдельных кусков, поэтому самый большой блок, который мы могли выдать, был гораздо меньше общего объёма. Остальное было на совести управления памятью в самом Doom: оно создавалось для машины с запасом места и не особенно заботилось о том, куда что попадёт. Мелкие выделения оказывались посреди пространства, нужного крупным, и дробили его на непригодные кусочки, так что объект, которому нужно 64 КБ одним куском, не мог их найти, хотя всего свободной памяти хватало. Мы отвели мелким выделениям отдельную зарезервированную область, чтобы они не мешали крупным.
Кроме того, Doom по мере надобности читает графику, звуки и уровни с SD-карты и выбрасывает их из ОЗУ, когда памяти становится мало; это он уже умеет. Мы только позаботились о том, чтобы выбрасывалось правильное, например звуковые эффекты, и чтобы данные уровня очищались по его окончании. Так мы не перезагружали всё подряд с медленной карты.
Выполняем код из флеш-памяти
Код игры был отдельной проблемой и претендовал на ту же ОЗУ. Обычно программу загружают в ОЗУ целиком, но места для этого не было даже близко. Вместо этого код можно выполнять прямо из микросхемы флеш-памяти, только бенчмарк показал, что это зверски медленно: код, не вмещающийся в крошечный кеш процессора, работал из флеш-памяти примерно в 18 раз медленнее, чем из ОЗУ. Но выбирать что-то одно не обязательно. Часть кода может жить в ОЗУ, а остальное во флеш-памяти, и вопрос сводился к тому, какие именно части куда.
Чтобы это выяснить, мы профилировали Doom в нашем эмуляторе и записывали, какие части кода реально выполнялись во время игры. Оказалось, что 99% времени уходит примерно на 29 КБ кода из 260 КБ; всё остальное — это стартовый код, меню, автокарта, экраны между уровнями и прочее. Поэтому мы положили это маленькое горячее ядро в ОЗУ, а всё, что выполняется редко, оставили во флеш-памяти, где медлительность не мешает. Это освободило сотни килобайт ОЗУ под рабочую память, и Doom шёл с той же скоростью, что и версия целиком в ОЗУ.
Скорость, звук и музыка
Кроме того, мы ускорились, обойдя событийную систему клавиш прошивки и читая клавиши напрямую. Выяснилось, что главный цикл прошивки сам по себе съедает около 38 миллисекунд каждого кадра, поэтому мы написали свой игровой цикл, и частота кадров примерно удвоилась. Прямое чтение клавиш также позволило сделать кнопку питания игровой, чтобы она не просто выключала камеру.
Когда игра заработала как надо, мы взялись за звук и музыку. Камера умеет проигрывать только один аудиопоток, а Doom рассчитывает играть много звуков разом, так что мы написали программный микшер, который сводит все звуковые эффекты Doom в этот единственный поток.
С музыкой было сложнее. Музыка Doom хранится как компактная партитура в формате MUS, очень похожем на MIDI: список вида «сыграй такую ноту на таком инструменте с такой громкостью». На ПК Doom проигрывает её через чип FM-синтезатора, которого в камере нет. Можно было бы эмулировать этот чип, но для процессора это слишком тяжело. Рассматривали мы и вариант заранее отрендерить музыку в MP3 и использовать встроенный декодер камеры, но это казалось неаутентичным.
Вместо этого мы написали собственный крошечный синтезатор. Он читает партитуру и играет её 16 голосами, каждый из которых представляет собой простую волну (прямоугольную, треугольную или пилообразную, в зависимости от типа инструмента) с базовой огибающей громкости, а для барабанов используется шум. Всё это подмешивается в тот же поток, что и звуковые эффекты, и обходится примерно в 2% процессора. В результате классическая музыка Doom звучит как на старой игровой приставке, очень чиптюново.
Прежде чем ставить всё это на камеру, мы скомпилировали синтезатор для ПК и отрендерили там первые музыкальные файлы, чтобы я мог их оценить. Решать, достаточно ли они хороши, предстояло мне, и они сразу мне полюбились. Возможность тестировать на ПК очень выручила. Вот первые 30 секунд двух композиций Бобби Принса: At Doom’s Gate (E1M1) и Dark Halls (E1M3). Это рендер на ПК из того самого кода, что работает на камере; с самой камеры звук не записывали.
Как управлять Doom на камере?
К счастью, у камеры есть четыре кнопки со стрелками, и это решило главную проблему, но с остальными кнопками пришлось пойти на компромиссы, а может, и на удачные решения.
Затвор стреляет. Это очевидно, и при удержании стрельба продолжается, что стало возможным только после перехода на прямое чтение клавиш.
Для смены оружия служит кнопка питания: одиночное нажатие перебирает оружие, пока не дойдёте до нужного. Долгое нажатие работает как Escape и открывает меню. Если меню уже открыто, для возврата хватает короткого нажатия.
Раз уж кнопку питания мы забрали, камеру она больше не выключает. Чтобы выключить, нужно выбрать в меню Doom пункт выхода из игры и подтвердить затвором, который работает как Enter.
Загрузка и сохранение обычно просят ввести название сохранения, поэтому мы сделали так, что они называются сами по номеру слота (Save 1, Save 2 и так далее).
Наконец, для «использовать» свободной кнопки не нашлось, поэтому мы отслеживаем, когда вы подошли к чему-то вплотную и продолжаете давить, и тогда объект активируется. Отступите и снова упритесь, и он сработает во второй раз, что идеально подходит для включения и выключения рубильников. Работает это хорошо, а приятный побочный эффект в том, что можно просто бежать на двери, и они открываются без всякой кнопки.
Когда меню открыто в разгар напряжённой игры, камера немного буксует. Подозреваю, причина в том, что меню редко используется и его код лежит в медленной флеш-памяти. Улучшать это я не пытался: на игровой процесс это не влияет, и из-за такого пограничного случая переживать не стоит.
Результатом я очень доволен: играется отлично. От первой прошивки до работающего Doom прошло всего два дня, включая отладку глубокой ночью, и ровно столько же я потратил на эту статью и документацию на GitHub.
Чтение прошивки через USB
Когда Doom заработал, мне захотелось, чтобы нашими наработками могли воспользоваться и другие владельцы похожих камер. Очевидным решением было бы выложить мой дамп прошивки, но это выглядело юридически сомнительно и пригодилось бы только тем, у кого в точности такая же прошивка. Даже реверсни я всё обратно в C, там остались бы ресурсы, которыми я не вправе делиться. Поэтому я захотел дать людям способ снимать дамп самостоятельно, не вскрывая устройство. К сожалению, прямого интерфейса для этого не было.
Зато мы нашли на USB-устройстве команду SCSI (да, тот самый SCSI, он всё ещё жив!), которая позволяла компьютеру попросить камеру выполнить код по любому адресу. Такую вещь можно довольно универсально использовать для доступа к устройству, но любые попытки заставить Claude что-то с ней сделать пресекались из соображений безопасности, и эта линия работы остановилась.
К счастью, у моего хорошего друга Джека была подписка на Kimi, и он одолжил её мне на несколько часов. У Kimi таких сомнений не было. Он прошёлся по прошивке, написал серию тестов, чтобы понять, что происходит, и в итоге придумал остроумный приём: записать в ОЗУ камеры небольшую заглушку, которая читает флеш-память и отдаёт её небольшими кусками, а потом запускать её снова и снова, пока не будет прочитано всё. Ни прошивки, ни вскрытия камеры не требуется. Собственный процессор камеры становится посредником для чтения её же флеш-памяти.
Дамп, снятый программатором, позволил проверить результат, и он совпал. Мне самому от этого уже мало пользы, но кому-нибудь другому, надеюсь, пригодится! Важная оговорка: приём использует конкретные адреса этой прошивки, поэтому вполне может работать только на этой версии. Буду рад узнать, если кто-то попробует на другой.
Что бы я сделал иначе и что дальше
Что бы я сделал иначе? Положил бы USB-кабель и сказал: «Господи, Спод, найди себе занятие получше».
Но теперь у нас есть солидная документация и какой-никакой SDK для того, чего раньше в интернете вообще не существовало. Перед тем как вернуть камеру дочери, я поставил оригинальную прошивку обратно, но теперь могу приступить ко второму этапу: упростить интерфейс камеры и обновить игры, добавив кое-что из Spode’s Playground. Когда она ей надоест, я снова поставлю Doom, и камера будет стоять у меня на полке как трофей.
Если у вас есть такая камера (или похожая), в репозитории есть всё: утилита для снятия прошивки через USB без вскрытия и скрипт, который превращает ваш дамп в обновление с Doom. Буду рад услышать, как у вас получится.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.