• Уважаемые гости и новички, приветствуем Вас на нашем форуме
    Здесь вы можете найти ответы практически на все свои вопросы о серии игр «Готика» (в том числе различных модах на нее), «Ведьмак», «Ризен», «Древние свитки», «Эра дракона» и о многих других играх. Можете также узнать свежие новости о разработке новых проектов, восхититься творчеством наших форумчан, либо самим показать, что вы умеете. Ну и наконец, можете обсудить общие увлечения или просто весело пообщаться с посетителями «Таверны».

    Чтобы получить возможность писать на форуме, оставьте сообщение в этой теме.
    Удачи!

6.1 Теория перехвата функций

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.300
Благодарности
4.633
Баллы
625
Общие положения
В принципе перехват функций дело элементарное. Все алгоритмы уже реализованы, так что программисту остается лишь указать адрес перехватываемой функции и куда ее переадресовать. Поэтому выделим для себя 2 главных понятия, которые нужно усвоить при реализации хуков: умение правильно определять сигнатуру функции (ее тип) и как правильно на ее основе расписать указатель. Хитрого тут ничего нет, все делается шаблонно, но если вдруг возникнут какие-либо трудности - все вопросы можно задать в комментариях к посту, потом сделаем FAQ в конце первого сообщения.​
Главная техническая особенность хуков в Union - возможность множественного перехвата функции, то бишь перехвата одной функции несколькими плагинами. Такой подход позволит разработчикам создавать проекты не боясь, что кто-то уже похукал определенную областью памяти и плагины окажутся несовместимы. Реализация такова, что запросы образуют стек на функцию, в котором плагин вызывает собственный обработчик сразу же после оператора return предшественника.​
Функция, в которую был переадресован вызов, описывает новый алгоритм. Внутри этого алгоритма возможно обращение и к оригинальной реализации. Для этого в любом месте тела функции необходимо обратиться к экземпляру хука как к оригинальной функции, а ее возвращаемый результат можно использовать для собственных вычислений. Новая функция будет считаться выполненой, как только отработает оператор return для всего стека хуков.​



Указатель на функцию
Обрадую заранее. Начиная с Union SDK 1.1a эти знания будут скорее полезными, чем необходимыми, так как там реализовано автоприведение указателей. А сейчас все-таки побеседуем.​
Что же необходимо знать для построения указателя. Во-первых тип и аргументы функции. Во-вторых соглашение о вызовах и чем они отличаются. Если с первым проблем нет, то со вторым есть нюансы, которые покажутся новичку неочевидными. Но начнем с простого и создадим общий шаблон указателя.​

C++:
Функция универсальная:    [тип]   [соглашение о вызовах]                          [имя функции]     ( [аргументы] )
Указатель универсальный: [тип] ( [соглашение о вызовах]                        * [имя указателя] ) ( [аргументы] )
Указатель на метод:            [тип] ( [соглашение о вызовах] [имя класса]:: * [имя указателя] ) ( [аргументы] )

Получается мы просто переписываем функцию "другими словами". Соглашение и имя заключаются в скобки, добавляется "*" маркируя указатель. Для методов класса, если речь идет об узкоспециальных указателях, добавляется имя класса с оператором обращения к членам.​

Начнем разбор на простых примерах:
Пример 1:
C++:
0x12345678 void __cdecl func1()
Что можно сказать об этой функции. Она обычная. Обычная настолько, что и соглашение о вызовах у нее по умолчанию - __cdecl. Это значит, что мы имеем право не указывать его в реализации.​
Строим указатель:
C++:
typedef void(__cdecl * LPFUNC1)();  // эквивалентно void( * LPFUNC1)()

// пример определения
LPFUNC1 lpfunc1 = (LPFUNC1)0x12345678;

Пример 2:
C++:
0x12345678 const int __stdcall func2()
Соглашение __stdcall в основном используется в win API, отличается от __cdecl только очисткой стека.
Строим указатель:
C++:
typedef const int(__stdcall * LPFUNC2)();

// пример определения
LPFUNC2 lpfunc2 = (LPFUNC2)0x12345678;

