Как преобразовать функцию в x86 и x64? (Как std::bind в C++, но динамический)

Как преобразовать произвольную функцию с произвольным (фиксированным) числом аргументов на x86 и x64?

(Мне не нужны числа с плавающей запятой, SSE и т.п. Все аргументы — целые числа или указатели.)


person user541686    schedule 27.08.2012    source источник


Ответы (2)


Вот моя общая реализация.

Сначала я сделал это с помощью AsmJit, а затем изменил его вручную, чтобы удалить зависимость.

  • Он работает как для x86, так и для x64!

  • Он работает как для cdecl и stdcall!
    Он должен также работать для thiscall как на VC++, так и на GCC, но я не проверял это.
    (VC++, вероятно, не коснется указателя this, тогда как GCC будет рассматривать его как первый аргумент.)

  • Он может связывать произвольное количество аргументов в любой позиции в списке параметров!

Просто будьте осторожны:

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

  • Он не был разработан для сверхвысокой производительности, но все равно должен быть достаточно быстрым.
    Скорость равна O(общее количество параметров), не O(связанный параметр считать).

Прокрутите вправо, чтобы увидеть ассемблерный код.

#include <stddef.h>

size_t vbind(
    void *(/* cdecl, stdcall, or thiscall */ *f)(), size_t param_count,
    unsigned char buffer[/* >= 128 + n * (5 + sizeof(int) + sizeof(void*)) */],
    size_t const i, void *const bound[], unsigned int const n, bool const thiscall)
{
    unsigned char *p = buffer;
    unsigned char s = sizeof(void *);
    unsigned char b = sizeof(int) == sizeof(void *) ? 2 : 3;  // log2(sizeof(void *))
    *p++ = 0x55;                                                                          // push     rbp
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8B; *p++ = 0xEC;                                 // mov      rbp, rsp
    if (b > 2)
    {
        *p++ = 0x48; *p++ = 0x89; *p++ = 0x4C; *p++ = 0x24; *p++ = 2 * s;                 // mov      [rsp + 2 * s], rcx
        *p++ = 0x48; *p++ = 0x89; *p++ = 0x54; *p++ = 0x24; *p++ = 3 * s;                 // mov      [rsp + 3 * s], rdx
        *p++ = 0x4C; *p++ = 0x89; *p++ = 0x44; *p++ = 0x24; *p++ = 4 * s;                 // mov      [rsp + 4 * s], r8
        *p++ = 0x4C; *p++ = 0x89; *p++ = 0x4C; *p++ = 0x24; *p++ = 5 * s;                 // mov      [rsp + 5 * s], r9
    }
    if (b > 2) { *p++ = 0x48; } *p++ = 0xBA; *(*(size_t **)&p)++ = param_count;           // mov      rdx, <param_count>
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8B; *p++ = 0xC2;                                 // mov      rax, rdx
    if (b > 2) { *p++ = 0x48; } *p++ = 0xC1; *p++ = 0xE0; *p++ = b;                       // shl      rax, log2(sizeof(void *))
    if (b > 2) { *p++ = 0x48; } *p++ = 0x2B; *p++ = 0xE0;                                 // sub      rsp, rax
    *p++ = 0x57;                                                                          // push     rdi
    *p++ = 0x56;                                                                          // push     rsi
    *p++ = 0x51;                                                                          // push     rcx
    *p++ = 0x9C;                                                                          // pushfq
    if (b > 2) { *p++ = 0x48; } *p++ = 0xF7; *p++ = 0xD8;                                 // neg      rax
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8D; *p++ = 0x7C; *p++ = 0x05; *p++ = 0x00;       // lea      rdi, [rbp + rax]
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8D; *p++ = 0x75; *p++ = 2 * s;                   // lea      rsi, [rbp + 10h]
    if (b > 2) { *p++ = 0x48; } *p++ = 0xB9; *(*(size_t **)&p)++ = i;                     // mov      rcx, <i>
    if (b > 2) { *p++ = 0x48; } *p++ = 0x2B; *p++ = 0xD1;                                 // sub      rdx, rcx
    *p++ = 0xFC;                                                                          // cld
    *p++ = 0xF3; if (b > 2) { *p++ = 0x48; } *p++ = 0xA5;                                 // rep movs [rdi], [rsi]
    for (unsigned int j = 0; j < n; j++)
    {
        unsigned int const o = j * sizeof(p);
        if (b > 2) { *p++ = 0x48; } *p++ = 0xB8; *(*(void ***)&p)++ = bound[j];           // mov      rax, <arg>
        if (b > 2) { *p++ = 0x48; } *p++ = 0x89; *p++ = 0x87; *(*(int **)&p)++ = o;       // mov      [rdi + <iArg>], rax
    }
    if (b > 2) { *p++ = 0x48; } *p++ = 0xB8; *(*(size_t **)&p)++ = n;                     // mov      rax, <count>
    if (b > 2) { *p++ = 0x48; } *p++ = 0x2B; *p++ = 0xD0;                                 // sub      rdx, rax
    if (b > 2) { *p++ = 0x48; } *p++ = 0xC1; *p++ = 0xE0; *p++ = b;                       // shl      rax, log2(sizeof(void *))
    if (b > 2) { *p++ = 0x48; } *p++ = 0x03; *p++ = 0xF8;                                 // add      rdi, rax
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8B; *p++ = 0xCA;                                 // mov      rcx, rdx
    *p++ = 0xF3; if (b > 2) { *p++ = 0x48; } *p++ = 0xA5;                                 // rep movs [rdi], [rsi]
    *p++ = 0x9D;                                                                          // popfq
    *p++ = 0x59;                                                                          // pop      rcx
    *p++ = 0x5E;                                                                          // pop      rsi
    *p++ = 0x5F;                                                                          // pop      rdi
    if (b > 2)
    {
        *p++ = 0x48; *p++ = 0x8B; *p++ = 0x4C; *p++ = 0x24; *p++ = 0 * s;                 // mov      rcx, [rsp + 0 * s]
        *p++ = 0x48; *p++ = 0x8B; *p++ = 0x54; *p++ = 0x24; *p++ = 1 * s;                 // mov      rdx, [rsp + 1 * s]
        *p++ = 0x4C; *p++ = 0x8B; *p++ = 0x44; *p++ = 0x24; *p++ = 2 * s;                 // mov      r8 , [rsp + 2 * s]
        *p++ = 0x4C; *p++ = 0x8B; *p++ = 0x4C; *p++ = 0x24; *p++ = 3 * s;                 // mov      r9 , [rsp + 3 * s]
        *p++ = 0x48; *p++ = 0xB8; *(*(void *(***)())&p)++ = f;                            // mov      rax, <target_ptr>
        *p++ = 0xFF; *p++ = 0xD0;                                                         // call     rax
    }
    else
    {
        if (thiscall) { *p++ = 0x59; }                                                    // pop      rcx
        *p++ = 0xE8; *(*(ptrdiff_t **)&p)++ = (unsigned char *)f - p
#ifdef _MSC_VER
                - s  // for unknown reasons, GCC doesn't like this
#endif
            ;                                                                             // call     <fn_rel>
    }
    if (b > 2) { *p++ = 0x48; } *p++ = 0x8B; *p++ = 0xE5;                                            // mov      rsp, rbp
    *p++ = 0x5D;                                                                          // pop      rbp
    *p++ = 0xC3;                                                                          // ret
    return p - &buffer[0];
}

