Анализ вылета

532D1F

генератор карт, доступ к клетке реки

Заключение
Виновник — код генератора карт движка SoD (h3hota.exe). Путь рисовки рек сам уводит координату за карту (исток x-2, шаг --y без ограничения координат), а функция доступа к клетке читает по отрицательному индексу width*y+x ниже массива клеток. Ограничение координат, которое есть у соседнего пути пометки истоков (sub_549C90), на пути рисовки пропущено.
1.8.0
Версия HotA
выход за границы
Класс сбоя
298
Количество логов
ВЫСОКАЯ
Полезность лога

Визуализация вылета #

Два аспекта одного дефекта: куда указывает плохой индекс клетки (он уходит НИЖЕ массива) и откуда он берётся (кисть реки шагает по диагонали за левый верхний угол карты, в координату (-1, -1)).

1. Индекс клетки уходит ниже массива

Функция доступа считает линейный номер клетки как index = width*y + x и читает элемент по адресу base + index*48 + 0x24. Координату (x, y) никто не ограничивает диапазоном карты, поэтому index становится ОТРИЦАТЕЛЬНЫМ и чтение попадает левее начала массива.

F — адрес сбоя индекс < 0
B — база индекс 0
массив клеток 0 .. W*H-1

смещение индекс*48 отрицательное -> адрес ЛЕВЕЕ базы, в чужой памяти

B base (регистр ECX, оканчивается на ...024) — начало массива клеток, клетка с индексом 0.
F faulting = B + index*48 + 0x24 при index < 0 (наблюдались -109, -145, -181, -217, -253). Пример: 0x06AA5024 - 0x1B30 + 0x24 = 0x06AA3518 (index = -145).

2. Откуда отрицательный индекс: шаг за левый верхний угол

В 211 из 298 вылетов (71%) индекс равен ровно -(width+1). При index = x + width*y это значит (x, y) = (-1, -1): кисть реки шагнула ПО ДИАГОНАЛИ за угол карты. Это прямо следует из кода рисовки — старт реки source_x - 2CreateRivers) и шаг --yCreateRiverWithDelta), оба без ограничения к 0.

S F

прямоугольник — карта; диагональная стрелка — шаг кисти из истока за угол

0 угол карты (0,0) — верхняя левая клетка.
S исток реки у края. Соседний путь (пометка истоков sub_549C90) здесь ограничивает координату и пропускает клетку.
F координата (-1, -1) за углом: путь рисовки ограничения НЕ делает, индекс становится -(width+1).

Ширина карты читается прямо из EAX

Поскольку при шаге за угол |index| = width+1, размер карты декодируется из регистра: width = |EAX/48| - 1. Все вылеты — на крупных картах (L и выше), на малых их нет.

индексширинаразмер картывылетов
109108L15
145144XL62
181180H32
217216XH26
253252G76

Краткое резюме

Игра падает во время работы генератора карт, на этапе прокладки рек. Алгоритм трассировки идёт по цепочке "откуда пришли" и просит у карты клетку по координате (x, y). Функция доступа считает смещение клетки как (width*y + x)*48 и читает по нему поле. В вылете эта координата оказывается ЗА пределами карты, причём с отрицательным номером клетки, и обращение уходит на память ниже массива клеток.

Тип: EXCEPTION_ACCESS_VIOLATION, чтение по адресу. Где: h3hota.exe, A0_RmgRoadMapParam_Cell_Get_River_sub_532D00, mov eax,[eax+ecx+24h] по адресу 0x00532D1F. Что: чтение клетки массива по вычисленному индексу width*y+x без ограничения координат (нет нижней и верхней границы). Корень: индекс отрицательный (EAX = index*48 < 0); координату выше по стеку сам формирует путь рисовки реки (исток x-2, шаг --y без ограничения к [0..size-1]).

В точку сбоя ведут ДВА вызывающих пути, оба из одной подсистемы рек и оба от одной и той же координаты за картой (см. раздел 6): A) основная кисть рисовки sub_4FA940 (94% вылетов) — прямой шаг по диагонали за угол; B) соседний размещатель sub_4FA590 (5% вылетов) — шаг к соседней клетке для проверки местности; именно он даёт самые далёкие "дикие" индексы.

Почему это дефект кода, а не просто "нет проверки": рядом, в той же подсистеме рек, функция пометки истоков sub_549C90 явно ограничивает координату (x<0 / x>=width / y<0 / y>=height -> пропустить) ПЕРЕД доступом к клетке, а путь рисовки этой проверки не делает.

