Можем ли мы реализовать преобразователь С++ в Linux?

Я хочу использовать функции-члены класса в качестве обратных вызовов, я не использую libsigc, потому что это медленно. В ATL мы можем использовать функцию-член для обратного вызова в стиле C (http://www.codeproject.com/KB/cpp/SoloGenericCallBack.aspx), поэтому можем ли мы реализовать преобразователь C++ в Linux?

Код ниже приведет к сбою:

#include <assert.h>
#include <stdio.h>
#include <sys/mman.h>

typedef char BYTE;
typedef int DWORD;
typedef int* DWORD_PTR;
typedef int* INT_PTR;
typedef bool BOOL;
typedef unsigned long ULONG;
typedef unsigned long* ULONG_PTR;
#define PtrToUlong( p ) ((ULONG)(ULONG_PTR) (p) )
#define __stdcall __attribute__((__stdcall__))

//#pragma pack( push, 1 )
struct  MemFunToStdCallThunk
{
    BYTE          m_mov;
    DWORD      m_this; 
    BYTE          m_pushEax;
    BYTE          m_jmp;
    DWORD      m_relproc;

    void  Init( DWORD_PTR proc, void* pThis )
    {
        printf("proc=%x\n", proc);
        m_mov = 0xB8;   // mov eax
        m_this = PtrToUlong(pThis);
        m_pushEax = 0xc3;// push eax
        m_jmp = 0xe9;          //jmp
        m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(MemFunToStdCallThunk)));
        printf("m_relproc = %x\n", m_relproc);
        mprotect(this, sizeof(MemFunToStdCallThunk), PROT_READ|PROT_WRITE|PROT_EXEC);
    }

    void* GetCodeAddress()
    {
        return this;
    }
}__attribute__ ((packed));
//#pragma  pack( pop )

template< typename TDst, typename TSrc >
TDst  UnionCastType( TSrc src )
{
    union
    {
        struct
        {
            int* pfn;  //function,index
            long delta; // offset, 
        }funcPtr;
        TSrc  uSrc;
    }uMedia;
    uMedia.uSrc  = src;
    return uMedia.funcPtr.pfn;
}




typedef  int  ( __stdcall *StdCallFun)(int, int);
class  CTestClass
{
public:
    int  m_nBase;
    MemFunToStdCallThunk  m_thunk;

    int  memFun( int m, int n )
    {
        int  nSun = m_nBase + m + n;
        printf("m=%d,n=%d,nSun=%d\n", m, n, nSun);
        return 1234;
    }

public:
    CTestClass()
    {
        m_nBase  = 10;
    }

    void  Test()
    {
        printf("%x\n", &CTestClass::memFun);
        m_thunk.Init(UnionCastType<DWORD_PTR>(&CTestClass::memFun), this );
        StdCallFun fun = (StdCallFun)m_thunk.GetCodeAddress();
        assert( fun != NULL );

        int ret =   fun( 9, 3 );
        printf("ret = %x\n", ret);


    }
};



int main()
{
    CTestClass test;
    test.Test();
    return 0;
}

РЕДАКТИРОВАТЬ: Благодаря user786653 я получаю правильный ответ:

#include <assert.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

typedef char BYTE;
typedef int DWORD;
typedef int* DWORD_PTR;
typedef int* INT_PTR;
typedef bool BOOL;
typedef unsigned long ULONG;
typedef unsigned long* ULONG_PTR;
#define PtrToUlong(p) ((ULONG)(ULONG_PTR) (p) )
#define __stdcall __attribute__((__stdcall__))

struct  MemFunToStdCallThunk
{
    BYTE          m_repairStack[10];
    DWORD      m_mov;
    DWORD      m_this;
    BYTE          m_jmp;
    DWORD      m_relproc;

