Главная » Хабрахабр » Реверсим «Нейроманта». Часть 2: Рендерим шрифт

Реверсим «Нейроманта». Часть 2: Рендерим шрифт

И, если ты не видел первую часть, то рекомендую начать с неё, там я рассказываю о своей мотивации и делюсь первыми результатами. Привет, ты читаешь продолжение статьи, посвящённой реверс-инжинирингу «Нейроманта» — видеоигры, выпущенной компанией Interplay Productions в 1988 году по мотивам одноимённого романа Уильяма Гибсона.

Часть 1: Спрайты Реверсим «Нейроманта».

А мы продолжаем буквально с того же места, на котором остановились в прошлый раз.

PIC хранятся такие же битмапы, можно попробовать применить к . Исходя из предположения, что в . IMH-ресурсы. PIC-файлам те же самые функции, которыми я распаковывал . PIC (R1. Беру первый попавшийся . PIC) и читаю его в буфер:

R1.PIC: 4934 байта 0x0000: 0008 010a 000f 020c 0102 020f 0600 0f0f
0x0010: 0004 060e 0000 0607 0309 010f 0a0d 0d0f
0x0020: 981d 0000 2002 0611 74a2 38c5 d003 e9cb
0x0030: fb18 ac3b 8fbf 2713 459e 8c3f 3aa1 6ca7 ...
0x1330: 6f8a c3f5 d9c7 53e5 f47c d945 51d9 c753
0x1340: e5bf 1ebf 3f00

IMH: Отлично, этот файл начинается так же, как и любой .

  • 0x00:0x1F — 32 байта неизвестного назначения [в прошлой части я ошибочно полагал, что это палитра, но что это на самом деле — я до сих пор не выяснил];
  • word 0x20 — байтовый размер данных после декомпрессии [я понял это несколько позже, когда заметил, что размер выходных данных алгоритма декомпрессии совпадает с этим значением];
  • word 0x22 — ноль, отделяющий данные [вполне возможно, что это второй ворд длины, но здесь я не встречал таких больших значений].

Функция декомпрессии завершается успешно и возвращает длину разжатых данных: Теперь осторожно, под отладчиком, пробуем это декодировать.

R1.PIC_decompd: 7576 байт 0x0000: e901 1130 1113 0111 3011 1301 1130 1113
0x0010: 0111 3011 1301 3130 0113 f801 3330 1333 ...
0x1d80: 00ff 0816 00fe 8000 0108 0100 0180 ff08
0x1d90: 2b00 ff80 0288 0200

IMH-ресурсов. Но то, что я здесь вижу, несколько отличается от того, что я наблюдал после декомпрессии . Это нехорошо, ведь я использовал эти размеры для своевременной остановки алгоритма декодирования [функция decode_rle() из прошлой части]_ и построения . Нет, это по-прежнему выглядит как битмап, закодированный тем же Run-Length алгоритмом, но без заголовка _[struct rle_hdr_t из прошлой части], содержащего пиксельные размеры. Если предположить, что там закодировано только одно изображение, то для остановки алгоритма хватило бы и длины входных данных, а вот для сохранения картинки в . BMP-заголовка. BMP уже никак не обойтись без линейных размеров.

Допустим, что в каждом . Но будем решать проблемы по мере их поступления. Переделываю функцию decode_rle() так, чтобы она считала количество обработанных входных байт, а не раскодированных выходных. PIC действительно хранится по одному изображению, и их пиксельные размеры одинаковы. Завершается успешно, вернув мне битмап размером 34048 байта (17024 * 2, с учётом того, что в одном байте записаны значения двух пикселей): Пропускаю через неё буфер r1_pic_decompd.

R1.PIC_decoded: 17024 байта 0x0000: 0111 3011 1301 1130 1113 0111 3011 1301
0x0010: 1130 1113 0131 3013 1301 3330 1333 0333 ...
0x4270: 7777 7777 7777 7777 7708 8800 0088 8880

