20121028

Direct3D - Часть 1: Исходный код и его разбор

Ребята, всем привет!

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

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

Почему статья, а не видеоурок? Потому что в основном я буду комментировать и пояснять код, в видео это так легко не сделаешь, зато потом будет видео с результатами.

Итак, что нам понадобится:

1. [Microsoft Visual Studio]. Express, ибо бесплатная, 2010 или 2012 - без разницы. Я использую 2012. Если используете 2012 - качайте версию для desktop-разработки, ага.
2. [DirectX SDK], на момент написания статьи - версия за июль 2010-го. Собственно, набор разработки (Lego) для тех, кто хочет что-нибудь рисовать под виндой.

!!!ВНИМАНИЕ!!!

Сначала ставим студию, потом - SDK!

!!!ВНИМАНИЕ!!!

Открываем студию, получаем вот такое окошко:


Видим манящую кнопку "New Project...", или же прожимаем Ctrl+Shift+N. В появившемся окне выбираем пустой проект на плюсах, внизу вбиваем ему какое-нибудь имя:


Больше ничего не трогаем и прожимаем Next, пока нам не покажут пустое окно студии. Всё верно, кода в нём нет. Сбоку находим "Solution Explorer", жмём на нём мышью, затем - правой кнопкой на "Source Files" и выбираем пункт "Add - New Item...":


Внизу обзываем его "main.cpp", жмём "Add", откроется пустое окно кода. Копируем туда вот это:


#include

typedef IDirect3D9* (STDMETHODCALLTYPE *DIRECT3DCREATE9)(UINT);
typedef HRESULT(WINAPI* tPresent)(LPDIRECT3DDEVICE9 pDevice, CONST RECT*, CONST RECT*, HWND, LPVOID);
typedef HRESULT(WINAPI* tReset)(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters);
typedef DWORD   (STDMETHODCALLTYPE *GETPROCESSHEAPS)(DWORD, PHANDLE);

static DWORD vtableFrag9[] = { 0, 0, 0, 0 };
static DWORD* presentPtr = 0;
static DWORD* resetPtr = 0;
static DWORD offsetPresent = 0;
static DWORD offsetReset = 0;
static tPresent g_D3D9_Present = 0;
static tReset g_D3D9_Reset = 0;
LPDIRECT3DDEVICE9 npDevice;

bool indicator = 0;

void DrawIndicator(LPVOID self)
{
        IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
        dev->BeginScene();
        D3DRECT rec = { 10, 10, 30, 30 };
        D3DCOLOR color = 0;
        if(indicator)
        {
                color = D3DCOLOR_XRGB(0, 255, 0);
        }
        else
        {
                color = D3DCOLOR_XRGB(255, 0, 0);
        }
        dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
        dev->EndScene();
}

HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{    
        while(!npDevice)
        {
        npDevice = pDevice;
        }
        DrawIndicator(pDevice);
        return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}

HRESULT WINAPI hkReset(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
        return g_D3D9_Reset(pDevice, pPresentationParameters);
}

BOOL SearchHeap(HANDLE heap)
{
        int vtableLenBytes = sizeof(DWORD)*4;
        PROCESS_HEAP_ENTRY mem;
        mem.lpData = 0;
        while(HeapWalk(heap, &mem))
        {
                if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
                DWORD* p = (DWORD*)mem.lpData;
                for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
                {
                        if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
                        {
                                presentPtr = p + 11;
                                resetPtr = p + 10;
                                offsetPresent = *presentPtr;
                                offsetReset = *resetPtr;
                                g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
                                g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
                                break;
                        }
                        p++;
                }
                if(presentPtr != 0) break;
        }
        return(presentPtr != 0);
}