Пример 3:
C++:
0x12345678 const zSTRING __fastcall func3(zCArray<unsigned short>& array, oCNpc* npc)
Соглашение __fastcall имеет особенность передавать аргументы в регистрах.
Строим указатель:
C++:
typedef const zSTRING(__fastcall * LPFUNC3)(zCArray<unsigned short>& array, oCNpc* npc);

// пример определения
LPFUNC3 lpfunc3 = (LPFUNC3)0x12345678;


Далее затронем указатели на методы класса (рассмотрим только нестатические, так как у статических все идентично предыдущим примерам). Принцип все тот же, но следует усвоить вот что. У методов первый аргумент ВСЕГДА неявный this. Неявный потому, что в аргументы он передается сам согласно правилам языка (сразу за оператором выбора членов . или ->). Также если у метода стоит соглашение о вызовах __thiscall (по умолчанию для методов), то после this второй аргумент - указатель на vtable.​

Пример 4:
C++:
0x12345678 void __thiscall CClass::method1()
Строим указатель, а точнее 3 эквивалентных:
C++:
typedef void(__thiscall * LPMETHOD1_V1)(CClass* _this);
typedef void(__fastcall * LPMETHOD1_V2)(CClass* _this, void* vtable);
typedef void(    CClass::* LPMETHOD1_V3)( );

// пример определения
LPMETHOD1_V1 method1_v1 =  (LPMETHOD1_V1 )0x12345678;
LPMETHOD1_V2 method1_v2 =  (LPMETHOD1_V2 )0x12345678;
LPMETHOD1_V3 method1_v3 = *(LPMETHOD1_V3*)0x12345678;
Теперь по порядку. Мы видим, что у оригинальной функции аргументов нет. Но если следовать правилу выше, то как минимум один все таки есть - это неявный this. А поскольку функция __thiscall, то и второй аргумент соответственно указатель на vtable. Тогда почему в method1_v1 он отсутствует? Все просто. Если указатель тоже имеет соглашение __thiscall, то он не указывается.​
Теперь почему у method1_v2 соглашение __fastcall. Это вопрос уже интереснее. Смысл этого соглашения равносилен __thiscall. Там и там аргументы передаются в регистрах, а __fastcall, в отличии от __thiscall, может объявляться не только в методах, но и в любых других функциях. Это означает, что благодаря этому соглашению можно перенаправить вызов метода в обычную функцию. И вот тут уже не забываем указатель на vtable.​
Конкретный указатель на метод класса описан в method1_v3. Поскольку компилятор и так понимает, что указатель строится на функцию-член, то и задавать соглашение о вызовах не обязательно, ведь это __thiscall (по умолчанию для методов). А this и vtable в данном случае писать вообще не нужно. Примечание: конструкция *(LPMETHOD1_V3*) имеет такой вид, потому что синтаксически (LPMETHOD1_V3) недопустим (преобразование int адреса к class::* указателю). Фактически же это одно и тоже.​

Пример 5:
C++:
0x12345678 void __cdecl CClass::method2(double x, double y);
Строим указатель:
C++:
typedef void(__cdecl             * LPMETHOD1_V1)(CClass* _this, double x, double y);
typedef void(__cdecl CClass::* LPMETHOD1_V2)(double x, double y);

// пример определения
LPMETHOD1_V1 method1_v1 =  (LPMETHOD1_V1 )0x12345678;
LPMETHOD1_V2 method1_v2 = *(LPMETHOD1_V2*)0x12345678;
Тут возможны только два варианта указателя. В обоих случаях обязательно указываем соглашение __cdecl, так как по умолчанию оно только для обычных функций. Указатель на vtable не пишем, поскольку функция не __thiscall. Неявный this необходимо учесть только в первом варианте.​

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


Для закрепления памяти
* __cdecl соглашение о вызовах по умолчанию для обычных функций
* __thiscall соглашение о вызовах по умолчанию для функций-членов (методов)
* У функций-членов первый аргумент всегда this
* У функций-членов с соглашением __thiscall второй аргумент всегда vtable
* В указателе на метод вида class::* аргументы this и vtable не указывается
* В указателе на метод вида class::* с соглашением __thiscall последний можно не писать
* Соглашения о вызовах __fastcall и __thiscall совместимы
 
Сверху Снизу