Стиль перехвата функций в C++

ОГЛАВЛЕНИЕ

Данная статья посвящена обходу функций в более безопасном стиле программирования на C++.

•    Скачать исходники - 2.16 Кб

Введение

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

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

Немного странно говорить о безопасном переписывании исполняемого кода или, более того, об использовании ООП при перехвате функций. Но вы удивитесь, обнаружив, что это работает. Шаблоны C++, инкапсуляция и автоматическое уничтожение очень облегчают это.

Использование кода

Вы можете использовать приведенный код путем добавления patcher.cpp и patcher.h в ваш проект C++. Архив не содержит примеры.

Краткая информация

Бывает три типа таких ошибок (назовем их 3TM):
1.    Использование неверного соглашения о вызовах, не совпадающего с соглашением исправленной функции
2.    Использование неверных аргументов, переданных функции
3.    Использование неверного типа возвращаемой переменной

Очень легко избежать данных видов ошибок во время компиляции.

Рассмотрим функции send/recv в winsock.h:

int PASCAL FAR recv (
                     IN SOCKET s,
                     OUT char FAR * buf,
                     IN int len,
                     IN int flags);
int PASCAL FAR send (
                     IN SOCKET s,
                     IN const char FAR * buf,
                     IN int len,
                     IN int flags); 

И простейшие исправления для них:

int (PASCAL FAR *lpfn_send )( IN SOCKET s, IN const char FAR * buf,
                IN int len, IN int flags);
int PASCAL FAR my_send ( IN SOCKET s, IN const char FAR * buf, IN int len, IN int flags)
{
    return lpfn_send (s, buf, len, flags);
}
int (PASCAL FAR *lpfn_recv )( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags);
int PASCAL FAR my_recv ( IN SOCKET s, OUT char FAR * buf, IN int len, IN int flags)
{
    return lpfn_recv (s, buf, len, flags);
}

Правила просты - если функция send имеет тип type_of_send, то исправленная функция my_send должна использовать тот же тип type_of_send. Функция-заглушка lpfn_send также должна иметь тип type_of_send. Если другая функция recv имеет другой тип type_of_recv, то исправленная функция my_recv должна использовать тот же тип type_of_recv. Функция-заглушка lpfn_recv также должна иметь тип type_of_recv.

И само исправление (пока теоретический код):

patch(send, my_send, lpfn_send);
patch(recv, my_recv, lpfn_recv);

Не видно никаких вынужденных приведений типов, никаких предупреждений, никаких ошибок. Гораздо легче писать и нет шанса сделать 3TM.

Прототип этой функции таков:

template<class T> patch(T, T, T&);

Данная конструкция помогает предотвратить 3TM. Если вы не навяжете приведение типа при передаче аргументов в функцию, то не сможете скомпилировать код с 3TM.

Использование указателей вместо ссылок мало отличается:

patch(send, my_send, &lpfn_send);
patch(recv, my_recv, &lpfn_recv);

Прототип функции таков:

template<class T> patch(T, T, T*);         

Это все о безопасности исправлений.

Типизация функции

Кое-что о правилах типов функций. Это может понадобиться при получении адреса функции через GetProcAddress. Возьмем простое объявление функции C++:

int func(int, char*, void*);  
Тип T функции будет:
int (*)(int, char*, void*)

Надо объявить указатель правильного типа и правильно привести тип при выполнении GetProcAddress:

int (*func_ptr)(int, char*, void*);
func_ptr = (     int (*)(int, char*, void*)     ) GetProcAddress(hModule, "func_name");

Более полное объявление функции таково:

return_type calling_convention function_name (список типов аргументов); 

Правило создания типа функции следующее:

return_type (calling_convention *) (список типов аргументов) 

Стандартное соглашение о вызовах для C++ в функции MSVC - __cdecl. Можно опустить его, но можно включить его в объявление. Соглашение о вызовах  вы должны добавлять при работе с функциями с разными соглашениями о вызовах. Пример функции выше с объявлением указателя и LoadLibrary с приведением типа становится:

int __cdecl func(int, char*, void*);
int (__cdecl* func_ptr)(int, char*, void*);
func_ptr = (  int (__cdecl*)(int, char*, void*)   )
        GetProcAddress(hModule, "func_name");

То же самое для функций send/recv выглядит так:

//объявления функций в winsock.h
int PASCAL FAR recv (
                     IN SOCKET s,
                     OUT char FAR * buf,
                     IN int len,
                     IN int flags);
int PASCAL FAR send (
                     IN SOCKET s,
                     IN const char FAR * buf,
                     IN int len,
                     IN int flags);

///////////////////////////////////////////////////
//объявления указателя и исправления в приложении
//опустим IN и OUT
int (PASCAL FAR *lpfn_send )( SOCKET , const char * , int , int) = send;
int PASCAL FAR my_send ( SOCKET s, const char * buf, int len, int flags);
int (PASCAL FAR *lpfn_recv )( SOCKET, char FAR *, int, int) = recv;
int PASCAL FAR my_recv ( SOCKET s, char FAR * buf, int len, int flags);

//////////////////////////////////////////////////
//присвоение указателя с GetProcAddress:
HINSTANCE hInstanceWs2 = GetModuleHandleA("ws2_32.dll");
lpfn_send = (int(PASCAL FAR*)(SOCKET,const char*,int,int))
            GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(PASCAL FAR*)(SOCKET,char*,int,int))
            GetProcAddress(hInstanceWs2, "recv");

//делать так не рекомендуется, но допускается:
lpfn_send = (int(__stdcall*)(SOCKET,const char*,int,int))
            GetProcAddress(hInstanceWs2, "send");
lpfn_recv = (int(__stdcall*)(SOCKET,char*,int,int))GetProcAddress(hInstanceWs2, "recv");

В большинстве случаев для функций WinAPI не надо вызывать GetModuleHandle/GetProcAddress, так как библиотеки и функции загружаются в некие стандартные адреса. Поэтому вышеприведенные приведения типов не нужны. Просто задайте адрес функции при инициализации или позже:

lpfn_send = send;
lpfn_recv = recv;