void CheckAndHookPresent9()
{
        if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
        HANDLE heap = 0;
        HMODULE hKern = GetModuleHandleA("kernel32.dll");    
        GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
        if(getProcessHeaps != 0)
        {
                HANDLE heaps[1000];
                int numHeaps = (getProcessHeaps)(1000, heaps);
                for(int k = 0; k < numHeaps; k++)
                {
                        heap = heaps[k];
                        if(SearchHeap(heap)) break;
                }
        }
        else
        {
                heap = GetProcessHeap();
                SearchHeap(heap);
        }
        HeapLock(heap);
        if(presentPtr != 0)
        {
                (*presentPtr) = (DWORD)hkPresent;            
        }
        if(resetPtr != 0)
        {
                (*resetPtr) = (DWORD)hkReset;
        }
        HeapUnlock(heap);
}

void CopyVMT9(DWORD* vtableFrag)
{    
        HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
        HMODULE hD3D9 = GetModuleHandleA("d3d9");
        DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
        IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
        D3DDISPLAYMODE d3ddm;
        d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
        D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        IDirect3DDevice9* d3dDevice = 0;
        d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);  
        DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
        for(int i = 0; i < 4; i++)
        {
                vtableFrag[i] = vtablePtr[i + 6];
        }
        d3dDevice->Release();
        d3d->Release();
        DestroyWindow(hWnd);
}

PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
    DWORD dwOld = 0;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
    PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
    (*dwVTable)[Index] = (DWORD)dwHook;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
    return pOrig;
}

bool hooked = 0;
DWORD WINAPI TF(LPVOID lpParam)
{
        CopyVMT9(vtableFrag9);
        CheckAndHookPresent9();
        while(!npDevice && !hooked)
        {
                Sleep(50);
        }
        hooked = !hooked;
        while(1)
        {
                Sleep(100);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
        }  
        return 0;
}

DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
        while(1)
        {
                if(GetAsyncKeyState(VK_F1))  
                {
                        indicator = !indicator;
                        Beep(500,200);
                }
                Sleep(100);
        }    
        return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
        switch (ul_reason_for_call)  
        {
        case DLL_PROCESS_ATTACH:
                {
                        CreateThread(0, 0, &TF, 0, 0, 0);
                        CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
                }
        case DLL_PROCESS_DETACH:
                break;
        }
        return 1;
}

О, совсем забыл. Нам надо указать, что на выходе мы хотим не exe-файл, а dll. Меню "Project - Properties", вкладка "General", справа ищем "Configuration type: Application (.exe)", радостно меняем его на "Dynamic Library (.dll)", жмём OK.

Попробуем выяснить, собирается ли оно. Меню "Build - Build Solution". После этого внизу должно появиться что-то такое:


Значит, проект собрался и всё отлично. Если студия ругается, что не может найти заголовочный файл d3d9.h, то идём в свойства проекта, находим там вкладку "VC++ Directories", добавляем в "Include Directories" и "Library Directories" соответствующие папки из DirectX SDK, который будет лежать где-то в Program Files на диске цэ.

Итак, вроде всё хорошо, вернёмся к коду. Смотрим на самый главный метод - точку входа:


BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
        switch (ul_reason_for_call)  
        {
        case DLL_PROCESS_ATTACH:
                {
                        CreateThread(0, 0, &TF, 0, 0, 0);
                        CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
                }
        case DLL_PROCESS_DETACH:
                break;
        }
        return 1;
}

Так как мы пишем DLL, то и точка входа у нас называется DllMain, умеет возвращать булевую переменную (ложь или правда), ну а APIENTRY - насколько я помню, соглашение о вызове для Win32-приложений. Лучше этим не забивать голову, да. В аргументах нас интересует только один параметр, ul_reason_for_call, который определяет, что с библиотекой случилось.

В кратце, чем отличается dll от exe? Exe умеет запускаться сам, а вот dll должна быть пропихнута к уже запущенному процессу. Проверкой этого, собственно, метод и занимается - если ul_reason_for_call == DLL_PROCESS_ATTACH (присоединились к процессу), то запускаем два потока, а если DETACH (отсоединились от процесса), то ничего не делаем. После выполнения всегда возвращаем 1, то есть правду.

Если не в курсе про switch-case - почитайте, прикольная штука, я сейчас останавливаться подробно не буду.