Исходные данные

Набор: 532D1F (адрес 0x00532D1F), папка mail-dumps/532D1F, получено с почты.

Уникальных вылетов: 298 Класс сбоя: выход за границы (вычисленный индекс клетки < 0) Версия HotA: 1.8.0 Версии HD-мода: смешанные (40 сборок, 5.6 R9 ... 5.7 RC12)

Code : EXCEPTION_ACCESS_VIOLATION (297 вылетов) EXCEPTION_GUARD_PAGE (1 вылет, см. ниже) Information : чтение по адресу (разные адреса в каждом вылете) Модуль : h3hota HD.exe / h3hota.exe, базовый адрес образа 0x400000 (адрес загрузки == базовому, адрес лога == адресу в листинге IDA)

Пример (вылет с адресом сбоя 0x06AA3518): Context : EAX=0xFFFFE4D0, ECX=0x06AA5024, EDX=0x001981CC, EBX=0x0019820C, ESI=0x0019822C, EDI=0x00198270, EBP=0x00198194, ESP=0x00198194 Map file: [HotA] Air Supremacy.h3m; Day 1; Me=AP=0

Версия HotA одинакова во всех 298 вылетах (1.8.0). Версия HD-мода РАЗНАЯ (40 сборок, 5.6 R9 ... 5.7 RC12, ни одна не доминирует) — это периферийный модуль, на сам сбой не влияет (см. раздел 9). Поле "Map file" НЕ АНАЛИЗИРУЕТСЯ. Это файл КАРТЫ (.h3m, готовый сценарий), А НЕ файл ШАБЛОНА (.h3t). Вылет происходит на генерации СЛУЧАЙНОЙ карты, которой управляют ШАБЛОН (.h3t) и зерно ГСЧ; а в поле "Map file" записан лишь файл карты, выбранный в списке по умолчанию, к генерации отношения не имеющий. Значимы только размер карты (читается из EAX, см. раздел 5) и зерно ГСЧ (Last RMG Seed).

Код исключения варьируется: в большинстве вылетов это EXCEPTION_ACCESS_VIOLATION (чтение по невыделенной памяти), но в одном вылете (лог 5.7 R10, адрес сбоя 0x18FCD6D8) — EXCEPTION_GUARD_PAGE: отрицательный индекс попал на страницу-сторож, а не на полностью невыделенный регион. Механизм идентичен; класс сбоя один (см. раздел 4).

Корреляция дампов (сравнительная таблица)

298 уникальных вылетов одного адреса 0x00532D1F, операция чтения.

Сводная картина (числовые поля по нескольким вылетам):

Адрес сбояEAX(=индекс*48)индексECX (база)EBP-EDI
10x1D07B0D80xFFFFD090-2530x1D07E024-?
20x15EA1E580xFFFFDE10-1810x15EA4024-?
640x06AA35180xFFFFE4D0-1450x06AA5024-88
-0x18FCD6D80xFFFFD690-2210x18FD0024-88 (*)
-0x........0xFFFFD750-2170x.....024-?
-0x........0xFFFFEB90-1090x.....024-?

(*) вылет 5.7 R10, код EXCEPTION_GUARD_PAGE (см. раздел 4).

Инварианты по всем вылетам:

  • faulting = ECX + EAX + 0x24 (адресная проверка сходится всегда);
  • EAX отрицателен и НАЦЕЛО делится на 48 (0x30) -> это index*шаг, index < 0 (наблюдались -109, -145, -181, -217, -253 и др.);
  • база ECX валидна и РАЗНАЯ в каждом вылете (своя область памяти под массив клеток на каждой сессии), оканчивается на ...024;
  • различаются: размер генерируемой карты (по EAX: width 108/144/180/216/252, см. раздел 5), версия HD-мода, машина игрока.

Группировка по непосредственному вызывающему коду (точка над функцией доступа) даёт ДВЕ точки в одной подсистеме рек: 0x4FAA7A (кисть sub_4FA940) 280 вылетов (94%) 0x4FA626 (размещатель sub_4FA590) 16 вылетов (5%) 0x4FA7DC / 0x4FA88D (sub_4FA590) по 1 Оба пути ведут в одну функцию доступа sub_532D00 и оба идут от одной координаты за картой (см. раздел 6).

