logo

Отладка игр SEGA MD/Genesis

В этом документе будут описаны некоторые методы нахождения данных в играх на SEGA. В этом документе нам понадобятся:

  1. Графика.
    Краткая Теория:
    Рано или поздно мы будем находить места в адресном пространстве Сеги, поэтому, в первую очередь, важно знать к чему они относятся (РОМ, оперативная память или еще что). Вкратце, распределение адресного пространства SMD выглядит следующим образом:

    $00:0000-3f:ffff = РОМ
    
    $a0:0000-a0:1fff = ОЗУ Z80
    $a0:4000-a0:4003 = YM2612
    $a1:0000-a1:101F = Порты Ввода/Вывода и регистры
    $a1:1000-a1:1001 = Захват шин Z80
    $a1:2000-a1:20ff = !FDC (сигнал выбора устройства, выведенный на порт SegaCD) 
    $a1:3000-a1:30ff = !TIME (сигнал выбора устройства, выведенный на контакт В31 гнезда картриджа, например для использования маппера или включения SRAM) 
    
    
    $c0:0000-c7:ffff = Видеопроцессор
    
    $ff:0000-ff:ffff = Оперативная память

    VDP (видеопроцессор) имеет 4 палитры по 16 цветов в каждой“Родная” для Сеги графика (которая содержится в видеопамяти) имеет очень простой формат - 4bpp linear. То есть, каждый пиксель может быть закодирован 4-мя битами. 4 бита позволяют нам описать как раз те самые 16 цветов, что хранятся в каждой из палитр. Таким образом, полный тайл будет занимать 4*8*8 = 256 бит или 32 (0х20) байт. А один ряд из 16 тайлов (какой обычно мы видим в тайловом редакторе, будет занимать в видеопамяти 0х200 байт). Учитывая, что объем видеопамяти всего 0х10000 байт, то максимально SMD может иметь 0x800 тайлов.
    Практика:
    Итак, приступим к нашей жертве. Найдем графику шрифта вступительного ролика (плывущие снизу вверх строки). Загрузим ROM в трейсер, дойдем до места, где только начинают появляться строки вступительной заставки, и нажмём клавишу ‘.’ в английской раскладке. В корневой папке трейсера должны появиться несколько дампов:

    - cram.bin
    - nt-scr_a.txt
    - nt-scr_b.txt
    - nt-win.txt
    - ram.bin
    - satb.txt
    - sram.bin
    - vram.bin
    - vsram.bin

    Нас пока что интересует файл ‘vram.bin’ - дамп видеопамяти приставки. Загрузим его в наш любимый тайловый редактор, отобразим в режиме 4bpp linear (иногда просто ‘smd’ - ‘genesis’) и отыщем шрифт, который мы только что видели на экране. Кстати, для нормального отображения шрифта (он у нас в режиме 8х16) в Tile Moelester’e можно выставить view -> row-interleaved. Vram
    Как видим, шрифт начинается по адресу 0x6000 в видеопамяти (или 0x300 тайлов от начала). Осталось отыскать команду, которая копирует в эту область графику из РОМ’а.
    И вот тут мы впервые сталкиваемся с порождением G8z et al под названием Gens Tracer. Нужно сказать, что благодаря плачевному состоянию дел с нормальными отладчиками под Сегу, G8z et al с его странной системой хуков кажется не таким уж и плохим вариантом, даже, несмотря на минималистичную до невозможности сопроводительную документацию. А при должной сноровке, такая система лишь немного уступает обычным ‘оконным’ отладчикам.Итак, немного практики: В корневом каталоге трейсера хранится заветный файл hook_log.txt, в котором в самом верху записаны непосредственно хуки:

    hook_pc1 1 -1 -1
    hook_pc2 1 -1 -1
    hook_pc3 1 -1 -1
    
    hook_rd1 1 -1 -1
    hook_rd2 0 -1 -1
    hook_rd3 0 -1 -1
    
    hook_wr1 1 -1 -1
    hook_wr2 0 -1 -1
    hook_wr3 0 -1 -1
    
    hook_ppu1 1 -1 -1
    hook_ppu2 1 -1 -1
    hook_ppu3 1 -1 -1

    А ниже - минимальная справочная информация, которую можно вообще удалить. С хуками нужно обращаться очень бережно - малейшая ошибка и поставленный, вроде бы, правильно хук не сработает. Если рассмотреть структуру хука, то он выглядит примерно так:
    hook_wr1 1 -1 -1
    ‘hook_wr1’ Хук номер один на запись в оперативную память
    ‘1’ Режим первый
    ‘-1 -1’ вообще-то означают адреса (в шестнадцатиричной системе счисления), на запись в которые должен сработать этот хук, но в данном случае такие адреса просто делают хук неактивным.
    Хук очень похож на останов, за исключением того, что при его срабатывании не останавливается выполнение программы, а лишь записывается информация о месте срабатывания в отдельный текстовый файл ‘hook.txt’.Трейсер может иметь до трех активных хуков каждого типа. Возможны 4 типов хуков:

    - hook_pc Хук на срабатывание команды по заданному интервалу адресов. PC подразумевает Program Counter                                         
    - hook_rd Хук на выполнение чтения из памяти по заданному интервалу адресов. rd расшифровывается как read                                      
    - hook_wr Хук на выполнение записи в оперативную память по заданному интервалу адресов. wr расшифровывается как write                          
    - hook_ppu Хук на выполнение записи в видеопамять по заданному интервалу адресов. PPU взято у названия видеопроцессора NES.

    И 8 режимов хуков.
    Я обычно пользуюсь нечетными режимами хуков, т.к. в этом случае адрес срабатывания хука можно писать полностью, вроде FFF400. Если использовать четные режимы, необходимо будет писать короткий адрес, то есть F400. Ведь старший FF лишь указывает на то, что это область оперативной памяти, а это и так подразумевает тип хука, например hook_wr. Поэтому обычно с нечетными режимами меньше путаницы.Чаще всего приходится пользоваться либо 1 либо 3 режимом. В первом случае, сведения о сработавших хуках будут записываться в файл hook.txt, а трейс лог будет сбрасываться в файл trace.log. В режиме номер три, и трейсинг и хуки сбрасываются в trace.log для большей наглядности порядка срабатывания хуков.
    Поначалу звучит все это громоздко и страшновато, но, уверяю, как только вы поставите пару хуков и увидите результат работы трейсера, все встанет на свои места.
    Итак, вернемся к нашему поиску графики шрифта. Нам, как выясняется, нужно поставить простой хук на запись в видеопамять по адресу 0x6000. Открываем файл hook_log.txt в текстовом редакторе и корректируем строку
    с
    hook_ppu1 1 -1 -1
    на
    hook_ppu1 1 6000 6000
    Сохраняем файл и запускаем еще раз трейсер. Перед тем, как повторно открыть РОМ можно уже сейчас нажать клавиши ‘,’ и ‘/’ в английской раскладке, которые соответственно включают срабатывания хуков и сбрасывание трейса в соответствующие файлы. Доходим опять до вступительного ролика, где мы видим появляющиеся на экране буквы (теперь мы можем быть уверенными, что графика шрифта уже записана в видеопамять), жмем еще раз ‘,’ и ‘/’, чтобы закрыть файлы, и смотрим, что получилось в файле hook.txt:

    [00:0242] VRAM DMA from 000000 to 0000 [FFFE]
    - SRC_H = $00:023A ~~ LEN_H = $00:023A
    
    [00:328A] VRAM W16 = 0000 [6000]
    [01:1A82] VRAM DMA from 0ED7A4 to 6000 [4000]
    - SRC_H = $01:1A7C ~~ LEN_H = $01:1A7A
    
    TRACE STOPPED

    Что же это означает? Начнем с простого и наиболее распространенного случая:
    [00:328A] VRAM W16 = 0000 [6000]
    [00:328A] - адрес команды, на которой сработал хук
    VRAM - произошла запись в видеопамять
    W16 = 0000 - произошла запись 16-битного word’а, который равен 00 00
    [6000] - адрес, куда произошла запись (ведь мы и ставили хук на запись в 0x6000).
    а вот другой случай:

    [01:1A82] VRAM DMA from 0ED7A4 to 6000 [4000]` 
    - SRC_H = $01:1A7C ~~ LEN_H = $01:1A7A
    Здесь произошла пересылка в видеопамять через DMA - контроллер прямого доступа в память. Это самый быстрый способ перебросить большой массив данных из одного места в другой.
    [01:1A82] - адрес команды, осуществившей пересылку
    VRAM DMA from 0ED7A4 - адрес, откуда осуществляется пересылка
    to 6000 - адрес, куда осуществляется пересылка
    [4000] - размер пересылаемого массива
    - SRC_H = $01:1A7C - адрес команды, которая осуществляет запись адреса, источника пересылки
    ~~ LEN_H = $01:1A7A - адрес команды, которая осуществляет запись размера пересылки.
    Очевидно, интересующая нас запись только последняя. Т.к. команда по адресу 00:328A, по всей видимости, только производит очистку видеопамяти.
    Graph of sub_3264
    Кроме того, длина последней пересылки DMA, равна 0х4000, что подразумевает 0х200 тайлов или две страницы в привычном для нас виде хексредактора (16х16 тайлов), а именно в таком виде и содержится наш шрифт в видеопамяти - после двух страниц графики находится пустота.
    И, похоже, что наши поиски уже закончены, ведь согласно распределению памяти Сеги, смещение источника DMA 0х0ED7A4 - это смещение в РОМе. И чтобы в этом убедиться, откроем РОМ в тайловом редакторе и перейдем по смещению 0х0ED7A4. Как видим, графика найдена и уже готова к изменению. Только не забудьте, что при любом изменении РОМа, будет изменяться его чексумма, что приведет к появлению красного экрана вместо запуска игры. Исправить эту ситуацию можно или заставляя пользователя перевода включать опцию auto fix checksum в своем эмуляторе, либо избавиться от проверки чексуммы путем правки байт РОМа.
  2. Код.
    Краткая Теория: Наиболее легкий способ проверить, не изменились ли данные в РОМе - это подсчитать сумму байт всего РОМа (весь код и данные) и сравнить с “правильной” проверочной суммой, которой обладает РОМ в исходном “коробочном” состоянии. При подсчете суммы обязательно чтение из каждого байта РОМа, даже из байтов кода. Маловероятно, что игре зачем-нибудь понадобится читать массив кода, кроме как проверки чексуммы или запуска кода из другого места (что встречается не очень часто). Поэтому оптимальный путь поиска процедуры проверки чексуммы - это поставить хук на чтение из какого-нибудь байта кода.
    Практика:
    Выставим в файле hook_log.txt строку:
    hook_rd1 1 200 200
    И нажав ‘,’ и ‘/’, а потом, запустив РОМ в трейсере, дождемся появления надписи SEGA (так мы будем уверены, что процедура проверки чексуммы уже пройдена). Обратимся к файлу результатов hook.txt:

    [00:0242] VRAM DMA from 000000 to 0000 [FFFE]
    - SRC_H = [00:023A] ~~ LEN_H = [00:023A]
    
    [00:035C] R16 = 4AB9 [000200]
    
    TRACE STOPPED

    Последняя команда и есть процедура чтения байт кода:

    ROM:00000350 movea.l #$200,a0; Начинаем с первого байта кода (по вектору RESET)
    ROM:00000356 move.w #$1FFD,d0; 0x2000 проходов
    ROM:0000035A moveq #0,d1; обнуляем регистр, в котором будет считаться чексумма
    ROM:0000035C
    ROM:0000035C loc_35C: ; CODE XREF: RESET+25C j
    ROM:0000035C add.w (a0)+,d1; Вычисление чексуммы
    ROM:0000035E add.w (a0)+,d1
    ROM:00000360 add.w (a0)+,d1
    ...
    
    ROM:0000045A add.w (a0)+,d1
    ROM:0000045C dbf d0,loc_35C; Конец блока, переходим к следующему
    ROM:00000460 movea.l #$18E,a1; По смещению 0х18E в РОМе находится word чексуммы
    ; (ROM:0000018E dc.w $A62B)
    ROM:00000466 cmp.w (a1),d1 ; Само сравнение с образцовой контрольной суммой!!!
    ROM:00000468 bne.w loc_596 ; Если равно,
    ROM:0000046C lea (dword_FFFFFF00).w,a1; продолжаем выполнение программы
    Такой дизассемблерный листинг можно получить либо из лога трейса, либо из IDA.
    Всего операций сложения word’ов типа add.w (a0)+,d1 0х80 штук. Таких блоков сложения за всю процедуру произойдет 0х2000, т.е. будет считано 0х100000 word’ов или 0x200000 байт, ну то есть абсолютно весь РОМ.Итак, чтобы избавиться от красного экрана, можно или сделать что-то с кодом или посчитать чексумму своего перевода и подправить два байта по смещению 0x18E. Чтож, пойдем по пути наименьшего сопротивления и сделаем простейший байт-хак. Например, вместо bne.w loc_596 впишем bne.w loc_46C:
    с
    ROM:00000468 6600 012C bne.w loc_596
    на
    ROM:00000468 6600 0002 bne.w loc_46C
    Успешный запуск игры с подправленным шрифтом на эмуляторе без выставленной галочки autofix checksum говорит нам о том, что мы поменяли нужные байты.Следующим пунктом нашей программы будет нахождение текста. Тут может быть два варианта: либо текст является частью тайловой карты, либо он выводится спрайтовой надписью. Выяснить это довольно легко. Gens Tracer имеет горячую клавишу ‘6’ для отключения спрайтов, поэтому если после нажатия этой клавиши надпись исчезает, то это спрайтовая надпись, а если нет - часть тайловой карты. Таким образом, легко увидеть, что, например растворяющаяся надпись ‘alien soldier’ - это спрайтовая надпись, а вот весь текст вводного ролика выводится с помощью карты тайлов.
  3. Тайловые карты.
    Краткая Теория:
    SMD имеет три слоя, которые могут отображаться одновременно. Их принято называть “планами”: Plane A, Plane B, Plane Window. Обычно, “окно” используется для отображения счета, времени, жизней и подобных мелких деталей, которые не нужно прокручивать. В то время как слои А и В могут скроллироваться независимо друг от друга. Слои составлены из тайлов, как мозаика. Каждый слой может иметь 6 разных размеров: 32*32, 32*64, 32*128, 64*32, 64*64, 128*32 (в тайлах). Номера входящих в слой тайлов и их атрибуты хранятся в видеопамяти и составляют тайловую карту.
    Каждая ячейка тайловой карты занимает 16 бит (word):

    -Бит 0-10 : Номер отображаемого тайла.
    -Бит 10-11 : Горизонтальное отражение тайла.
    -Бит 11-12 : Вертикальное отражение тайла.
    -Бит 12-14 : Номер цветовой палитры, в которой будет отображен тайл.
    -Бит 15 : Приоритет (теневой или нормальный).

    Выглядит довольно сложно, но на самом деле, если нет отражений тайлов упрощенно и очень условно можно считать следующим образом. Предположим, ячейка имеет следующий word: A3 16Здесь 0х316 можно считать номером тайла, а 0x0A = %1 01 0, где условное %01 - номер палитры (в данном случае это палитра номер два).
    Ячейки слоя А хранятся в видеопамяти, начинаясь по адресу 0xC000, а слоя В - по адресу 0xE000.
    Практика:
    В первую очередь, определим, в каком слое находится наш текст, для чего в Gens Tracer имеются горячие клавиши ‘4’ и ‘5’, соответственно для отключения слоя А и слоя В. Итак, легко увидеть, что текст проходит в слое А. Пришло время воспользоваться сделанным нами в самом начале дампом ‘nt-scr_a.txt’. Откроем его в текстовом редакторе. Для лучшего обзора желательно отключить перенос по строкам.Каждая строка здесь, для удобства, представлена также, как на экране - 64 тайла шириной. Т.е. 0x80 байт. Каждая ячейка представлена здесь трехзначным числом - это номер тайла в 16-ричном счислении. Здесь не показаны атрибуты вроде номеров палитры, отражения и приоритета тайла, потому что чаще всего это не нужно для поиска.Итак, в дампе можно увидеть такую строку:

    CC80: 000 000 000 000 33C 324 31E 300 346 31E 316 338 300 326 33A 300
    CD00: 000 000 000 000 33D 325 31F 301 347 31F 317 339 301 327 33B 301

    33C - номер одного из отображаемых на экране тайлов. Чтобы понять какой именно это тайл, достаточно умножить его номер на 0x20 (столько байт занимает один тайл) и перейти в дампе видеопамяти по полученному адресу. 33c*20 = 6780
    TLetterAddress
    Оказывается, по этому адресу в видеопамяти хранится верхняя часть графики буквы T, соответственно, по адресу CD00 - нижняя часть графики буквы T. Как было сказано в самом начале, весь алфавит начинается с 300-го тайла, а найденные в дампе карты памяти номера соответствуют словам:

    CC80: 000 000 000 000 33C 324 31E 300 346 31E 316 338 300 326 33A 300  
    CD00: 000 000 000 000 33D 325 31F 301 347 31F 317 339 301 327 33B 301  
    _ _ _ _ T H E _ Y E A R _ I S _ ...                                    

    То есть первые слова нашего вводного ролика. Теперь осталось отследить, какая же команда записывает байты в эту область видеопамяти, и откуда эти байты берутся.Ставим хук:
    hook_ppu1 1 cc88 cc89
    Доходим до места появления первой строки вводного ролика с включенным memory и trace логингами и выходим. В файле ‘hook.txt’ видим следующую картину:

    [00:331A] VRAM W16 = 0000 [CC88]
    [00:28B6] VRAM W16 = 0000 [CC88]
    [01:137A] VRAM DMA from FFA980 to CC80 [0080]
    - SRC_H = $01:1364 ~~ LEN_H = $01:1358
    
    [00:0EA2] VRAM DMA from FFF400 to CC88 [0042]
    - SRC_H = $00:0E9C ~~ LEN_H = $00:0E9A
    
    TRACE STOPPED

    Очевидно, что в тайловую карту байты попали именно в результате последнего DMA, источником которого является область в оперативной памяти: 0xFFF400, поэтому дополнительно выставляем хук на запись в оперативную память:
    hook_wr1 1 FFF400 FFF400
    и проделываем те же операции, что и в прошлый раз:

    [00:4710] W16 = C33C [FFF400]
    [00:0EA2] VRAM DMA from FFF400 to CC88 [0042]
    - SRC_H = $00:0E9C ~~ LEN_H = $00:0E9A
    
    TRACE STOPPED

    По смещению 0х4710 в РОМе хранится команда, которая записывает готовый word тайловой карты в буфер ОЗУ. Он в дальнейшем копируется непосредственно в видеопамять. Рассмотрим поподробнее процедуру, в которой находится команда по смещению 0х4710:
    Graph of Load_Text
    Думаю, здесь все видно из комментариев. Основной вывод, который мы можем сделать из этой процедуры, это то, что в качестве параметра в регистре A0 нам передается указатель на массив текста. Как найти, откуда загрузился текст? Можно посмотреть в IDA на предмет Xref’ов на данную процедуру, однако вернее будет посмотреть в логе команд (‘trace.log’), найдя адрес первой команды нашей процедуры загрузки текста (00:46FC):

    00:5AE6 20 79 MOVE.l ($FFFF00F8),A0 A0=00006BD7 ; Команда загрузки указателя на текст 
    из оперативной памяти
    00:5AEC 4E B9 JSR ($000046FC) A0=00005B60 ; Это вызов нашей подпрограммы Load_Text
    00:46FC 32 78 MOVE.w ($F70E),A1 A0=00005B60 ; Первая команда в Load_Text 
    Легко увидеть, что искомый указатель имеет значение 0х5B60. Это область РОМа, а значит, наши поиски текста закончились. Перейдя по этому адресу в хексредакторе и подключив для наглядности простенькую табличку можно увидеть, что адрес текста мы определили правильно:
    Text
    Также поискав выше адрес в оперативной памяти, куда был записан указатель (0xFFFF00F8) наталкиваемся на такую команду:
    00:59BC 23 FC MOVE.l #$00005B60,($FFFF00F8) То есть указатель вписан в код жестко, и, очевидно, что он один и таблиц указателей нет. Поменяв указатель можно легко перенести весь текст в свободную часть РОМа или добавить его в конец.
  4. Спрайты.
    Краткая Теория:
    Сега может отображать до 80-ти спрайтов. Спрайты на SMD очень похожи по своей структуре на тайловую карту. Разумеется, они также должны иметь байты, описывающие положение спрайта на экране (координаты х и y). Кроме того, спрайты на Сеге могут иметь разные размеры: от 1х1 до 4х4 тайла - всего 16 вариантов. Еще спрайты имеют приоритет и составляют список спрайтов. Первый в этом списке отображается поверх прочих. Поэтому любой спрайт описывается четырьмя 16-битными word’ами (2 байта):

    -16 бит : Y
    -16 бит : Размер спрайта + номер следующего по приоритету спрайта.
    -16 бит : Тот же word, что и в тайловой карте (номер, отражение, приоритет, палитра)
    -16 бит : X

    Практика:
    Для начала, сохранимся (F5) прямо перед появлением спрайтовой надписи ‘Alien Soldier’ (сразу после растворения надписи ‘Treasure’). Загрузиться можно в любой момент клавишей F8. И сделаем дамп (‘.’) в момент нахождения спрайтовой надписи на экране. Пришло время заглянуть в файл ‘satb.txt’:

    F400
    OBJ #00: x=-128 y=-128 w= 8 h=16 t= 0 p=0000 
    OBJ #01: x= 64 y= 72 w= 8 h=24 t= 0 p=0000 
    OBJ #02: x= 96 y= 72 w= 8 h=32 t= 0 p=0000 
    OBJ #03: x=128 y= 72 w=16 h= 8 t= 0 p=0000 
    OBJ #04: x=160 y= 72 w=16 h=16 t= 0 p=0000 
    OBJ #05: x=192 y= 72 w=16 h=24 t= 0 p=0000 
    OBJ #06: x=224 y= 72 w=16 h=32 t= 0 p=0000 
    OBJ #07: x= 64 y=104 w=24 h= 8 t= 0 p=0000 
    OBJ #08: x= 96 y=104 w=24 h=16 t= 0 p=0000 
    OBJ #09: x=128 y=104 w=24 h=24 t= 0 p=0000 
    OBJ #10: x=160 y=104 w=24 h=32 t= 0 p=0000 
    OBJ #11: x=192 y=104 w=32 h= 8 t= 0 p=0000 
    OBJ #12: x=224 y=104 w=32 h=16 t= 0 p=0000 
    OBJ #13: x= 88 y=104 w=32 h=24 t=790 p=62c0 ;A 
    OBJ #14: x=100 y=104 w=32 h=32 t=812 p=6580 ;L
    OBJ #15: x=112 y=104 w= 8 h= 8 t=806 p=64c0 ;I
    OBJ #16: x=124 y=104 w= 8 h=16 t=798 p=63c0 ;E
    OBJ #17: x=136 y=104 w= 8 h=24 t=816 p=6600 ;N
    OBJ #18: x=160 y=104 w= 8 h=32 t=826 p=6740 ;S
    OBJ #19: x=172 y=104 w=16 h= 8 t=818 p=6640 ;O
    OBJ #20: x=184 y=104 w=16 h=16 t=812 p=6580 ;L
    OBJ #21: x=196 y=104 w=16 h=24 t=796 p=6380 ;D
    OBJ #22: x=208 y=104 w=16 h=32 t=806 p=64c0 ;I
    OBJ #23: x=220 y=104 w=24 h= 8 t=798 p=63c0 ;E
    OBJ #24: x=232 y=104 w= 8 h= 8 t=824 p=6700 ;R
    OBJ #25: x=172 y=104 w=24 h=24 t=818 p=6640 ;O
    OBJ #26: x=184 y=104 w=24 h=32 t=812 p=6580 ;L
    OBJ #27: x=196 y=104 w=32 h= 8 t=796 p=6380 ;D
    OBJ #28: x=208 y=104 w=32 h=16 t=806 p=64c0 ;I
    OBJ #29: x=220 y=104 w=32 h=24 t=798 p=63c0 ;E
    OBJ #30: x=232 y=104 w= 8 h= 8 t=824 p=6700 ;R
    ...

    Буквы я расставил для большей наглядности. Здесь первая цифра - номер спрайта в списке, затем следуют его координаты, затем ширина и высота в пикселях, номер спрайта в десятеричной системе и адрес в видеопамяти первого тайла спрайта. Из последнего в тайловом редакторе легко увидеть какая именно букве соответствует спрайт. Итак, первая буква это ‘A’, которая выводится спрайтом номер 13. Если учесть, что начало атрибутов спрайтов в видеопамяти по адресу 0xF400 и каждый спрайт занимает 4х2 байта, то 13 спрайт будет занимать 0xF468-0xf46F в видеопамяти, а word, который нас интересует (номер тайла 0xA316) будет находиться по адресам 0xf46c-0xf46d. Поставим хук по этому диапазону:
    hook_ppu1 1 f46c f46d
    Загружаем наш сейв и дожидаемся загрузки спрайтовой надписи:

    [00:0E34] VRAM DMA from FFE000 to F400 [0280]
    - SRC_H = $00:0E24 ~~ LEN_H = $00:0E18

    00:0E34 - команда в подпрограмме, постоянно вызываемой во время VBLANK’а, т.е. это именно тот DMA, который обновляет спрайты при каждом новом кадре. Спрайты пересылаются из области в оперативной памяти в видеопамять целиком (0x280 байт - это 80 спрайтов)Таким образом, интересующий нас word в оперативной памяти будет в интервале: 0xFFe06c -0xFFe06d.
    После установки хука, приходим к такой команде записи:
    00:2420 36 C1 MOVE.w D1,(A3)+ ...D1=0000C500
    Это одна из команд большой процедуры формирования атрибутов спрайтов, которая будет вызываться каждый раз, когда нужно поменять спрайт. Каждый раз она будет вызываться из разных мест, и ей будут передаваться разные аргументы. И здесь возникает проблема. Наш любимый трейсер записывает только те, команды, которые выполняются эмулятором впервые, а при повторном выполнении в .log файл ничего не сбрасывается. Поэтому размеры trace.log файла остаются разумными. Как видим, при первом выполнении интересующей нас команды, записывается совсем другой номер тайла (0x500). Всегда есть вариант исследовать подробно всю процедуру и ее вызовы дизассемблером, однако, в нашем случае, это чересчур хлопотно. Пришло время использовать тяжелую артиллерию - заставить трейсер записывать абсолютно все выполняемые команды. Для чего достаточно поставить хук на выполнение команд в диапазоне, который имеет начало и не имеет конца. В нашем случае хук выглядит так:
    hook_pc1 1 2420 -1
    Замечу, что в результате мы будем иметь чудовищный файл лога (несколько сотен мегабайт), а выполнение кода эмулятором будет очень замедленно (до одного фрейма в несколько секунд) из-за дополнительно возникающих операций записи в память и на жесткий диск. Чтобы хоть как-то снизить эти негативные явления, мы загрузимся из ранее сделанного сохранения непосредственно перед загрузкой интересующей нас надписи, сразу нажмем ‘,’ и ‘/’, а после появления спрайтовой надписи, тут же прекратим трассирование и выйдем из эмулятора.
    Полученный лог лучше не пытаться открыть обычными текстовыми редакторами. Lister из Total Commander прекрасно справляется с огромными текстовыми файлами и даже довольно шустро в них ищет. В полученном файле лога нам стоит найти команду по адресу 00:2420. Их оказывается несколько, но нам нужна та единственная, что записывает word по адресу FFFFE06C, поэтому в строку поиска стоит ввести следующее:

    00:2420 36 C1 MOVE.w D1,(A3)+ A0=0000240C A1=00000000 A2=FFFFED02 A3=FFFFE06C

    И строка не замедлит найтись. Следя снизу вверх, как изменяется регистр D1, и откуда в нем появился word A316, в конце-концов приходим к командам:

    242A 36 1C MOVE.w (A4)+,D3 A0=0000240C A1=00000000 A2=FFFFED02 A3=FFFFE068 A4=0001DD66
    242C 30 1C MOVE.w (A4)+,D0 A0=0000240C A1=00000000 A2=FFFFED02 A3=FFFFE068 A4=0001DD68 
    ... D3=00000316

    Итак, нужный нам номер спрайта берется из РОМа по смещению 0x1DD66, о чем свидетельствует реакция игры на изменение word’а по этому адресу. Внимательное изучение окружающего пространства дает понять, что структура хранения каждой буквы следующая:

    16 бит: номер тайла
    16 бит: размеры спрайта
    8 бит: координата Y
    8 бит: координата Х

    Далее идет следующий спрайт. Такая информация позволяет нам легко редактировать спрайтовую надпись.