Реверс-инжиниринг «Казаков», часть вторая: Увеличение очереди

7622f50f486c4d1b878982981077d70b.png

В большинстве случаев слово «очередь» не вызывает положительных эмоций, тем более в сочетании со словом «увеличить». Но если вы любите играть с миллионами единиц ресурсов к началу игры, чтобы на десятой минуте бросить в бой тысячи солдат, то стандартного заказа по пять боевых единиц единиц с помощью клавиши Shift вам будет мало. Вот если бы можно было заказывать по 20 или по 50 солдат, или ещё лучше — иметь несколько разных клавиш-модификаторов…

Вступление


После публикации предыдущей статьи и возникшего интереса со стороны сообщества LCN меня спросили, смогу ли я увеличить объём очереди заказа боевых единиц с пяти до 20 или до 50. «Почему бы и нет», подумал я, «да и вообще — если повезёт, то нужно будет только один байт с 0×05 на 0×14 заменить, и всё».

Если бы я знал тогда, чем это обернётся… Но я не знал, так что поехали!

С чего начнём?


Естественно, с поиска нужного участка машинного кода. Тут есть выбор: Можно искать место, в котором обрабатываются нажатия кнопок мыши и попытаться отследить код до ветки, отвечающей за заказ боевых единиц. Или же можно обойти все места, в которых проверяется состояние клавиши Shift. Мне второй вариант показался более перспективным. Запускаем всеми любимую для таких дел программу и смотрим в список импортированных функций.

Хм, GetKeyState звучит многообещающе. Что там у нас по перекрёстным ссылкам? Сто восемнадцать вызовов? Многовато, нужно отсеять те вызовы, в которых не проверяется клавиша Shift. Функция GetKeyState принимает только один параметр, а именно код клавиши, который для Shift равняется 0×10. В моём dmcr.exe это соответствует такому куску машинного кода:

push        10h                             6A 10
call    GetKeyState                     FF 15 EC C1 5C 00


Поиск этой последовательности байт выдал 38 адресов, на которых вызывается GetKeyState (VK_SHIFT). Расставляем по точке останова на каждый из них, запускаем отладчик и снимаем лишние, пока не доберёмся до нужной процедуры. Если быть точным, то процедур две: Одна для заказа боевых единиц и одна для отмены. Но так как они различаются только адресом вызываемой в них функции, то далее мы будем рассматривать их как одну процедуру.

Вот что нас там ждёт:

300253d1339f4f3aab887228bf9f1932.png

Ну конечно. Что делает компилятор, когда видит маленький цикл с небольшим, но постоянным количеством выполнений? Правильно, разворачивает его в повторяющуюся последовательность инструкций тела цикла. Надежда на однобайтовый патч непринуждённо помахала ручкой.

Патч первый или зацикливаемся на ассемблере


Для цикла нам потребуются счётчик, инкрементирование, сравнение и условный прыжок. Тело цикла оставим без изменений за исключением правки смещения вызова функции соответственно новому адресу инструкции call.

После некоторого курения мануалов изучения документации был создан следующий набросок машинного кода:

; Сохраняем регистр перед изменениями
push    cx                              66 51
; Обнуляем регистр
xor     cx, cx                          66 31 C9

; Тело цикла
; Сохраняем регистр-счётчик перед каждым исполнением цикла
push    cx                              66 51
; Отрезок кода, отвечающий за заказ боевой единицы
mov     dx, word ptr [ebp+arg_0]        66 8B 55 F0
push    edx                             52
xor     eax, eax                        33 C0
mov     al, byte_10FC290                A0 90 C2 0F 01
xor     eax, 85h                        35 85 00 00 00
push    eax                             50
call    sub_4FD01E                      E8 .. .. .. ..
add     esp, 8                          83 C4 08

; Восстанавливаем, инкрементируем и сравниваем регистр-счётчик
pop     cx                              66 59
inc     cx                              66 41
cmp     cx, 14h                         66 83 F9 14
; Прыжок в начало цикла, если счётчик меньше 20
jl                                      7C DA

; Восстанавливаем регистр после окончания цикла
pop     cx                              66 59


Небольшое отступление про cx и 66h
Регистр cx считается «регистром цикла» и используется вместе с инструкцией loop. Хоть я и решил использовать вместо loop обычную комбинацию из inc, cmp и jl, в качестве регистра-счётчика я всё равно оставил cx. Однако при подборе машинных команд у меня возникла проблема: Что бы я не делал, в итоге всегда выходили операции с регистром ecx вместо его младшего брата. Пришлось прибегнуть к помощи онлайн ассемблера. Какого же было моё удивление, когда в ответ на мой набросок он выдал по большей части те же самые операционные коды, но с префиксом 0×66. В документации операционный код 66h описывается как «Operand-size override prefix. Reserved and may result in unpredictable behavior». При таком описании неудивительно, что он не бросился мне в глаза раньше. Префикс 0×66 заставляет машинные коды, оперирующие 32-битными регистрами переключиться на их 16-битных собратьев и наоборот.