Различие баз при одинаковой регулярной арифметике индекса -> индекс вычисляется кодом (не зависит от машины), база — своя область памяти. Это вычисленный выход за границы, не случайный мусор и не порча кучи.

Сопоставление адресов

Листинг h3hota.exe: базовый адрес образа 0x400000, адрес загрузки == базовому, значит адрес из лога равен адресу в листинге IDA. адрес лога 0x00532D1F -> IDA 0x00532D1F Совпадает с заголовком лога (Module h3hota HD.exe). Подтверждено.

Шапка листинга (h3hota.exe.txt): Input SHA256 1766ADBF6ED99B58A923338E42A78C0C517934A3550BA499D55DC86036696657, Imagebase 400000, Section size 00239000 — h3hota.exe (движок SoD).

Точка сбоя — разбор инструкций

Функция A0_RmgRoadMapParam_Cell_Get_River_sub_532D00 (доступ к клетке по координате (x,y), thiscall):

.text:00532D03  mov   ecx, [ecx+4]        ; подобъект карты
.text:00532D06  mov   edx, [ebp+arg_0]    ; -> XY (x,y)
.text:00532D0A  mov   eax, [ecx+0Ch]      ; width (x_size)
.text:00532D0D  mov   esi, [edx]          ; x
.text:00532D0F  imul  eax, [edx+4]        ; width * y
.text:00532D13  mov   ecx, [ecx+8]        ; база массива клеток
.text:00532D16  add   eax, esi            ; index = width*y + x
.text:00532D19  lea   eax, [eax+eax*2]    ; index*3
.text:00532D1C  shl   eax, 4              ; index*48 (клетка 0x30)
.text:00532D1F  mov   eax, [eax+ecx+24h]  ; читать клетку.поле+0x24  <- СБОЙ
.text:00532D23  shl   eax, 0Eh
.text:00532D26  sar   eax, 1Ch            ; вырезать битовое поле (тип реки)

Размер клетки — 48 байт (lea*3 + shl 4). Поле +0x24 — упакованный dword с битами реки. Никакой проверки index на нижнюю (>=0) или верхнюю (< width*height) границу нет: координате доверяют.

Адресная проверка (вылет 0x06AA3518): EAX = 0xFFFFE4D0 = -6960 = -145 * 48 -> index = -145 ECX = 0x06AA5024 (база) base + index*48 + 0x24 = 0x06AA5024 - 0x1B30 + 0x24 = 0x06AA3518 = faulting [OK]

Ещё два вылета: 0x1D07E024 - 0x2F70 + 0x24 = 0x1D07B0D8 (index = -253) [OK] 0x15EA4024 - 0x21F0 + 0x24 = 0x15EA1E58 (index = -181) [OK]

Вылет на HD-моде 5.7 R10 (код EXCEPTION_GUARD_PAGE, адрес сбоя в поле "Information" = 0x18FCD6D8): EAX = 0xFFFFD690 = -10608 = -221 * 48 -> index = -221 ECX = 0x18FD0024 (база) 0x18FD0024 - 0x2970 + 0x24 = 0x18FCD6D8 = faulting [OK] Тот же механизм и та же формула; различие лишь в коде исключения (отрицательный индекс попал на страницу-сторож, а не на полностью невыделенную память).

Индекс ОТРИЦАТЕЛЬНЫЙ во всех проверенных вылетах -> чтение уходит

НИЖЕ начала массива клеток.

Регистровые инварианты

EAX = index * 48, index < 0 (нет нижнего ограничения координаты) ECX = база массива клеток (валидна, своя в каждой сессии) faulting = ECX + EAX + 0x24 (стабильно) EAX делится на 48 без остатка -> это именно индекс клетки, а не мусор.

Расшифровка геометрии (ключевой инвариант). Индекс = x + width*y. В 211 из 298 вылетов (71%) index РАВЕН точно -(width+1), где width — размер карты (кратный 36). А -(width+1) при index=x+width*y означает ровно (x,y) = (-1,-1): кисть шагнула по диагонали за ЛЕВЫЙ ВЕРХНИЙ УГОЛ карты (согласуется с source_x-2 и --y). Распределение |index| по размерам карты (угловые вылеты): |index|=109 -> width 108 (L) 15 вылетов |index|=145 -> width 144 (XL) 62 |index|=181 -> width 180 (H) 32 |index|=217 -> width 216 (XH) 26 |index|=253 -> width 252 (G) 76 Следствие: ширина карты ПРЯМО читается из EAX: width = |EAX/48| - 1. Дефект бьёт только по крупным картам (L и выше), чаще всего G (252) и XL (144). На малых картах (S/M) в этом наборе вылетов нет.

