О третьих героях, понятное дело, все знают. Недели времени, потерянного за hotseat-ом, вызубренные характеристики юнитов, тактика застройки, развития. Еженедельные посиделки с друзьями за героями c кофе и рогаликами. Здорово, в общем было. А вот на днях захотелось повторить и сыграть по сети. Уже ж разъехались все, hotseat не прокатит. Скачали дистрибутив клинка Армагеддона, настроили hamachi, да не заладилось что-то. Пинг большой почему-то, не завелось ничего. Ну, занятые все, разбираться некогда, да и неохота. Не пошла игра.
А герои то установлены, желание играть есть. Решил кампанию пройти (к своему стыду, так ни разу не хватило терпения пройти ее в прошлом). Запустил, выбрал кампанию возрождения Эратии, нажал на эпизод «Жертвы войны». И я не знаю, интуиция, наверное, появляется какая-то за то время, пока программированием занимаешься. В общем, показали мне герои такую картинку:
И, как часто у меня случается, игра закончилась, а началась более интересное и захватывающее занятие.
Запускаю в отладчике, выбираю тот самый, проблемный пункт. На этом участке кода происходит исключение при доступе к памяти на строчке mov bl, [eax+ecx+1F879h].
.text:004567A3 mov ecx, dword_68C818
.text:004567A9 test ecx, ecx
.text:004567AB jl short loc_4567F2
.text:004567AD mov dl, [eax+edi+1F879h]
.text:004567B4 mov bl, [eax+ecx+1F879h]
Смотрю на значения регистров: EDI равен нолю, EAX указывает на правильный участок памяти, ECX просто огромный и указывает в никуда. Скорее всего, проблема в нем. Теперь нужно найти откуда в него заносится значение. Поднимаюсь чуть выше по коду, нахожу строку mov ecx, dword_68C818. Ага, значит регистр ECX заполняется из глобальной переменной. Открываю окно перекрестных ссылок, чтобы посмотреть, где в эту переменную пишут значение.
Что интересно, значение в нее пишут в этой же функции. Вообще странное дело. Переменная глобальная, а используется только в одной функции. По-всему выходит, что она должна на стеке создаваться? Ну ладно, наверное я чего-то не понимаю. Смотрю кто же в нее пишет. А выглядит это примерно так:
.text:00456736 mov eax, [eax+1F468h] ; это аргумент
.text:0045673F push eax
.text:00456743 mov ecx, [ecx+0A4h] ; здесь объект с "плохим" значением
.text:00456749 mov edx, [ecx] ; а это его таблица указателей на виртуальные функции
.text:0045674B call dword ptr [edx+20h] ; здесь вызывают подпрограмму
.text:00483A60 push ebp
.text:00483A61 mov ebp, esp
.text:00483A63 mov eax, [ecx+8]
.text:00483A66 mov ecx, [ebp+arg_0] ; этот аргумент равен -1
.text:00483A69 mov eax, [eax+ecx*8] ; здесь происходит что-то вроде result = arr[-1]. Очень подозрительное место
.text:00483A6D pop ebp
.text:00483A6E retn 4
.text:0045674E mov dword_68C818, eax ; здесь записуют в нашу глобальную переменную значение, которое подпрограмма вернула. Это та самая, "плохая", цифра, которая и обрушивает игру.
Очень странно, что к массиву обращаются по отрицательному индексу. Можно предположить, что ошибка в аргументе. Текстовым поиском ищу обращение к переменной со смещением 1F468h
Как видно, обращаются к ней ровно два раза. Первый раз в нее записывают константу при инициализации (видимо эта константа как раз мне и попадает). Второй случай придется изучить более детально. Перехожу по адресу, указанному результатах поиска (функция большая поэтому скриншот приводить не буду), ставлю точку останова, выбираю сценарий. Ничего не происходит. Поднимаюсь выше, к началу функции. Функция вызывается довольно часто. По наличию множества ветвлений, можно предположить, что это обработка каких-то сообщений. Пробую нажать на все кнопки, которые имеются в окне сценария. Точка останова срабатывает во время выбора начального приза. Примерно вот так, как на картинке:
Теперь можно сформулировать ошибку по-человечески. Существует виртуальная функция getStartupBonus, которая возвращает начальный бонус. В сценариях одного типа возвращается переменная, в сценариях другого — элемент массива, заданный каким-то внешним индексом. Подразумевается, что индекс не выходит за рамки массива. Но по каким-то причинам, перед загрузкой сценария, в индекс записывается неправильное значение. Происходит выход за пределы массива, приложение падает.
Тут есть несколько вариантов решения. Можно, например изменить константу, которой инициализируется переменная на ноль. Но, правда, это может привести к совсем неожиданным последствиям. Мне кажется, что лучше всего будет дописать перед функцией, которая работает с массивом, несколько инструкций, которые будут проверять аргумент. И функция станет выглядеть так:
.text:0048385A push ebp
.text:0048385B mov ebp, esp; ebp указывает на стек
.text:0048385D mov eax, [ecx+8]; eax указывает на массив
.text:00483860 mov ecx, [ebp+arg_0]; в аргументе содержится предполагаемый индекс массива
.text:00483863 test ecx, ecx; проверяем, не является ли индекс отрицательным
.text:00483865 jns short loc_483869; если не является, то переходим к cчитыванию элеменита массива
.text:00483867 xor ecx, ecx; если индекс массива отрицательный, то приравниваем его к нолю
.text:00483869 mov eax, [eax+ecx*8]; возвращаем элемент массива
.text:0048386C pop ebp
.text:0048386D retn 4
Записываю изменения в исполняемый файл, перезапускаю игру, выбираю кампанию, работает.
Автор: k_d