    void  Init( DWORD_PTR proc, void* pThis )
    {
        printf("proc=%p\n", proc);
        m_repairStack[0] = 0x83; //sub esp, 0x4
        m_repairStack[1] = 0xec;
        m_repairStack[2] = 0x04;
        m_repairStack[3] = 0x8b; //mov eax,[esp + 0x4]
        m_repairStack[4] = 0x44;
        m_repairStack[5] = 0x24;
        m_repairStack[6] = 0x04;
        m_repairStack[7] = 0x89;//mov [esp], eax
        m_repairStack[8] = 0x04;
        m_repairStack[9] = 0x24;
        m_mov = 0x042444C7;   // mov   dword   ptr   [esp+0x4], 
        m_this = PtrToUlong(pThis);
        m_jmp = 0xe9;          //jmp
        m_relproc = (DWORD)proc - ((DWORD)this+sizeof(MemFunToStdCallThunk));
        printf("m_relproc = %d\n", m_relproc);
        //long page_size = sysconf(_SC_PAGE_SIZE);
        //mprotect((void*)(PtrToUlong(this) & -page_size), 2*page_size, PROT_READ|PROT_WRITE|PROT_EXEC);
    }

    void* GetCodeAddress()
    {
        return this;
    }
}__attribute__ ((packed));


template< typename TDst, typename TSrc >
TDst  UnionCastType( TSrc src )
{
    union
    {
        struct
        {
            int* pfn;  //function or index
            long delta; // offset
        }funcPtr;
        TSrc  uSrc;
    }uMedia;
    uMedia.uSrc  = src;
    return uMedia.funcPtr.pfn;
}




typedef  int  ( __stdcall *StdCallFun)(int, int);
class  CTestClass
{
public:
    int  m_nBase;
    MemFunToStdCallThunk  m_thunk;

    int  memFun( int m, int n )
    {
    printf("this=%p\n", this);
        int  nSun = m_nBase + m + n;
        printf("m=%d,n=%d,nSun=%d\n", m, n, nSun);
        return nSun;
    }

public:
    CTestClass()
    {
        m_nBase  = 10;
    }

    void  Test()
    {
        int (CTestClass::*abc)(int, int);
        printf("sizeof(MemFunToStdCallThunk)=%d,sizeof(abc)=%d\n", sizeof(MemFunToStdCallThunk), sizeof(abc));
        printf("memFun=%p\n", &CTestClass::memFun);
        m_thunk.Init(UnionCastType<DWORD_PTR>(&CTestClass::memFun), this );
        StdCallFun fun = (StdCallFun)m_thunk.GetCodeAddress();
        assert( fun != NULL );

        int ret = memFun(2, 3);
        printf("ret 1= %d\n", ret);
        ret =  fun( 9, 3 );
        printf("ret 2= %d\n", ret);
    }
};


int main()
{
    CTestClass test;
    test.Test();
    return 0;
}

person xufan    schedule 11.09.2011    source источник


Ответы (4)


Да, но я бы не рекомендовал. Это (очевидно) сделает ваш код намного менее переносимым, и вы потенциально открываете дыру в безопасности, если не будете осторожны.

Вам нужно будет сделать код исполняемым с помощью mprotect(2). Что-то вроде mprotect(&thunk_struct, sizeof(struct _CallBackProcThunk), PROT_READ|PROT_WRITE|PROT_EXEC).

Также нормальный синтаксис GCC для упаковки структуры — struct S { /* ... */ } __attribute__ ((packed)), хотя более новые версии могут поддерживать синтаксис #pragma pack.

Возможно, вы также захотите заменить DWORD на uint32_t вместо stdint.h и BYTE на uint8_t (или просто вставить туда typedef).

РЕДАКТИРОВАТЬ:

Из справочной страницы на mprotect "[..]addr должен быть выровнен по границе страницы". Вы должны проверить возвращаемое значение. Вместо этого попробуйте сделать что-то вроде этого:

long page_size = sysconf(_SC_PAGE_SIZE);
uintptr_t addr = ((uintptr_t)this) & -page_size;
if (mprotect((void*)addr, 2*page_size, PROT_READ|PROT_WRITE|PROT_EXEC)) {
    perror("mprotect"); 
    /* handle error */
}

Следующий расчет неверен:

DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(MemFunToStdCallThunk)))

Он делает свои расчеты на int*.