Два диапазона |index| (по всем 298, не только угловым):

  • "у края" (|index| <= 514): 287 из 298 (96%) — координата чуть за границей карты; сюда входят и угловые (-1,-1), и соседние точки края (x=-2,y=0 левый край; x=-3,y=-1 и т. п.);
  • "аномально далеко" (|index| > 514): 11 из 298 (4%) — индексы от -3456 до -241777. ВСЕ они приходят через путь B (вызывающий код 0x4FA626, функция sub_4FA590), а не через основную кисть. Это согласуется с механизмом пути B (раздел 6): когда входная координата уже за картой, проверка краёв в sub_5BCD10 её не ловит, и шаг к соседу уводит индекс ещё дальше.

EDX/EBX/ESI/EDI/EBP/ESP лежат в стеке (0x0019xxxx) и относятся к кадру трассировщика, не к адресуемым данным.

Восстановленная цепочка вызовов

Все кадры дефектного пути — в h3hota.exe (SoD). В точку сбоя ведут ДВА вызывающих пути из одной подсистемы рек, оба от координаты, которую путь рисовки уже увёл за карту.

EXEПуть A — основная кисть рисовки (280 вылетов, 94%): A0_ARandMapGen_CreateRivers_sub_549D60
EXEA0_ARandMapGen_CreateRiverFromSource_ToXYZ_sub_5489F0
EXEA0_ARandMapGen_CreateRiverWithDeltaFromSource_ToXYZ_sub_5492E0 (идёт по цепочке prev_xyz клеток: читает prev_xyz из cell+0x10, формирует end_xy и зовёт кисть)
EXEA0_RoadRiverBrush_CreateHorizontal_RoadRiver_sub_4FA940 (горизонтальный проход кисти между точками)
EXEA0_RoadRiverBrush_Set_RoadRiver_At_Coords_sub_4FAA50 (0x4FAA7A)
косвенный вызов через таблицу методов
EXEA0_RmgRoadMapParam_Cell_Get_River_sub_532D00 <- СБОЙ (чтение)
EXEПуть B — соседний размещатель (16 вылетов, 5%; даёт дикие индексы): ... -> A0_RoadRiverPlacer_SetRoadRiverAtXY_sub_4FA590 (0x4FA626)
EXEA0_RmgRoadMapParam_Cell_Get_River_sub_532D00 <- СБОЙ (чтение)

Функция sub_4FA590 для каждого из 8 направлений шагает к соседней клетке (A0_XY4_Step_To_Direction_sub_4FABD0) и читает её, чтобы проверить тип местности. Перед этим она зовёт A0_Setup_StepDirections_FromXY_Availability_sub_5BCD10 — настройку доступности направлений, которая ОТКЛЮЧАЕТ шаг только если клетка стоит РОВНО на границе (y==0, y==map_y-1, x==0, x==map_x-1). Если же входная клетка УЖЕ за картой (x=-1 и дальше, что приходит от реки без ограничения координат), ни одно из этих равенств не выполняется, все 8 направлений считаются доступными, и шаг к соседу уводит координату ещё дальше за карту. Поэтому через путь B приходят самые далёкие индексы (раздел 5). То есть проверка краёв здесь есть, но она предполагает, что вход УЖЕ на карте, и не спасает от координаты за картой.

Выше по стеку (V2) идут кадры HW_HOTA.dll / HD_HOTA.dll и переходники patcher_x86 (0x010Dxxxx / 0x025Bxxxx) — это инфраструктура запуска и диспетчеризации генератора из HD-мода; к виновному коду они отношения не имеют (переходников в листинге нет). Сам дефектный путь целиком в h3hota.exe.

По логу 5.7 R10 (есть листинг HD_HOTA.dll-5.7 R10) полный путь запуска генератора раскрыт и подтверждает подсистему:

HOTAHD_HOTA.dll!sub_115B0D0 / sub_113D5D0 / sub_10794F0 / sub_107B890 HW_HOTA.dll!sub_1001A050 / sub_10013A50 / sub_10017F10 / sub_10016440 (диспетчеры запуска генерации из HD-мода)
EXEh3hota.exe!A0_ARandMapGen_PrepareSettings_AndGenerateMap_sub_5863B0
EXEA0_ARandMapGen_RmgSettings_CreateRandomMap_ByFilename_sub_54C580
EXEA0_ARandMapGen_RmgSettings_CreateRandomMap_sub_54C450
EXEA0_ARandMapGen_Main_sub_549E20
EXEA0_ARandMapGen_CreateRivers_sub_549D60
EXEA0_ARandMapGen_CreateRiverWithDeltaFromSource_ToXYZ_sub_5492E0
EXEA0_RiverPlacer_CreateAndStartRiver_sub_55F080
EXEA0_RmgRoadRiverBrush_Create_AndStart_RoadRiver_sub_4FA910
EXEA0_RoadRiverBrush_Set_RoadRiver_At_Coords_sub_4FAA50
EXEA0_BaseRiverPlacer_GetRiver_sub_55F060 (таблица методов +0x14)
EXEA0_RmgRoadMapParam_Cell_Get_River_sub_532D00 <- СБОЙ

Кадры HW_HOTA/HD_HOTA здесь — именно диспетчеры запуска генерации (не виновники): они зовут штатный конвейер генератора SoD, и дефект возникает уже внутри него.

Первопричина

Функция доступа sub_532D00 — простая вычислительная функция доступа к клетке: index = width*y + x, элемент = base + index*48, читает поле. Она НЕ проверяет границы и по контракту доверяет поданной координате — как и соседние функции доступа того же подобъекта карты (рядом в листинге sub_532C90 Get_RiverRiverTailRiverMirror, sub_532D30 Get_TerrainType — та же арифметика без проверки границ). Значит точка сбоя ИСПРАВНА, это симптом, а не виновник.

Первопричина — в коде ПУТИ РИСОВКИ реки, который САМ формирует координату вне карты и не ограничивает её к [0..size-1]. Конкретно: 1) в sub_549D60 (CreateRivers) старт дельта-реки вычисляется как разность координат истока (v9 = source_x - trigger_x), и затем ещё явно уменьшается:

       .text:00549DDB  sub  esi, 2     ; x_start = source_x - 2
   без проверки x_start >= 0;
2) в sub_5492E0 (CreateRiverWithDelta) река шагает вверх:
       .text:0054942D  dec  edx        ; --y
   тоже без проверки y >= 0, после чего координата идёт в
   кисть sub_4FA940 -> функция доступа sub_532D00.

Координата истока — это РАЗНОСТЬ source_x - trigger_x (а не абсолютный X), она и так может быть мала, а -2 для дельта-реки добивает её за 0. Ни CreateRivers, ни CreateRiverWithDelta, ни кисть, ни функция доступа не ограничивают полученную координату к диапазону карты.

Доказательство от соседней функции (ограничение ЕСТЬ рядом, но на другом пути): в той же подсистеме, в функции пометки истоков A0_ARandMapGen_MarkRiverSources_sub_549C90 (вызывается из того же CreateRivers, строкой выше пометки рек) ПЕРЕД доступом к клетке стоит полная проверка границ:

.text:00549D05  sub  edx, edi      ; x = obj_x - half_w
.text:00549D09  test edx, edx
.text:00549D0B  jl   loc_549D3A    ; x < 0       -> пропустить
.text:00549D0D  cmp  edx, [ecx+18h]
.text:00549D10  jge  loc_549D3A    ; x >= width  -> пропустить
.text:00549D12  test esi, esi
.text:00549D14  jl   loc_549D3A    ; y < 0       -> пропустить
.text:00549D19  cmp  esi, eax
.text:00549D1B  jge  loc_549D3A    ; y >= height -> пропустить

Тот же разработчик, та же подсистема, тот же способ индексации клетки (base + (x + w*(y + h*z))*48) — но на пути рисовки эта проверка пропущена. Это и есть первопричина: несимметрия "пометка истока ограничивает координату / рисовка реки не ограничивает".

Замечание про путь B (sub_4FA590). У него проверка краёв формально есть (sub_5BCD10), но она отключает шаги только для клетки РОВНО на границе и не рассчитана на координату, которая УЖЕ за картой. Это не самостоятельный дефект, а то же следствие неограниченной координаты из CreateRivers/CreateRiverWithDelta: на исправную карту путь B такой координаты бы не получил.