Дык вот. Присоединились к процессу, запустили два потока. Что они, собственно, делают? Рассмотрим сначала функцию для потока KeyboardHook, потому что она покороче и попроще:


DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
        while(1)
        {
                if(GetAsyncKeyState(VK_F1))  
                {
                        indicator = !indicator;
                        Beep(500,200);
                }
                Sleep(100);
        }    
        return 0;
}

Всё просто до безобразия. Крутимся в бесконечном цикле, в котором:

1. Если нажата F1, то
2. Меняем значение переменной indicator на противоположное (0\1)
3. Пиликаем динамиком (Beep)
4. Ждём 100 мсек

Если вдруг цикл прерывается - возвращаем 0.

...Ничего не напоминает? Правильно! Эта штука включает и выключает нашу менюшку.

Что же у нас крутится во втором потоке? Давайте посмотрим:


DWORD WINAPI TF(LPVOID lpParam)
{
        CopyVMT9(vtableFrag9);
        CheckAndHookPresent9();
        while(!npDevice && !hooked)
        {
                Sleep(50);
        }
        hooked = !hooked;
        while(1)
        {
                Sleep(100);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
                HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
        }  
        return 0;
}

Ого, а вот тут уже интересно! Метод CopyVMT9, судя по названию, что-то делает с VMT для 9-й версии библиотеки Direct3D, а именно - вот код:


void CopyVMT9(DWORD* vtableFrag)
{    
        HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
        HMODULE hD3D9 = GetModuleHandleA("d3d9");
        DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
        IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
        D3DDISPLAYMODE d3ddm;
        d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
        D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        IDirect3DDevice9* d3dDevice = 0;
        d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);  
        DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
        for(int i = 0; i < 4; i++)
        {
                vtableFrag[i] = vtablePtr[i + 6];
        }
        d3dDevice->Release();
        d3d->Release();
        DestroyWindow(hWnd);
}

Если попроще, то он создаёт пустое и невидимое окно, получает его хэндл, создаёт для него новый D3D-объект и копирует кусочек VMT этого объекта. Затем всё уничтожает, но сохраняет кусочек VMT.

Фишка тут в том, что таблицы методов (VMT) получаются для разных D3D-объектов, ибо одновременно можно использовать только один (а его создаёт и использует игра), но вот по кусочку таблицы можно найти уже созданную ранее таблицу, которая и используется игрой. По механизму действия это похоже на поиск по сигнатуре.

В общем, после выполнения этого метода у нас есть "зацепка" - сигнатура VMT для D3D-объекта, который используется игрой в данный момент. Идём дальше. Что там дальше? Ага, CheckAndHookPresent9. Вот он:


void CheckAndHookPresent9()
{
        if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
        HANDLE heap = 0;
        HMODULE hKern = GetModuleHandleA("kernel32.dll");    
        GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
        if(getProcessHeaps != 0)
        {
                HANDLE heaps[1000];
                int numHeaps = (getProcessHeaps)(1000, heaps);
                for(int k = 0; k < numHeaps; k++)
                {
                        heap = heaps[k];
                        if(SearchHeap(heap)) break;
                }
        }
        else
        {
                heap = GetProcessHeap();
                SearchHeap(heap);
        }
        HeapLock(heap);
        if(presentPtr != 0)
        {
                (*presentPtr) = (DWORD)hkPresent;            
        }
        if(resetPtr != 0)
        {
                (*resetPtr) = (DWORD)hkReset;
        }
        HeapUnlock(heap);
}

Этот метод делает довольно тривиальтую штуку - ищет в куче (Heap) процесса указатели на две нужных нам функции - Present и Reset. Ищет он при помощи метода SearchHeap, в котором нам интересна только пара деталей:


BOOL SearchHeap(HANDLE heap)
{
        int vtableLenBytes = sizeof(DWORD)*4;
        PROCESS_HEAP_ENTRY mem;
        mem.lpData = 0;
        while(HeapWalk(heap, &mem))
        {
                if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
                DWORD* p = (DWORD*)mem.lpData;
                for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
                {
                        if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
                        {
                                presentPtr = p + 11;
                                resetPtr = p + 10;
                                offsetPresent = *presentPtr;
                                offsetReset = *resetPtr;
                                g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
                                g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
                                break;
                        }
                        p++;
                }
                if(presentPtr != 0) break;
        }
        return(presentPtr != 0);
}

Откуда взялись смещения 10 и 11? А всё просто. VMT - это таблица указателей, в которой указатели на функции всегда расположены в одинаковом порядке. Собственно, 10 и 11 - это порядковые номера функций.

У нас есть кусочек VMT, мы идём и ищем такой же кусочек в куче процесса. Как только нашли - отсчитываем от него 10 - это будет указатель на Reset, отсчитываем 11 - указатель на Present.

И всё, нужные указатели нашли, осталось только их поменять на свои. Этим будет заниматься HookVTableFunction:


PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
    DWORD dwOld = 0;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
    PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
    (*dwVTable)[Index] = (DWORD)dwHook;
    VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
    return pOrig;
}

Она принимает указатель на текущее устройство D3D, указатель на новую функцию и порядковый номер функции. Вы спросите, откуда же мы возьмём устройство D3D? А всё просто. В методе CheckAndHookPresent9 мы уже заменили указатели на оригинальные функции нашими собственными. Интересовать нас будет изменённая функция hkPresent:


HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{    
        while(!npDevice)
        {
        npDevice = pDevice;
        }
        DrawIndicator(pDevice);
        return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}

Первыми же тремя строчками мы сохраняем себе адрес D3D-устройства, которое попыталось вызвать Present, но так как мы поменяли указатель в VMT - попало сюда. После этого мы можем спокойно использовать все его функции, будто это наша собственная программа, чем мы и пользуемся - вызываем DrawIndicator, который я рассмотрю чуть ниже. В общем, ясно, да? Получили устройство, установили все указатели на нужные нам.

Как выглядело раньше:

Игра - D3DDevice - VMT-Present - D3D9.DLL

Как выглядит теперь:

Игра - D3DDevice - VMT-hkPresent - hook.dll - D3D9.DLL

А вот и метод DrawIndicator, который банально рисует "менюшку":


void DrawIndicator(LPVOID self)
{
        IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
        dev->BeginScene();
        D3DRECT rec = { 10, 10, 30, 30 };
        D3DCOLOR color = 0;
        if(indicator)
        {
                color = D3DCOLOR_XRGB(0, 255, 0);
        }
        else
        {
                color = D3DCOLOR_XRGB(255, 0, 0);
        }
        dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
        dev->EndScene();
}

Говорим, что из аргументов нам пришло устройство D3D, вызываем у него BeginScene(), чтобы порисовать, создаём квадрат 20х20 пикселей в верхнем левом углу экрана. Дальше смотрим, если indicator == true (VK_F1 и KeyboardHook все помнят?) - рисуем зелёным цветом, если false - то красным.

Рисуем мы, собственно, методом Clear() - просто очищаем выбранную область выбранным цветом, затем вызываем EndScene() и выходим.

Вот и результат, для нетерпеливых:

Оговорюсь, для запуска всего этого надо открыть игру в Cheat Engine, перейти в отладчик и найти там в меню "Tools - Inject DLL".


В принципе, вооружившись бумажкой и карандашом можно рисовать уже сейчас, но в следующем уроке (статье?) попробуем прикрутить сюда какой-нибудь шрифт. Доработок ещё уйма предстоит, этот способ работает не везде (например, у меня не работает в StarCraft II), да и много чего можно улучшить.

Такие вот дела. 200 строчек кода - а сколько веселья. :)










Комментариев нет:

Отправить комментарий

Не люблю мат и низкий уровень грамотности. Чем конкретнее поставите свой вопрос и чем лучше он будет выглядеть - тем большая вероятность на мой ответ. :)