Но всё ещё проще, ведь среди экспортированных . Ширина и высота по-проженему неизвестны, но я думаю, что имею дело с бэкграундами локаций, а значит, можно попытаться замерить их прямо в игре. IMH). IMH-ресурсов нашлось изображение внутриигрового пользовательского интерфейса в натруральную величину (NEURO. Открываю изображение в Paint и делаю замеры:

BMP (всего 55 штук): Перемножение ширины и высоты даёт нужные 34048 байта — можно смело заворачивать этот и все остальные задники в .


BIH и . На этой волне можно попробовать расколоть типы ресурсов неизвестного назначения — . Действую по старой схеме: читаю рандомный файл; если похоже на то, что можно разжать — разжимаю; если похоже на RLE — декодирую. ANH. Беру, к примеру, содержимое ресурса R1. И, в общем-то, план сработал, но то, что внутри, так сказать… неоднозначно. BIH после декомпрессии:

R1.BIH_decompd: 2151 байт 0x0000: 00 00 00 00 00 00 fe 00 2a 00 00 00 00 00 fb 00 ......ю.*.....ы.
0x0010: fd 00 fc 00 00 00 c7 00 46 5a 5a 5f 9b 5f 9f 73 э.ь...З.FZZ_._џs
0x0020: 46 73 5a 77 00 5f 02 73 01 00 2e 00 7a 00 01 02 FsZw._.s....z...
0x0030: 13 00 03 04 05 12 00 7f 2c 00 08 14 00 2e 13 00 ........,.......
0x0040: 0e 3d 00 01 00 0e 12 00 ff ff 0e 14 00 00 00 03 .=......яя......
0x0060: 01 11 16 db 00 01 1f 02 20 0e 14 00 00 00 0e 12 ...Ы.... .......
0x0070: 00 ff ff 12 06 10 00 ff cc ff 05 10 00 06 07 00 .яя....яМя......
0x0080: 01 08 04 04 00 01 07 04 bc ff 01 0b 13 00 0c 03 ........јя......
0x0090: 17 05 10 00 ff 05 00 04 f9 ff 05 10 00 0c 07 00 ....я...щя......
0x00A0: 01 0f 04 0a 00 05 10 00 0d 0b 00 01 10 13 00 0c ................
0x00B0: 03 04 df ff 01 13 13 00 17 02 06 10 00 ff f8 ff ..Яя.........яшя
0x00C0: 05 10 00 17 07 00 01 19 04 ed ff 13 00 1b 02 01 .........ня.....
0x00D0: 1a 06 10 00 ff fc ff 05 10 00 1b 07 00 01 1d 04 ....яья.........
0x00E0: 04 00 01 1e 13 00 ff 00 04 ff ff e8 13 00 81 c3 ......я..яяи..ЃГ
0x00F0: 04 00 8b 1f 8b 47 14 01 87 ca 00 83 97 cc 00 00 .....G...К.ѓ—М..
0x0100: cb e8 01 00 90 5b 81 eb f4 00 c3 cb cb cb 59 6f Ли..ђ[Ѓлф.ГЛЛЛYo
0x0110: 75 27 76 65 20 6a 75 73 74 20 73 70 65 6e 74 20 u've just spent 0x0120: 74 68 65 20 6e 69 67 68 74 20 73 6c 65 65 70 69 the night sleepi
0x0130: 6e 67 20 66 61 63 65 2d 64 6f 77 6e 20 69 6e 20 ng face-down in 0x0140: 61 20 70 6c 61 74 65 20 6f 66 20 73 79 6e 74 68 a plate of synth
0x0150: 2d 73 70 61 67 68 65 74 74 69 20 69 6e 20 61 20 -spaghetti in a 0x0160: 62 61 72 20 63 61 6c 6c 65 64 20 74 68 65 20 43 bar called the C
0x0170: 68 61 74 73 75 62 6f 2e 20 41 66 74 65 72 20 72 hatsubo. After r
0x0180: 75 62 62 69 6e 67 20 74 68 65 20 73 61 75 63 65 ubbing the sauce
0x0190: 20 6f 75 74 20 6f 66 20 79 6f 75 72 20 65 79 65 out of your eye
0x01A0: 73 2c 20 79 6f 75 20 63 61 6e 20 73 65 65 20 43 s, you can see C
0x01B0: 68 69 62 61 20 73 6b 79 20 74 68 72 6f 75 67 68 hiba sky through
0x01C0: 20 74 68 65 20 77 69 6e 64 6f 77 2c 20 74 68 65 the window, the
0x01D0: 20 63 6f 6c 6f 72 20 6f 66 20 74 65 6c 65 76 69 color of televi
0x01E0: 73 69 6f 6e 20 74 75 6e 65 64 20 74 6f 20 61 20 sion tuned to a 0x01F0: 64 65 61 64 20 63 68 61 6e 6e 65 6c 2e 0d 0d 41 dead channel...A ...
0x0840: 3f 00 0d 0d 52 61 74 7a 20 72 65 66 75 73 65 73 ?...Ratz refuses
0x0850: 20 74 6f 20 74 61 6b 65 20 79 6f 75 72 20 63 72 to take your cr
0x0860: 65 64 69 74 73 2e 00 edits..

BIH имеют схожую структуру: некоторое количество байт сверху, за которыми следует полотно текста. [Да, это именно то, о чём вы подумали.] Другие файлы с именами R%n. Стартовая локация, к примеру, подгружает задник из R1. Очевидно, я имею дело с внутриигровыми строками, разделёнными по локациям, к которым они относятся [R1 — первая, R2 — вторая и так далее. Байты из вехней части организуют некую управляющую структуру, но в отрыве от кода разобрать её не удастся, попробую заглянуть в другие . PIC, а первый текст, который мы там увидим, это: You've just spent the night sleeping face-down in a plate of synth-spaghetti in a bar called the Chatsubo]. BIH файлы:

CORNERS.BIH_decompd: 128 байт 0x0000: ff ff f0 00 ff f0 0f ff ff 0f ff ff f0 ff ff ff яяр.яр.яя.яяряяя
0x0010: f0 ff ff ff 0f ff ff ff 0f ff ff ff 0f ff ff ff ряяя.яяя.яяя.яяя
0x0020: 00 00 ff ff 0f ff 00 ff 0f ff ff 0f 0f ff ff f0 ..яя.я.я.яя..яяр
0x0030: 0f ff ff f0 0f ff ff ff 0f ff ff ff 0f ff ff ff .яяр.яяя.яяя.яяя
0x0040: 0f ff ff ff 0f ff ff ff 0f ff ff ff f0 ff ff ff .яяя.яяя.яяяряяя
0x0050: f0 ff ff ff ff 0f ff ff ff f0 0f ff ff ff f0 00 ряяяя.яяяр.яяяр.
0x0060: ff ff ff f0 ff ff ff f0 ff ff ff f0 ff ff ff 0f яяяряяяряяяряяя.
0x0070: ff ff ff 0f ff ff f0 ff ff f0 0f ff 00 0f ff ff яяя.яяряяр.я..яя

ROOMPOS.BIH_decompd: 1160 байт 0x0000: 68 8f 75 0d 4b 69 00 02 8e 63 02 17 71 74 29 02 hЏu.Ki..Ћc..qt).
0x0010: 0e 63 02 17 68 8f 75 15 72 69 24 02 8e 63 02 17 .c..hЏu.ri$.Ћc..
0x0020: 15 74 7a 02 16 63 02 17 68 8f 75 0d 2a 69 00 02 .tz..c..hЏu.*i..
0x0030: 8e 63 02 17 08 74 8c 02 0e 63 02 17 70 63 75 0d Ћc...tЊ..c..pcu. ...
0x0470: 0e 6b 02 0f 6e 8f 75 0d 08 6f 8c 02 8e 69 02 11 .k..nЏu..oЊ.Ћi..
0x0480: 08 74 8c 02 0e 69 02 11 .tЊ..i..

BIH. То, что я здесь вижу, совсем не похоже на паттерн, который я наблюдал в R%n. Содержимое CORNERS. Они даже не похожи друг на друга! BIH, судя по названию, может иметь отношение к позиционированию объектов на локации, но его содержимое — непонятно.
Кроме этих, ещё есть: COPEN%n. BIH напоминает битмап, а ROOMPOS. BIH, HITACHI0. BIH, DB%nBIH, FIJU0. BIH хотя бы тем, что содержат в себе читабельный текст, а вот заголовки местами различаются. BIH
и много других, но они похожи на R%n. ANH. Оставлю это на потом и посмотрю, что там с .

Все . Здесь ситуация лучше. ANH, значит, они так или иначе относятся к локациям. ANH озаглавены как R%n. PIC и R%n. Их не много: если для всех n присутствуют R%n. ANH встречается лишь для некоторых n. BIH, то соответсвующий R%n. Они сжаты всё тем же алгоритмом, посмотрим, что внутри:

R1.ANH_decomp: 1100 байт 0x0000 04 00 4e 01 0f 00 17 00 00 00 0c 00 00 00 02 00 ..N.............
0x0010 0e 00 03 00 0e 00 01 00 0e 00 02 00 0e 00 02 00 ................
0x0020 0e 00 03 00 0e 00 01 00 0e 00 02 00 0e 00 0e 00 ................
0x0030 00 00 15 00 19 00 03 00 72 00 11 00 72 00 03 00 ........r...r...
0x0040 ba 00 23 25 03 17 fd 87 00 87 3e 00 ff 44 01 00 є.#%..э...>.яD.. ...
0x0160 73 00 04 00 73 00 03 00 73 00 14 00 73 00 04 00 s...s...s...s...
0x0170 00 00 03 00 00 00 03 00 73 00 04 00 73 00 04 00 ........s...s...
0x0180 00 00 03 00 00 00 03 00 73 00 04 00 73 00 0e 00 ........s...s...
0x0190 00 00 17 00 00 00 03 00 73 00 04 00 73 00 04 00 ........s...s...
0x01A0 00 00 03 00 00 00 03 00 73 00 04 00 73 00 1b 30 ........s...s..0
0x01B0 0b 10 01 00 ff 03 09 00 ff 30 08 00 fc 13 30 08 ....я...я0..ь.0. ...
0x0430 30 02 05 fe 08 88 02 08 fb 88 08 00 08 00 71 36 0..ю.€..ы€....q6
0x0440 02 05 fe 00 80 02 08 ff 88 03 08 00 ..ю.Ђ..я€...

Пока можно заняться другими вещами. Увы, здесь мало полезного, ну и ладно, буду разбираться по ходу дела. И это интересно, учитывая, что в . [Как-то раз, запустив в игре стартовую локацию, я зметил, что задник этой локации анимирован. Пока не проверено, но я думаю, что именно в . PIC лежат статичные изображения. ANH содержатся эти анимации.]

В процессе пришлось переехать с 2017-й студии на 2015-ю в связи с тем, что в первой сломано MFC. Некоторое время потратил на написание простенькой оконной утилитки — просмотрщика ресурсов. Первым же делом туда попали функции декомпрессии и декодирования ресурсов. В том же солюшене я создал проект библиотеки LibNeuroRoutines, в которую постепенно буду добавлять реверснутые процедуры из оригинальной игры. Удобно держать эти вещи отдельно.

[Сперва я надеялся просто извлечь шрифты, но, в итоге, это позволило мне добиться гораздо большего.] Сделать первые шаги в этом направлении было легко — изучая функцию main в дизассемблированном листинге, я обнаружил функцию, принимающую на вход адрес строки, отображаемой в главном меню игры — "New/Load": Перечитав заметку о реверс-инжиниринге на вики проекта ScummVM, решил пореверсить местный рендеринг текста.

... sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call sub_13C6E ; sub_13C6E("New/Load", 1, 0) ...

Выполнив функцию sub_13C6E под отладчиком, убеждаюсь в том, что именно она выводит на экран переданную строку:

При этом текст отрисовывается точно в центре рамки. И тут можно было бы начать её трассировать, но есть нюанс, она не принимает ничего похожего на координаты. Но при чём здесь аргументы 1 и 0? Может, она также отрисовывается в этой функции? Тогда я обратил внимание на вызов другой функции, сразу над sub_13C6E:

... sub ax, ax push ax push ax mov ax, 1 push ax mov ax, 0Ah push ax mov ax, 14h push ax mov ax, 5 push ax mov ax, 6 push ax call sub_13A9E ; sub_13A9E(6, 5, 20, 10, 1, 0, 0) add sp, 0Eh sub ax, ax push ax mov ax, 1 push ax mov ax, 5098h ; "New/Load" push ax call draw_string ; draw_string("New/Load", 1, 0) ...

Вероятно, что эти вызовы связаны через какие-то глобальные переменные. Потрассировав этот код я увидел, что функция sub_13A9E рендерит рамку, а функция draw_string — текст в ней. В любом случае, начинать разбираться лучше с sub_13A9E, тем более, что она принимает интересный набор аргументов.

Какой-то очевидной зависмости между числами здесь не наблюдается, а значит, будем трассировать sub_13A9E. Нужно сделать замеры и посмотреть — соотносятся ли как-нибудь измерения со значениями этих аргументов.

Вот, что там происходит: во первых, в сегменте данных заполняется некая структура (в комментариях я указал очерёдность выполнения и конкретные операции, вычисляющие записываемое значение):

; sub_13A9E(6, 5, 20, 10, 1, 0, 0)
; (a, b, c, d, e, f, g) .dseg ...
word[0x65FA]: 0x20 ; 10: word[0x6602] - 8 = 32
word[0x65FC]: 0x98 ; 11: word[0x6604] - 8 = 152
word[0x65FE]: 0x7F ; 12: word[0x6606] + 8 = 127
word[0x6600]: 0xAF ; 13: word[0x6608] + 8 = 175
word[0x6602]: 0x28 ; 2: b << 3 = 40
word[0x6604]: 0xA0 ; 3: c << 3 = 160
word[0x6606]: 0x77 ; 4: (d << 3) + word[0x6602] - 1 = 119 word[0x6608]: 0xA7 ; 5: (e << 3) + word[0x6604] - 1 = 167
word[0x660A]: 0x28 ; 6: word[0x6602] = 40
word[0x660C]: 0xA0 ; 7: word[0x6604] = 160
word[0x660E]: 0x77 ; 8: word[0x6606] = 119
word[0x6610]: 0xA7 ; 9: word[0x6608] = 167
word[0x6612]: 0x06 ; 1: a = 6
word[0x6614]: 0x00 ; 14: 0 ...
word[0x66D6]: 0x30 ; 17: (d << 2) + 8 = 48
word[0x66D8]: 0x02 ; 15: 2
word[0x66DA]: 0x22FB ; 16: seg11

Если пойти дальше и вычесть эти значения из тех, что записаны в 0x65FE и 0x6600, то мы получим 95 и 23. Нельзя не заметить, что в вордах по адресам 0x65FA и 0x65FC расположились координаты левого (32) верхнего (152) угла рамки, соответсвенно. Весьма занятно. Добавив по единице, выйдет аккурат ширина (96) и высота (24) рамки. Теперь, если проименовать аргументы функции draw_frame (sub_13A9E) от a до e [последние два аргумента функцией не используются, вероятно они просто добавлены компилятором], то можно вывести следующие выражения:

left = b * 8 - 8 = (b - 1) * 8
top = c * 8 - 8 = (c - 1) * 8
width = (d * 8) + (b * 8) + 8 - ((b * 8) - 8) = (d * 8) + 16 = (d + 2) * 8
height = (e * 8) + (c * 8) + 8 - ((c * 8) - 8) = (e * 8) + 16 = (e + 2) * 8

После заполнения рассмотренной структуры, по адресу [0x66DA]:[0x66D8] (22FB:0002 — сегмент: смещение) строится битмап-изображение рамки вычисленных размеров, которое затем построчно переносится в VGA-память, начиная с адреса (в VGA-буфере), соответсвующего вычисленной координате левого верхнего угла (152 * 320 + 32 = 0xBE20, A000:BE20):

так выглядит рамка в памяти

SEG11:
22FB:0002 0000 0000 3000 1800 22FB:000A 000000000000000000000000...000000000000000000000000
22FB:003A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:006A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:009A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:00CA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:00FA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:012A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:015A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:018A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:01BA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:01EA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:021A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:024A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:027A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:02AA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:02DA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:030A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:033A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:036A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:039A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:03CA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:03FA 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:042A 0FFFFFFFFFFFFFFFFFFFFFFF...FFFFFFFFFFFFFFFFFFFFFFF0
22FB:045A 000000000000000000000000...000000000000000000000000

VGA:
A000:BE20 000000000000000000000000...000000000000000000000000... + 0x140 (320)
A000:BF60 000F0F0F0F0F0F0F0F0F0F0F...0F0F0F0F0F0F0F0F0F0F0F00... + 0x140 (320)
A000:C0A0 000F0F0F0F0F0F0F0F0F0F0F...0F0F0F0F0F0F0F0F0F0F0F00... + 0x1000 (320 * 21)
A000:DAE0 000000000000000000000000...000000000000000000000000

Можно двигаться дальше, к функции draw_string.

Углубляясь в draw_string, зацепился за следующий участок кода:

loc_1DB47: mov bx, dx inc dx sub ax, ax mov al, ss:[bx] shl ax, 1 jz short loc_1DB97 shl ax, 1 shl ax, 1 add ax, 0FA6Eh mov si, ax mov cx, 8 mov bx, 4BA1h loc_1DB62: lodsb mov ah, al sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx] stosb add di, ss:4BA5h loop loc_1DB62 sub di, ss:4BA7h jmp short loc_1DB47 loc_1DB97: ...

Иду от метки loc_1DB47: Именно "зацепился", из-за большого скопления инструкций lodsb, stosb, xlat,[для себя я сделал вывод, что именно эти инструкции делают большую часть полезной работы, в конце-концов, всё это программирование сводится к тривиальному перемещению данных из одной области памяти в другую, не так ли?] и начал его трассировать.

  • в регистре dx находится адрес исходной строки "New/Load", сохраняем его в bx и инкрементируем;
  • зануляем ax (sub ax, ax);
  • помещаем в al код символа исходной строки, на которую указывает bx (mov al, ss:[bx], al = 0x4E ('N'));
  • сдвигаем код символа на один бит влево и, если результат равен нулю, прыгаем на loc_1DB97 (проверка на нуль-терминатор);
  • двигаем ax ещё на два бита влево и прибавляем 0xFA6E (вместе с предыдущим сдвигом равносильно: ax = ax * 8 + 0xFA6E, ax = 0xFCDE);
  • сохраняем ax в si (а значит в ax — адрес);
  • сохраняем в cx — 8, а в bx0x4BA1 (вероятно, тоже адрес).

Учитывая дальнейшую инструкцию loop loc_1DB62, здесь был заготовлен цикл на 8 (cx) итераций, разбираюсь с ним:

  • инструкцией lodsb в al загружается значение байта по адресу ds:si, si после этого инкрементируется. В моём случае ds:si = F000:FCDE, в al загружается значение 0xC6, si становится равным 0xFCDF;
  • младший байт ax записывается в старший (mov ah, al, ax = 0xC6C6);
  • al зануляется, ax циклически сдвигается на два бита влево (ax = 0x1803);
  • инструкция xlat использует al как индекс в байтовом массиве по адресу ds:bx и сохраняет записанное там значение в al, при этом сегмент ds может быть переопределён. Здесь идёт обращение к массиву по адресу ss:bx (47EA:4BA1 = ), и в результате выполнения инструкции: al = ss:bx[al] = 0x00;
  • иструкцией stosb значение из al записывается по адресу es:di, di после этого инкрементируется. У меня es:di = 22FB:0192, и это важно, ведь именно там расположен битмап рамки, в которой будет отображён текст. После выполнения инструкции по адресу 22FB:0192 будет записано значение 0x00 (al), а di станет равным 0x0193.

После завершения цикла к di прибавляется ворд, сохранённый по адресу ss:4BA7 (0x17C), и программа прыгает назад, на метку loc_1DB47. Последние три шага повторяются трижды, затем к di прибавляется ворд, сохранённый по адресу ss:4BA5 (0x2C), и инструкция loop ещё 7 раз прогоняет код от метки loc_1DB62 до самой себя.

В результате, в рамке формируется заданный текст [подсветите нули, или уменьшите страницу]: Таким вот замысловатым способом здесь по очереди обрабатываются все символы исходной строки.

FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFF00FFF00FFFFFFFFFFFFFFFFFFFFFF00F0000FFFFFFFFFFFFFFFFFFFFFFF000FFF
FFF000FF00FFFFFFFFFFFFFFFFFFFFF00FFF00FFFFFFFFFFFFFFFFFFFFFFFFF00FFF
FFF0000F00FF0000FFF00FFF00FFFF00FFFF00FFFFFF0000FFFF0000FFFFFFF00FFF
FFF00F0000F00FF00FF00F0F00FFF00FFFFF00FFFFF00FF00FFFFFF00FFF00000FFF
FFF00FF000F000000FF0000000FF00FFFFFF00FFF0F00FF00FFF00000FF00FF00FFF
FFF00FFF00F00FFFFFF0000000F00FFFFFFF00FF00F00FF00FF00FF00FF00FF00FFF
FFF00FFF00FF0000FFFF00F00FF0FFFFFFF0000000FF0000FFFF000F00FF000F00FF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

Это явно находится за пределами памяти самой программы, а значит нужно гуглить схему памяти MS-DOS. Здесь, в приципе, понятно всё, кроме магического числа 0xFA6E, прибавив к которому код символа, умноженный на восемь, мы получили адрес в сегменте F000. Учитывая сегментную адресацию, запись FFA6:E эквивалентна F000:FA6E, а по этому адресу "зашит" шрифт для отображения символов в родной досовской кодировке CP437. И нагуглил, конкретно, там интересует вот что: FFA6:E ROM graphics character table. Вот как это работает на примере буквы 'N': На каждый символ там отведено по 8 байт.

1. берём код символа: 0x4E
2. умножаем на восемь: 0x4E * 8 = 0x270
3. смещаемся от начала шрифта на полученное значение: 0xFA6E + 0x270 = 0xFCDE
4. читаем восемь байт по этому адресу: C6 E6 F6 DE CE C6 C6 00
5. записывем прочитанные байты в столбик, в двоичной системе: C6: 11000110 E6: 11100110 F6: 11110110 DE: 11011110 (здесь единицами нарисована буква 'N') CE: 11001110 C6: 11000110 C6: 11000110 00: 00000000

А вот этот код:

sub al, al rol ax, 1 rol ax, 1 xlat byte ptr ss:[bx]

можно проиллюстрировать следующим образом:

[Чем я и занялся, оставив позиционирование текста на потом.] Всё достоточно просто для того, чтобы реализовать эту логику самостоятельно и вывести шрифт на экран.

В качестве мультимедиа-бэкенда решил использовать SFML. Под это дело, в одном солюшене с ResourceBrowser и LibNeuroRoutines, создал проект NeuromancerWin32. SDL2 мне нравилась, в том числе, тем, что она реализована на C и, соответсвенно, из коробки имеет C-совместимый интерфейс. До этого у меня уже был очень позитивный опыт работы с SDL2, но здесь захотелось попробовать что-нибудь новое. Чтобы не усложнять, свой проект я буду вести на C, и, к счастью, SFML имеет официальный С-биндинг — CSFML. SFML, в свою очередь, написана на C++.

Вот, например, всё, что нужно для создания окна: Уже со старта CSFML порадовала простотой использования.

sfEvent event;
sfVideoMode mode = { 320, 200, 32 };
sfRenderWindow *window = sfRenderWindow_create(mode, "NeuromancerWin32", sfClose, NULL); while (sfRenderWindow_isOpen(window))
{ while (sfRenderWindow_pollEvent(window, &event)) { if (event.type == sfEvtClosed) { sfRenderWindow_close(window); } } sfRenderWindow_clear(window, sfBlack); sfRenderWindow_display(window)
} sfRenderWindow_destroy(window);

В этом режиме рисование сводится к записи значений цвета пикселей в VGA-память (A000:0000, 320 * 200 байт). Оригинальная игра для вывода графики на экран использует 256-цветный режим VGA (mode 0x13). Я решил действовать схожим образом, но с поправкой на то, что будет использоваться 32-битный видеорежим. Один байт, при этом, соответсвует одному пикселю. В этом буфере я буду рисовать, а затем, при помощи SFML, выводить содержимое на экран: Таким образом, я завёл себе буфер uint8_t *g_vga[320*200*4] (каждый пиксель в 32-битном режиме представлен 4-мя компонентами — RGBA), который будет служить мне аналогом VGA-памяти из оригинала.

sfRenderWindow *g_ window = NULL;
sfTexture *g_texture = NULL; uint8_t *g_vga[320*200*4];
... g_texture = sfTexture_create(320, 200); ... void render()
{ sfTexture_updateFromPixels(g_texture, g_vga, 320, 200, 0, 0); sfSprite *sprite = sfSprite_create(); sfSprite_setTexture(sprite, g_texture, 1); sfRenderWindow_clear(g_window, sfBlack); sfRenderWindow_drawSprite(g_window, sprite, NULL); sfRenderWindow_display(g_window); sfSprite_destroy(sprite);
} ... sfTexture_destroy(g_texture);

Тестируем:

for (int i = 0; i < 320 * 240 * 4; i++)
{ g_vga[i] = rand() % 256;
} while (sfRenderWindow_isOpen(g_window))
{ ... render();
}

После этого написал функцию, которая по заднным координатам мапает битмап заданных размеров на VGA-буфер: void draw_to_vga(int32_t l, int32_t t, uint32_t w, uint32_t h, uint8_t *pixels) [логика переноса битмапа на VGA, на мой взгляд, тривиальна]: То, что нужно.

uint8_t red_rectangle[96*48];
memset(red_rectangle, 0x44, 96*48); draw_to_vga(10, 10, 96, 48, red_rectangle); ... render();

Реализовал процедуру, формирующую битмап (8x8) с заданным символом:

static uint8_t cp437_font[1024] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00 NULL ... 0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00, // 0x41 A 0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00, // 0x42 B 0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00, // 0x43 C ...
};
static uint8_t cp437_font_pixels[4] = { 0xFF, 0xF0, 0x0F, 0x00
};
static uint8_t cp437_font_mask[4] = { 0xC0, 0x30, 0x0C, 0x03
}; void build_character(char c, uint8_t *dst)
{ uint32_t index = c * 8; /* unprintable */ if (c < 0x20 || c > 0x7E) { return; } memset(dst, 0, 32); for (int i = 0; i < 8; i++) { uint8_t al = cp437_font[index++]; for (int j = 0; j < 4; j++) { dst[i * 4 + j] = cp437_font_pixels[(al & cp437_font_mask[j]) >> (6 - j * 2)]; } }
}