Почему вылет непостоянный (тест детерминированность vs частота): дефект детерминирован, но срабатывает только когда исток реки лёг близко к краю карты (тогда source_x-2 или шаг --y выходят за 0). Это зависит от зерна ГСЧ и размера карты, поэтому редок в среднем, но чаще на крупных картах (см. раздел 5: все вылеты — L и выше). Противоречия между "детерминированный дефект" и "редкий вылет" нет: условие активации (исток у края) само по себе редкое. кратны 48 и в основном лежат в узком диапазоне (-109..-253), база валидна — это регулярная арифметика координат у верхнего края, а не случайный мусор.

Условия, спровоцировавшие сбой

HotA 1.8.0; модуль h3hota.exe (SoD); подсистема — генератор карт, фаза прокладки рек. Day = 1 11 ВО ВСЕХ 298 вылетах -> сбой всегда на этапе генерации карты, никогда в ходе партии. Версии HD-мода разные (40 сборок) -> от HD-мода не зависит. Лог 5.7 R10 дополнительно даёт Last RMG Seed = 1780845074 — прямое подтверждение, что это генерация карты. Код исключения: 297 EXCEPTION_ACCESS_VIOLATION + 1 GUARD_PAGE.

Размер карты — определяющее условие. По расшифровке индекса (раздел 5) все вылеты — на картах размера L и выше (width 108/144/180/216/252), чаще всего G (252, 76 вылетов) и XL (144, 62). На малых картах вылетов нет. Это согласуется с механизмом: чем крупнее карта, тем больше рек и истоков, и тем выше шанс, что хотя бы один исток ляжет вплотную к углу.

Достоверность и ограничения

Подтверждено / высокая достоверность
  • ПОЧЕМУ при данных размерах карты исток реки иногда ложится вплотную к краю (частота активации) — это зависит от размещения зон/истоков конкретного шаблона случайной карты и зерна ГСЧ и требует повтора с минидампом (шаблон и зерно берутся из RMG-настроек/Last RMG Seed, НЕ из поля Map file);
Ограничения и неизвестное
  • природа 11 "диких" индексов через путь B (раздел 5): далеко ли уехал шаг к соседу от координаты, уже бывшей за картой, или есть дополнительная порча входной XY-пары — различимо только по минидампу (проверка входной пары в момент вызова sub_4FA590). Переходники patcher_x86 (0x010Dxxxx / 0x025Bxxxx) между кадрами HD-мода — это заглушки-переходы хуков, в листинге по определению отсутствуют и в виновный путь не входят. Остальные кадры HD_HOTA/HW_HOTA для лога 5.7 R10 сопоставлены (раздел 6).

Признаки поиска в старых версиях HotA / в SoD

Код виновного пути — в h3hota.exe (неизменный EXE движка SoD), поэтому адреса СТАБИЛЬНЫ между версиями HotA и присутствуют в SoD.

Context-инварианты: чтение по адресу; EAX отрицателен и кратен 48; база-регистр (ECX) валидна; faulting = ECX + EAX + 0x24. Текстовая сигнатура (любой версии листинга h3hota.exe): imul eax,[edx+4]; add eax,esi; lea eax,[eax+eax*2]; shl eax,4; mov eax,[eax+ecx+24h] в функции доступа к клетке. Стабильные адреса h3hota.exe (искать как фикс. адреса/вызовы): 0x532D00 (доступ Get_River), 0x4FA940 (кисть дорог/рек), 0x4FAA50 (Set_RoadRiver_At_Coords), 0x4FA590 (соседний размещатель SetRoadRiverAtXY), 0x5492E0 (трассировка реки с дельтой), 0x5489F0/0x549D60 (CreateRiver*). Две вызывающие точки над функцией доступа: 0x4FAA7A (через кисть sub_4FA940) — основной путь, 94%; 0x4FA626 (через размещатель sub_4FA590) — даёт дикие индексы, 5%. Соседние функции доступа без ограничения координат: 0x532C90, 0x532D30 — та же арифметика width*y+x без проверки границ. Соседняя функция С проверкой (эталон правильного поведения): 0x549C90 (MarkRiverSources) — та же подсистема, перед доступом к клетке делает test/jl и cmp [..18h]/jge по x и y. Наличие проверки здесь и её отсутствие на пути рисовки (0x549D60/ 0x5492E0) — ключевая несимметрия дефекта. Генераторы отрицательной координаты (стабильные адреса): 0x549DDB (sub esi,2 в CreateRivers), 0x54942D (dec edx/--y в CreateRiverWithDelta) — оба без ограничения к 0.

