by Henadzi Matuts
published on https://habr.com/post/357972/
Привет, ты читаешь продолжение статьи, посвящённой реверс-инжинирингу «Нейроманта» - видеоигры, выпущенной компанией Interplay Productions в 1988 году по мотивам одноимённого романа Уильяма Гибсона. И, если ты не видел первую часть, то рекомендую начать с неё, там я рассказываю о своей мотивации и делюсь первыми результатами.
А мы продолжаем буквально с того же места, на котором остановились в прошлый раз.
Исходя из предположения, что в .PIC
хранятся такие же битмапы, можно попробовать применить к .PIC
-файлам те же самые функции, которыми я распаковывал .IMH
-ресурсы. Беру первый попавшийся .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
-ресурсов. Нет, это по-прежнему выглядит как битмап, закодированный тем же Run-Length алгоритмом, но без заголовка [struct rle_hdr_t
из прошлой части], содержащего пиксельные размеры. Это нехорошо, ведь я использовал эти размеры для своевременной остановки алгоритма декодирования [функция decode_rle()
из прошлой части] и построения .BMP
-заголовка. Если предположить, что там закодировано только одно изображение, то для остановки алгоритма хватило бы и длины входных данных, а вот для сохранения картинки в .BMP
уже никак не обойтись без линейных размеров.
Но будем решать проблемы по мере их поступления. Допустим, что в каждом .PIC
действительно хранится по одному изображению, и их пиксельные размеры одинаковы. Переделываю функцию decode_rle()
так, чтобы она считала количество обработанных входных байт, а не раскодированных выходных. Пропускаю через неё буфер r1_pic_decompd
. Завершается успешно, вернув мне битмап размером 34048 байта (17024 * 2, с учётом того, что в одном байте записаны значения двух пикселей):
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
-ресурсов нашлось изображение внутриигрового пользовательского интерфейса в натруральную величину (NEURO.IMH
). Открываю изображение в Paint и делаю замеры:
Перемножение ширины и высоты даёт нужные 34048 байта - можно смело заворачивать этот и все остальные задники в .BMP
(всего 55 штук):
На этой волне можно попробовать расколоть типы ресурсов неизвестного назначения - .BIH
и .ANH
. Действую по старой схеме: читаю рандомный файл; если похоже на то, что можно разжать - разжимаю; если похоже на RLE - декодирую. И, в общем-то, план сработал, но то, что внутри, так сказать… неоднозначно. Беру, к примеру, содержимое ресурса 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..
[Да, это именно то, о чём вы подумали.] Другие файлы с именами R%n.BIH
имеют схожую структуру: некоторое количество байт сверху, за которыми следует полотно текста. Очевидно, я имею дело с внутриигровыми строками, разделёнными по локациям, к которым они относятся [R1
- первая, R2
- вторая и так далее. Стартовая локация, к примеру, подгружает задник из R1.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..
То, что я здесь вижу, совсем не похоже на паттерн, который я наблюдал в R%n.BIH
. Они даже не похожи друг на друга! Содержимое CORNERS.BIH
напоминает битмап, а ROOMPOS.BIH
, судя по названию, может иметь отношение к позиционированию объектов на локации, но его содержимое - непонятно.
Кроме этих, ещё есть: COPEN%n.BIH
, DB%nBIH
, FIJU0.BIH
, HITACHI0.BIH
и много других, но они похожи на R%n.BIH
хотя бы тем, что содержат в себе читабельный текст, а вот заголовки местами различаются. Оставлю это на потом и посмотрю, что там с .ANH
.
Здесь ситуация лучше. Все .ANH
озаглавены как R%n.ANH
, значит, они так или иначе относятся к локациям. Их не много: если для всех n
присутствуют R%n.PIC
и R%n.BIH
, то соответсвующий R%n.ANH
встречается лишь для некоторых 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
, в которую постепенно буду добавлять реверснутые процедуры из оригинальной игры. Первым же делом туда попали функции декомпрессии и декодирования ресурсов. Удобно держать эти вещи отдельно.
Перечитав заметку о реверс-инжиниринге на вики проекта ScummVM, решил пореверсить местный рендеринг текста. [Сперва я надеялся просто извлечь шрифты, но, в итоге, это позволило мне добиться гораздо большего.] Сделать первые шаги в этом направлении было легко - изучая функцию main
в дизассемблированном листинге, я обнаружил функцию, принимающую на вход адрес строки, отображаемой в главном меню игры - “New/Load
”:
...
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
Нельзя не заметить, что в вордах по адресам 0x65FA
и 0x65FC
расположились координаты левого (32) верхнего (152) угла рамки, соответсвенно. Если пойти дальше и вычесть эти значения из тех, что записаны в 0x65FE
и 0x6600
, то мы получим 95 и 23. Добавив по единице, выйдет аккурат ширина (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:
...
Именно “зацепился”, из-за большого скопления инструкций lodsb
, stosb
, xlat
,[для себя я сделал вывод, что именно эти инструкции делают большую часть полезной работы, в конце-концов, всё это программирование сводится к тривиальному перемещению данных из одной области памяти в другую, не так ли?] и начал его трассировать. Иду от метки loc_1DB47
:
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, а в bx
- 0x4BA1
(вероятно, тоже адрес).Учитывая дальнейшую инструкцию 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 = {0xFF, 0xF0, 0x0F, 0x00}
), и в результате выполнения инструкции: al = ss:bx[al] = 0x00
;stosb
значение из al
записывается по адресу es:di
, di
после этого инкрементируется. У меня es:di = 22FB:0192
, и это важно, ведь именно там расположен битмап рамки, в которой будет отображён текст. После выполнения инструкции по адресу 22FB:0192
будет записано значение 0x00
(al
), а di
станет равным 0x0193
.Последние три шага повторяются трижды, затем к di
прибавляется ворд, сохранённый по адресу ss:4BA5
(0x2C
), и инструкция loop
ещё 7 раз прогоняет код от метки loc_1DB62
до самой себя. После завершения цикла к di
прибавляется ворд, сохранённый по адресу ss:4BA7
(0x17C
), и программа прыгает назад, на метку loc_1DB47
.
Таким вот замысловатым способом здесь по очереди обрабатываются все символы исходной строки. В результате, в рамке формируется заданный текст [подсветите нули, или уменьшите страницу]:
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFF00FFF00FFFFFFFFFFFFFFFFFFFFFF00F0000FFFFFFFFFFFFFFFFFFFFFFF000FFF
FFF000FF00FFFFFFFFFFFFFFFFFFFFF00FFF00FFFFFFFFFFFFFFFFFFFFFFFFF00FFF
FFF0000F00FF0000FFF00FFF00FFFF00FFFF00FFFFFF0000FFFF0000FFFFFFF00FFF
FFF00F0000F00FF00FF00F0F00FFF00FFFFF00FFFFF00FF00FFFFFF00FFF00000FFF
FFF00FF000F000000FF0000000FF00FFFFFF00FFF0F00FF00FFF00000FF00FF00FFF
FFF00FFF00F00FFFFFF0000000F00FFFFFFF00FF00F00FF00FF00FF00FF00FF00FFF
FFF00FFF00FF0000FFFF00F00FF0FFFFFFF0000000FF0000FFFF000F00FF000F00FF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
Здесь, в приципе, понятно всё, кроме магического числа 0xFA6E
, прибавив к которому код символа, умноженный на восемь, мы получили адрес в сегменте F000
. Это явно находится за пределами памяти самой программы, а значит нужно гуглить схему памяти MS-DOS. И нагуглил, конкретно, там интересует вот что: FFA6:E ROM graphics character table
. Учитывая сегментную адресацию, запись FFA6:E
эквивалентна F000:FA6E
, а по этому адресу “зашит” шрифт для отображения символов в родной досовской кодировке CP437. На каждый символ там отведено по 8 байт. Вот как это работает на примере буквы ‘N’:
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]
можно проиллюстрировать следующим образом:
Всё достоточно просто для того, чтобы реализовать эту логику самостоятельно и вывести шрифт на экран. [Чем я и занялся, оставив позиционирование текста на потом.]
Под это дело, в одном солюшене с ResourceBrowser и LibNeuroRoutines, создал проект NeuromancerWin32. В качестве мультимедиа-бэкенда решил использовать SFML. До этого у меня уже был очень позитивный опыт работы с SDL2, но здесь захотелось попробовать что-нибудь новое. SDL2 мне нравилась, в том числе, тем, что она реализована на C и, соответсвенно, из коробки имеет C-совместимый интерфейс. SFML, в свою очередь, написана на C++. Чтобы не усложнять, свой проект я буду вести на C, и, к счастью, SFML имеет официальный С-биндинг - CSFML.
Уже со старта 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);
Оригинальная игра для вывода графики на экран использует 256-цветный режим VGA (mode 0x13). В этом режиме рисование сводится к записи значений цвета пикселей в VGA-память (A000:0000
, 320 * 200
байт). Один байт, при этом, соответсвует одному пикселю. Я решил действовать схожим образом, но с поправкой на то, что будет использоваться 32-битный видеорежим. Таким образом, я завёл себе буфер uint8_t *g_vga[320*200*4]
(каждый пиксель в 32-битном режиме представлен 4-мя компонентами - RGBA), который будет служить мне аналогом VGA-памяти из оригинала. В этом буфере я буду рисовать, а затем, при помощи SFML, выводить содержимое на экран:
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();
Изначально я планировал написать больше. На самом деле, на момент написания этого текста у меня сделано почти работающее главное меню и плюс-минус реализована система рендеринга из оригинала. Но эта часть получилась и без того больше предыдущей, и её пора заканчивать. Так что уже в следующей я добью меню и рендеринг и выкачу это всё в один присест.
Реверсим «Нейроманта». Часть 3: Добили рендеринг, делаем игру