Несмотря на то, что этот патч приводит к желаемому результату, он имеет один большой недостаток: Не вмешиваясь в логику игры можно переопределить размер очереди производства, создаваемой с помощью клавиши-модификатора Shift, но не более того. Патчить очередь перед каждой игрой в зависимости от условий игры не очень привлекательная перспектива, поэтому в сообществе довольно быстро было озвучено желание иметь разные модификаторы очереди на клавишах Shift, Alt, и ~. Что ж, вызов принят!

Патч второй или «программа максимум»


Заменив последовательность из пяти повторяющихся блоков одним циклом, мы освободили приличное количество байт. Но как в образовавшееся пространство встроить проверку нескольких клавиш и регулировку цикла в зависимости от результата? Самое простое на мой взгляд решение это последовательные вызовы GetKeyState, чередующиеся с присвоениями регистру соответствующего значения, с которым будет сравниваться счётчик в цикле. Если вызов GetKeyState показывает, что клавиша не нажата, то инструкция присвоения перепрыгивается. Таким образом у нас вместо развилок в зависимости от состояния клавиш будет ряд последовательных проверок и присвоений, завершающийся одним циклом:

; В том случае, если ни одна клавиша не нажата, цикл выполнится один раз
mov     ebx, 01h                        BB 01 00 00 00

; Проверка клавиши
push    10h                             6A 10
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
; В случае отрицательного результата перепрыгиваем mov, оставляя предыдущее значение в регистре
jz                                      74 05
mov     ebx, 05h                        BB 05 00 00 00

; Следующая клавиша
push    12h                             6A 12
call    GetKeyState                     FF 15 EC C1 5C 00
[...]


В этот раз я решил использовать регистр ebx для сохранения количества исполнений цикла и регистр esi как счётчик цикла. Для этого есть две причины. Следуя соглашению о вызове функций эти регистры являются «постоянными», т.е. если в теле функции в них вносятся изменения, то функция обязана сохранить их значения в стеке и восстановить их перед завершением. Это освобождает меня от необходимости самому выполнять push и pop перед каждым исполнением цикла. Вторая причина в том, что в отличии от регистра cx мне больше не требуется префикс 0×66, а это экономия одного байта на каждой операции с регистрами кроме mov.

В итоге мы имеем клавиши-модификаторы Shift, Alt, TAB, F1 и F2. От клавиши ~ пришлось отказаться, так как на разных раскладках ей соответствуют разные идентификаторы, например VK_OEM_3 и VK_OEM_5.

Финальный код патча
; Сохраняем регистр перед изменением
push    ebx                             53

; В том случае, если ни одна клавиша не нажата, цикл выполнится один раз
mov     ebx, 01h                        BB 01 00 00 00

; Shift: 5 боевых единиц
push    10h                             6A 10
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
jz                                      74 05
mov     ebx, 05h                        BB 05 00 00 00

; Alt: 20 боевых единиц
push    12h                             6A 12
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
jz                                      74 05
mov     ebx, 14h                        BB 14 00 00 00

; TAB: 50 боевых единиц
push    09h                             6A 09
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
jz                                      74 05
mov     ebx, 32h                        BB 32 00 00 00

; F1: 15 боевых единиц
push    70h                             6A 70
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
jz                                      74 05
mov     ebx, 0Fh                        BB 0F 00 00 00

; F2: 36 боевых единиц
push    71h                             6A 71
call    GetKeyState                     FF 15 EC C1 5C 00
movsx   ecx, ax                         0F BF C8
and     ecx, 8000h                      81 E1 00 80 00 00
test    ecx, ecx                        85 C9
jz                                      74 05
mov     ebx, 24h                        BB 24 00 00 00

; Сохраняем и обнуляем регистр-счётчик цикла
push    esi                             56
xor     esi, esi                        31 F6

; Тело цикла
mov     dx, word ptr [ebp+arg_0]        66 8B 55 F0
push    edx                             52
xor     eax, eax                        33 C0
mov     al, byte_10FC290                A0 90 C2 0F 01
xor     eax, 85h                        35 85 00 00 00
push    eax                             50
call    sub_4FD01E                      E8 .. .. .. ..
add     esp, 8                          83 C4 08

; Инкрементируем, сравниваем, прыгаем в начало цикла
inc     esi                             46
cmp     esi, ebx                        39 DE
jl                                      7C E1

; Восстанавливаем регистры
pop     esi                             5E
pop     ebx                             5B


Послесловие


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

© Habrahabr.ru