Пример (для Windows):

#include <assert.h>
#include <stdio.h>
#include <Windows.h>
void *__cdecl test(void *value, void *x, void *y, void *z, void *w, void *u)
{
    if (u > 0) { test(value, x, y, z, w, (void *)((size_t)u - 1)); }
    printf("Test called! %p %p %p %p %p %p\n", value, x, y, z, w, u);
    return value;
}
struct Test
{
    void *local;
    void *operator()(void *value, void *x, void *y, void *z, void *w, void *u)
    {
        if (u > 0) { (*this)(value, x, y, z, w, (void *)((size_t)u - 1)); }
        printf("Test::operator() called! %p %p %p %p %p %p %p\n", local, value, x, y, z, w, u);
        return value;
    }
};
int main()
{
    unsigned char thunk[1024]; unsigned long old;
    VirtualProtect(&thunk, sizeof(thunk), PAGE_EXECUTE_READWRITE, &old);
    void *args[] = { (void *)0xBAADF00DBAADF001, (void *)0xBAADF00DBAADF002 };
    void *(Test::*f)(void *value, void *x, void *y, void *z, void *w, void *u) = &Test::operator();
    Test obj = { (void *)0x1234 };
    assert(sizeof(f) == sizeof(void (*)()));  // virtual function are too big, they're not supported :(
    vbind(*(void *(**)())&f, 1 + 6, thunk, 1 + 1, args, sizeof(args) / sizeof(*args), true);
    ((void *(*)(void *, int, int, int, int))&thunk)(&obj, 3, 4, 5, 6);
    vbind((void *(*)())test, 6, thunk, 1, args, sizeof(args) / sizeof(*args), false);
    ((void *(*)(int, int, int, int))&thunk)(3, 4, 5, 6);
}
person user541686    schedule 27.08.2012
comment
log2 sizeof(int) не обязательно будет 2 - person obataku; 27.08.2012
comment
Изменение защиты памяти только для того, чтобы прервать вызов функции, в лучшем случае уродливо, а изменение защиты самого стека опасно. - person James McNellis; 27.08.2012
comment
@JamesMcNellis: Это просто пример: пример, чтобы продемонстрировать, как вы можете его использовать. Я никогда не утверждал, что он следует лучшим практикам кодирования (и я никогда не собирался этого делать). - person user541686; 27.08.2012
comment
@SecurityMatt: вам действительно нужно прочитать ответ, прежде чем голосовать против него. Вы прокрутили вправо, как я сказал, выделенный полужирным шрифтом? - person user541686; 02.09.2012
comment
Это нарушает всевозможные правила для 64-битной Windows. Эпилог содержит недопустимые инструкции. Вычисления в прологе запрещены. pushfq также не допускается. И он не регистрирует никаких кодов раскрутки в операционной системе для обработки исключений. - person Raymond Chen; 11.11.2013
comment
@RaymondChen: я понятия не имел о большинстве этих правил, спасибо, что сообщили мне. (Однако что касается исключений: я никогда не планировал использовать это для кода, использующего исключения, поэтому я игнорирую здесь все, что связано с безопасностью исключений.) Однако у меня есть вопрос: что пойдет не так, если пролог и эпилог не следовать конвенции? Связано ли это только с безопасностью исключений или вызывает проблемы, даже если исключения никогда не выдаются? - person user541686; 11.11.2013
comment
На самом деле вы не контролируете, используется ли этот код с исключениями, потому что исключения не находятся под вашим контролем. Предупреждение о взаимоблокировке, предупреждение о неверном дескрипторе, предупреждение о переполнении стека, копирование при записи — все это может произойти, и ядро ​​обработает исключение за вас. (Кроме того, я думаю, что вы неправильно выровняли стек.) - person Raymond Chen; 11.11.2013
comment
@RaymondChen: Хм, когда вы говорите предупреждение, вы имеете в виду защитные страницы или что-то еще? (И спасибо за примечание... Сейчас у меня нет времени заниматься исправлением выравнивания стека, но я попытаюсь исправить это позже.) - person user541686; 11.11.2013
comment
Предупреждение о тупиковой ситуации возникает, когда EnterCriticalSection блокируется слишком долго. Предупреждение о недопустимом дескрипторе возникает, когда вы передаете недопустимый дескриптор в CloseHandle. Копирование ресурса при записи вызывает нарушение прав доступа. Все они обрабатываются фильтром необработанных исключений по умолчанию. Это только те, о которых я мог подумать навскидку. Вероятно, есть и другие. Если вы не зарегистрируете свою функцию с помощью надлежащего пролога, ядро ​​завершит процесс, когда произойдет что-либо из этого, потому что оно не может пройти по стеку. - person Raymond Chen; 11.11.2013
comment
@RaymondChen: Ах, интересно, я этого не знал, большое спасибо, что указали на это. Когда у меня снова появится шанс, я исправлю это (но может потребоваться еще полгода или год или около того, чтобы у меня было время изучить это...). - person user541686; 11.11.2013