Посимвольно вывел алфавит на экран:

memset(g_vga, 0xFF, 320 * 200 * 4); uint8_t character_bm[32];
int left = 2, top = 2; for (char c = 0x20; c <= 0x7E; c++)
{ if (left + 8 >= 320) { left = 2; top += 10; } build_character(c, character_bm); draw_to_vga(left, top, 8, 8, character_bm); left += 8;
} render();

На основе build_character сделал функцию build_string(char *s, uint32_t w, uint32_t h, uint8_t *dst), которая печатает строку целиком:

memset(g_vga, 0xFF, 320 * 200 * 4); uint8_t string[320 * 20]; build_string("The future is here.\n" "It’s just not widely distributed yet.", 320, 20, dst);
draw_to_vga(20, 88, 320, 20, string); render();

На самом деле, на момент написания этого текста у меня сделано почти работающее главное меню и плюс-минус реализована система рендеринга из оригинала. Изначально я планировал написать больше. Так что уже в следующей я добью меню и рендеринг и выкачу это всё в один присест. Но эта часть получилась и без того больше предыдущей, и её пора заканчивать.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Наша книжная полка С#-программиста. А что у вас?

Привет! Обычно мы рекомендуем несколько источников, сопровождая их своими комментариями, почему именно они будут полезны. Будущие студенты Veeam Academy часто спрашивают нас о книгах, которые были бы полезны при подготовке к поступлению на наш курс по программированию на С#. Поэтому ...

[Из песочницы] Компрессия больших массивов простых чисел

Естественный формат хранения в виде целых чисел той или иной разрядности страдает при этом некоторыми недостатками, которые становятся существенными при росте объема данных. Свойства простых чисел редко позволяют работать с ними иначе, чем в виде заранее вычисленного массива — и ...