Семейство дефекта (тот же корень, другой адрес сбоя): Кисть sub_4FA940 ОБЩАЯ для рек И дорог (её перекрёстные ссылки — и CreateRiverFromSource_sub_5489F0, и CreateRoadToCell_sub_548530). Функция доступа к дороге A0_RmgRoadMapParam_Cell_Get_Road_sub_532950 имеет БАЙТ-В-БАЙТ ту же арифметику без ограничения координат:

    .text:0053296F  mov eax, [eax+ecx+24h]
(различие лишь в последующей распаковке битового поля:
shl eax,2 вместо shl eax,0Eh). Поэтому то же пропущенное

ограничение может дать вылет и по адресу h3hota.exe+0x53296F (трассировка дорог), а не только 0x532D1F (реки). Поиск по диким логам стоит вести по ОБОИМ адресам. В текущем наборе дампов адрес 0x53296F не встречается — это пока предсказание по коду, не подтверждённое дампом.

Воспроизведение вылета

Вылет непостоянный (298 наблюдений у разных игроков и сборок HD-мода, но не у всех и не каждый раунд) -> гарантированной пошаговой последовательности нет; зависит от конкретного запуска генерации. КРИТИЧНО: вылет бьёт только по КРУПНЫМ картам (L и выше; чаще всего G и XL) — на малых картах в наборе вылетов нет (см. раздел 5).

Условие активации пути: запущена генерация карты, фаза прокладки рек; исток реки лёг близко к краю карты, и код рисовки реки (source_x-2 в CreateRivers либо шаг --y в CreateRiverWithDelta) выводит координату за 0 без ограничения; линейный индекс клетки становится отрицательным.

Вероятностный путь повтора: массовый прогон генерации СЛУЧАЙНЫХ карт крупных размеров (L и выше) по разным шаблонам (.h3t) с перебором зёрен ГСЧ; цель — поймать минидамп на этапе CreateRivers. Поле "Map file" (файл карты .h3m из списка) для воспроизведения НЕ используется — решают шаблон и размер карты.

Что ловить: чтение-AV на 0x00532D1F с EAX, кратным 48 и отрицательным, при валидной базе в ECX (faulting = ECX + EAX + 0x24). Вызывающий код над функцией доступа — 0x4FAA7A (основной путь) либо 0x4FA626 (соседний размещатель, дикие индексы).

Дифференциальный тест: точка останова на 0x549DDB (sub esi,2) и 0x54942D (dec edx) — если после этих инструкций x или y становятся < 0, а затем координата без ограничения идёт в кисть -> функцию доступа — это подтверждает пропущенную проверку как причину. Прямое подтверждение починкой: добавить на пути рисовки такое же ограничение координаты к [0..size-1], как в sub_549C90 — вылеты исчезают. Связь с размером карты: прогнать генерацию случайных карт крупных размеров (L/XL/H/XH/G) на множестве зёрен ГСЧ — доля вылетов растёт с размером карты; на малых картах вылетов нет (см. раздел 5: размер читается из EAX).

Вывод

532D1F — чтение клетки карты в генераторе карт SoD (h3hota.exe, A0_RmgRoadMapParam_Cell_Get_River_sub_532D00) по вычисленному индексу width*y+x, который оказывается отрицательным (обращение ниже массива клеток). Первопричина — пропущенное ограничение координаты на пути рисовки рек: CreateRivers (sub_549D60) берёт исток и делает source_x-2, CreateRiverWithDelta (sub_5492E0) шагает --y — оба без ограничения к [0..size-1]. Тот же движок в соседней функции пометки истоков (sub_549C90) такое ограничение делает — именно эта несимметрия и есть дефект. В точку сбоя ведут два пути из той же подсистемы: основная кисть sub_4FA940 (94%, шаг за угол) и соседний размещатель sub_4FA590 (5%, шаг к соседу; его проверка краёв не рассчитана на координату, уже бывшую за картой, — отсюда самые далёкие индексы). Функция доступа sub_532D00 исправна и лишь читает по плохому индексу. Виновник — код генератора движка SoD (путь рисовки рек), не HD-мод.

Скопировано