Вот модификация функций thiscall

Приведенный выше генератор заглушек vbind() также предназначен для использования в функциях-членах C++, хотя неясно, как действовать дальше. Вот что я придумал:

// experimental x64 thiscall thunking
class TestHook {
public:
    typedef void (TestHook::*TMFP)();

    TestHook(DWORD num) 
    {
        m_context = num;

        union { void* (*func)(); TMFP method; } addr;
        addr.method = (TMFP)CBTHook_stub;

        // pass "this" as the first fixed argument
        void *args[] = { this };
        size_t thunk_size = vbind(addr.func, 4, m_thunk, 0, args, 1);
        ATLASSERT(thunk_size < sizeof(m_thunk));

        unsigned long old;
        VirtualProtect(m_thunk, thunk_size, PAGE_EXECUTE_READWRITE, &old);
        FlushInstructionCache(GetCurrentProcess(), m_thunk, thunk_size);
    }

    FARPROC GetThunk() const {    return (FARPROC)(void*)m_thunk; }

protected:
    // test thiscall: one integer and two 8-byte arguments
    LRESULT CBTHook_stub(int nCode, WPARAM wParam, LPARAM lParam) 
    {
        ATLTRACE(_T("this=%p, code=%d, wp=%x, lp=%x, context=%x\n"), this, nCode, wParam, lParam, m_context);
        return lParam;
    }

    DWORD m_context;
    unsigned char m_thunk[1024]; // fixed; don't know size required apriori!
};

#ifndef _WIN64
#error does not work for win32
#endif
void main(void)
{
    TestHook tmp(0xDeadBeef);

    HOOKPROC proc = (HOOKPROC)tmp.GetThunk();
    ATLTRACE(_T("object %p return value=%d\n"), &tmp, proc(1, 2, 3));
}

Я не специалист по сборке, но этот код правильно вставляется в функцию-член для 64-битного кода. Есть некоторые неявные предположения (я не уверен на 100 %, что они верны, пожалуйста, поправьте меня, если я ошибаюсь):

  1. в x64 (amd/microsoft VS) все аргументы функции передаются длиной 8 байт. Таким образом, хотя vbind был только для аргументов типа указателя, можно использовать другие прототипы функций (например, HOOKPROC принимает одно целое число и два __int64)

  2. Указатель this передается в качестве первого аргумента стека в x64 вместо ECX. Я использовал ограниченный аргумент для передачи указателя this и предоставления контекста объекту C++.

person nikos    schedule 10.11.2013
comment
В 32-битной версии thiscall была ошибка, теперь она должна быть исправлена. - person user541686; 11.11.2013
comment
ваши модификации были только для 32-битной версии, для 64-битной это GPF. Обратите внимание, что 32-битное преобразование намного лучше обрабатывается в CAuxThunk, входящем в состав ATL/. Библиотека AUX - person nikos; 11.11.2013