(DWORD)proc - ((DWORD)this+sizeof(MemFunToStdCallThunk)

здесь должно быть достаточно.

Далее следует очень уродливый (не переносимый и т.д. и т.п.), но небольшой и автономный пример:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>

struct thunk {
    uint32_t mov;
    uint32_t this_ptr;
    uint8_t jmp;
    uint32_t rel;
} __attribute__((packed));

class Test {
public:
    virtual int foo(void) {
        printf("foo! %p\n", (void*)this);
        return 42;
    }
};

int main()
{
    Test test;
    printf("%d\n", test.foo());

    thunk t;
    t.mov = 0x042444C7;
    t.this_ptr = (uint32_t)&test;
    t.jmp = 0xe9;
    t.rel = ((uint32_t)(void*)&Test::foo) - ((uint32_t)&t + sizeof(thunk));

    uint32_t addr = (uint32_t)&t;
    long page_size = sysconf(_SC_PAGE_SIZE);
    if (mprotect((void*)(addr & -page_size), 2*page_size, PROT_READ|PROT_WRITE|PROT_EXEC)) {
        perror("mprotect");
        return 1;
    }

    union {
        void* p;
        int (*foo)(int);
    } u;
    u.p = &t;
    printf("%d\n", u.foo(0));
    return 0;
}
person user786653    schedule 11.09.2011
comment
Я написал тестовый код (см. выше), но он вылетит (ошибка сегментации) при вызове инструкции jmp. - person xufan; 11.09.2011
comment
Спасибо, я получил правильный ответ. Вызываю ли я mprotect или нет, это не имеет значения. - person xufan; 12.09.2011
comment
+1 Добавлю, что ATL не делает стек исполняемым (это немного рискованно), а выделяет страницы памяти и пытается использовать одну выгружаемую память для многих переходов (чтобы не тратить память впустую) - person xanatos; 12.09.2011
comment
@xufan: Будьте осторожны, если вы не вызываете mprotect с PROT_EXEC, ваш код, вероятно, не будет работать на 64-битных процессорах, работающих в 32-битном режиме, даже если 32-битные процессоры не проверяют (не могут) его. - person user786653; 12.09.2011

Разумный подход примерно такой:

struct Foo {
   void doit();
};

extern "C" {
   void callback(void *handle) {
      reinterpret_cast<Foo*>(handle)->doit();
   }
}

Сборка обратного вызова выглядит вот так (x64):

callback:
    jmpq    _ZN3Foo4doitEv
person Maister    schedule 11.09.2011
comment
Обычно он встраивает doit(), если определение доступно. - person Maxim Egorushkin; 12.09.2011

Вы не можете напрямую передавать указатели на элементы в обратные вызовы C, но есть переносимые трюки (т. е. не ограниченные одной целевой ОС), которые работают очень хорошо.

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

void wrapper()
{
  object->callWhatever();
}

Вы можете передать wrapper() как указатель на функцию.

См. также, например, приведение функции-члена для вызова create_pthread(), чтобы узнать, как обрабатывать случаи, когда вы получаете параметр void* с обратным вызовом и хотите использовать это для хранения (напрямую или нет) ссылки/указателя на объект, с которым вы хотите работать.

person Mat    schedule 11.09.2011

Я хочу использовать функции-члены класса в качестве обратных вызовов, я не использую libsigc, потому что это медленно. В ATL мы можем использовать функцию-член для обратного вызова в стиле C (http://www.codeproject.com/KB/cpp/SoloGenericCallBack.aspx), поэтому можем ли мы реализовать преобразователь C++ в Linux?

Вы, вероятно, можете. Однако в этом нет необходимости.

Большинство асинхронных API позволяют передавать аргумент void* при регистрации на асинхронное событие. Когда о событии сообщается, об этом void* также сообщается, и его можно использовать для вызова функции-члена объекта. (Расплывчатый язык, потому что такие API, как epoll_wait(), на самом деле не перезванивают вам, в отличие от pthread_create()).

person Maxim Egorushkin    schedule 12.09.2011