본문 바로가기

3.구현/C or C++

Thunk와 용법3 - 인터페이스 프록시

드디어 thunk의 마지막 용법이다. 별다른 것은 아니고, 함수 호출시 중간에 thunk에 의해서 호출되는 기능이 결정되는 것일 뿐이다. 사실 John TWC의 글을 읽으면서 interface proxy에 thunk를 이용할 필요가 있을까하는 의문이 들었다.
이글을 정리해서 올리면서도 왜 사용하는지에 대해서는 의문이 들었다. 아직 내공이 부족한가보다...
작성: http://ospace.tistory.com/,2010.01.11 (ospace114@empal.com)

Interface Proxy

Interface Proxy라는 것은 다양한 인터페이스는 하나의 proxy로 접근하겠다는 의도이다. 예를 들어 하나의 모듈이 있다면, 이 모듈에는 IOne과 ITwo 인터페이스가 있다. 모듈은 다양한 기능을 사용하기 위해서는 적절한 인터페이스를 통해서 접근해야 한다.

이 인터페이스를 액세스하기 위한 객체는 어떻게 가져올 수 있을까하는 문제가 있다. 물론 모듈을 생성해서, 모듈에 바로 연산을 할 수 있지만, 여기서는 interface proxy라는 것을 통해서 인터페이스에 접근하고 있다. 즉, interface proxy에서 인터페이스에 접근할 객체를 얻고 그 객체에 연산을 수행하는 방식이다.

여기서 사용한 인터페이스는 좀더 구체적으로 정의해보자.

//////////////////////////////////////////////////////////////
// Interface
class IOne
{
public:
    virtual void memFunOne1(int arg1) = 0;
    virtual void memFunOne2(int arg1, char arg2) = 0;
};
class ITwo
{
public:
    virtual void memFunTwo() = 0;
};

IOne 인터페이스는 2개의 메소드가 있고, ITwo 인터페이스에는 1개의 메소드가 있다. 이를 구현한 클래스는 다음과 같다.

#include <iostream>
//...
//////////////////////////////////////////////////////////////
// Interface Implementation
class OneImpl : public IOne
{
public:
    void memFunOne1(int arg1)
    {
        std::cout << "memFunOne1: " << arg1 << std::endl;
    }
    void memFunOne2(int arg1, char arg2)
    {
        std::cout << "memFunOne2: " << arg1 << ", " << arg2 << std::endl;
    }
};

class TwoImpl : public ITwo
{
public:
    void memFunTwo()
    {
        std::cout << "memFunTwo: " << std::endl;
    }
};

특별한 작업은 없다. 단지, 인자를 받아서 출력만 해준다.

다음으로 가기 전에 Interface ID(이하 IID)를 살펴보고 가자. IID는 말 그대로 인터페이스를 식별하기 위한 식별자이다. 하나의 모듈에 여러 인터페이스가 있다면, 원하는 인터페이스를 가져오기 위해서 IID를 사용한다는 것이다. 이 부분은 COM에서도 사용하는 방식이다. 어찌됬든 IID를 사용해서 원하는 인터페이스 객체를 획득한다. 얻은 인터페이스 객체에 작업을 수행한다.

Proxy Implementation

Proxy Implementation은 앞에서 구현한 OneImpl과 TwoImpl에 대한 접근을 알아서 해주는 함수를 구현하는 부분이다. 굳이 함수가 아니라 클래스로도 정의할 수도 있다. 간단히 하기 위해서 함수를 사용했다.

이 함수는 사용자에게 보여지는 함수는 아니고, 내부적으로 모든 인터페이스에 대한 접근을 하나의 proxy로 접근해서 처리하는 기능을 가진다. 이를 위해서 수행할 인터페이스가 무엇이가, 어떤 연산을 수행할지를 알아야 하고, 연산을 수행할때 사용할 인자를 받아오게 된다.

그럼, Proxy Implementation에 사용된 함수 시그니처를 보자.

static void ProxyImpl(int midx, int iid, int client_site_addr, void* pThis);

첫 번째 인자는 midx은 method index 로 인터페이스에 몇 번째 메소드를 호출할지를 가리킨다. 그리고 다음 iid인 인터페이스 식별자이다. iid를 가지고 인터페이스를 식별하고 midx로 원하는 연산자를 식별해서 호출한다.

세 번째 인자인 client_site_addr은 함수 반환 주소로서 무시하면 된다. 다음 pThis는 클래스 인스탄스 객체(this)가 넘겨지는 부분이다. 이 부분은 함수 호출규약에 의해서 스택에 저장되어서 넘어오기 때문에, 인자로서 받을 수 있다. 그러나 여기서는 사용되지 않고, 다른 인자를 계산할 때 사용할 것이다. 가변인자처럼 사용될 것이다.

실제 구현된 예를 보자.

// simgle proxy implementation
static void __cdecl ProxyImpl(int midx, int iid, int client_site_addr, void* pThis)
{
    static IOne* pOne = new OneImpl();
    static ITwo* pTwo = new TwoImpl();

    if(0 == iid) {
        if( 0 == midx ) {
            char* arg_addr = (char*)&pThis;
            int arg1 = *(int*)(arg_addr + 4);

            pOne->memFunOne1(arg1);
        } else if( 1 == midx ) {
            char* arg_addr = (char*)&pThis;
            int arg1 = *(int*)(arg_addr + 4);
            arg_addr += 4;
            char arg2 =*(char*)arg_addr;

            pOne->memFunOne2(arg1, arg2);
        }
    } else if( 1 == iid) {
        if( 0 == midx ) {
            pTwo->memFunTwo();
        }
    }
}

코드가 좀 지저분하다. John TWC의 예제 코드를 나름 간략화하여 정리했는데도 좀 지저분하다.

IID와 midx를 구분하면

  • IID 0: IOne 인터페이스
    • midx 0: memFunOne1()
    • midx 1: memFunOne2()
  • IID 1: ITwo 인터페이스
    • midx 0: memFunTwo()

그리고 인자를 계산 하는 부분이 직접 포인터 연산을 통해서 얻어오고 있다.

char* arg_addr = (char*)&pThis;
int arg1 = *(int*)(arg_addr + 4);
pOne->memFunOne1(arg1);

이 부분도 마음에 안드는 것 중에 하나이다. 어찌됬든, ProxImpl은 모든 인자에 대해 정보를 알고 있고 적절한 타입으로 캐스팅해야 한다. 그리고 원하는 멤버함수로 호출하게 된다. Proxy implementation은 하나의 proxy 구현 예제일뿐 실제 구현에 있어서 이런 형태를 취할 필요는 없다고 본다.

원하는 인터페이스 객체에 대해서 연산을 실행할 수 있게 되었다. 이부분을 먼저 이야기한 이유가 나중에 thunk가 어떻게 ProxyImpl 함수를 사용하는지 이해를 돕기위해서이다.

ProxyImpl 함수에 의해서 인터페이스 프록시를 제공하여, 모든 인터페이스에 대한 접근이 가능해졌다. 이 것을 어떻게 사용하는가를 살펴보겠다.

전체적인 구조

전체 구조를 집고 넘어가자. 우리에게는 ProxyImpl라는 interface proxy 객체가 생겼다. Thunk를 사용하여 호출하는 과정을 그림으로 그려보았다.

호출되는 과정을 살펴보면,

  1. Client가 ProxyProvider에서 IID를 넘겨서 원하는 인터페이스를 요청한다.
  2. ProxyProvider는 해당 IID에 대한 가상함수테이블을 생성해서, 각각 메소드에 대해 IID와 midx(메소드 인덱스)로 초기화한 thunk 객체를 넣어서 이를 반환한다.
  3. Client는 획득한 인터페이스 객체에 대해 가상테이블에서 원하는 연산자를 호출한다.
  4. ProxyProvider에 의해 생성된 가상함수 테이블에서 호출한 연산자에 해당하는 thunk를 호출해서 실행한다.
  5. Thunk에서는 이미 설정된 IID와 midx를 이용해서 ProxyImpl를 호출한다.
  6. ProxyImpl에서는 IID와 midx를 이용해서 적당한 객체을 찾아서 연산을 수행한다.

ProxyProvider에서 생성된 가상함수 테이블은 IOne이나 ITwo 인터페이스에 대응되는 위치에 thunk 객체가 저장된다. 바로 thunk객체가 IOne이나 ITwo 인터페이스와 ProxImpl 간에 다리역활을 한다.

각 단계별 스택 구조를 살펴보면 다음과 같다. 일단 Client가 원하는 인터페이스(IOne)을 획득했다고 보자.

스택에 저장되는 데이터를 잘 알고 싶으면 함수 호출 규약을 참조하기 바란다. 전체적인 호출 순서를 잘 알 것이다.

Proxy Provider

Proxy Provider는 말 뜻 그대로 Proxy 객체를 제공하는 역활이다. 정확히 이야기 하면, 사용자가 IID로 원하는 인터페이스 객체를 요청하면, 해당 인테페이스에 대한 가상 테이블을 내부적으로 생성해서 반환해준다. 가상테이블을 생성하지만, 인터페이스를 획득하여 반환할 수도 있다. COM에서 비교하자면 Universal Marshaler(또는 Type Library Marshaler)와 비슷하다.

ProxyProvider는 thunk를 이용해서 ProxyImpl 함수를 호출한다. 이 thunk가 가상함수 테이블에 저장이된다.

먼저 가상함수 테이블이 생성되는 소스를 보자.

void ProxyProvider(int iid, void** ppv)
{
    // 메소드 개수 획득, 가상함수 테이블 생성개수가 됨
    int methods = FakeTypeLib::GetNumOfMethods(iid);
    int **vptr = new int*;
    int *vtable = new int[methods]; // 메소드 개수만큼 vtable에 포인터 생성

    // methods 개수만큼 thunk를 생성해서 vtable에 할당함
    for(int midx=0; midx < methods; ++midx) {
        thunk* pThunk = new thunk();
        int bytes_to_top = FakeTypeLib::GetAugumentStackSize(iid, midx);// + 4; //RA 크기?
        // ProxyImpl호출할 때 사용할 인자를 thunk 넘겨서 초기화
        pThunk->init(iid, midx, bytes_to_top);
        vtable[midx] = (int)pThunk;
    }
    (*vptr) = vtable;
    *ppv = vptr;
}

ProxyProvider를 작업시나리오를 정리하면,

  1. 요청한 iid 인터페이스에 대한 메소드 개수를 획득
  2. 획득한 메소드 개수만큼 가상함수 테이블을 생성
  3. 가상함수 테이블에 thunk를 인터페이스의 각메소드를 호출할 때 사용할 iid와 midx로 초기화하여 할당함
  4. 요청한 iid에 맞는 생성된 가상함수를 반환

여기서 새로운 것이 보인다. FakeTypeLib라는 클래스이다. 앞의 ProxyProvider에서 iid 인터페이스에 대한 메소드 개수나 thunk 초기화할 때 사용할 특정 메소드의 인자 크기 정보를 제공한다. 메소드 개수로는 생성할 가상함수 테이블 크기, 메소드 인자 크기로는 thunk 호출 종료해서 스택정리할 때 사용할 인자 크기에 사용된다.

어떻게 이런 정보를 제공하는지 살펴보자.

// Type library
// 실제 COM에서 인터페이스 정보는 type library를 파싱해서 획득함
class FakeTypeLib
{
public:
    // 인터페이스의 메소드 개수
    static int GetNumOfMethods(int iid)
    {
        if ( 0 == iid ) {
            // for class One
            return 2;
        }
        if ( 1 == iid ) {
            // for class Two
            return 1;
        }
        return 0;
    }
    // 특정 인터페이스와 메소드에 대한 인자 크기
    static int GetAugumentStackSize(int iid, int midx)
    {
        if( 0 == iid ) {
            // for class One
            if( 0 == midx ) {
                // for memFunOne1 method
                return 4;
            }
            if( 1 == midx ) {
                // for memFunOne2 method
                return 8;
            }
        }
        if( 1 == iid ) {
            // for class Two
            if( 0 == midx ) {
                // for memFunTwo method
                return 0;
            }
        }
        return 0;
    }
};

단순하다. 원하는 정보를 입력된 값으로 반환한다. 물론 각 인터페이스와 메소드 인자들의 크기는 사전에 알고 있어야 한다.

COM에서는 Type Library를 사용해서 이런 정보를 얻게 된다. Type Library는 바이너리 파일로 DLL에서 프로시저나 클래스 사용할 때 필요한 타입 정보를 가지고 있다.[3] 이 곳에서 원하는 정보를 파싱해서 획득할 수 있다.

THUNK

마지막으로 thunk를 보자. thunk코드는 인터페이스의 메소드 호출과 ProxyImpl 함수와 연동함으로서 ProxyImpl 함수에서 적절한 연산을 수행할 수 있도록하는 중재자 역활을 한다. 그럼 어셈블리 코드를 보면서, 인터페이스의 메소드가 호출될때 어떻게 작동하는지 살펴보자. 아래는 thunk에 들어 있는 코드이다.

push iid       // ProxyImpl에 넘겨질 두 번째 인자
push midx      // ProxyImpl에 넘겨질 첫 번째 인자
call ProxyImpl // ProxyImpl 함수 호출
add  esp, 8    // cdecl 호출규약으로 호출자에서 스택정리함
ret  bytes_parameters // 인터페이스 메소드가 thiscall 호출규약으로 피호출자가 스택정리

위의 코드를 적절한 iid와 midx를 thunk 구조체에 저장해서 인터페이스의 메소드 호출할때 대신 실행이 된다.

실행될때 이미 저장된 iid와 midx 을 가지고 ProxyImpl에 넘기므로 원하는 연산을 수행할 수 있게 된다.

실제 thunk를 생성하는 코드를 샆펴보자.

#pragma pack(push,1)
struct thunk
{
    unsigned char push_interface; // push iid (interface id)
    unsigned long interface_id;
    unsigned char push_method;    // push midx (method index)
    unsigned long method_idx;
    unsigned char call;           // call ProxyImplementation
    unsigned long func_offset;
    unsigned short add_esp;        // add esp, 8 (pop iid and midx)
    unsigned char bytes_to_add;   // 0x08
    unsigned char retn;           // retn bytes_to_pop
    unsigned int  bytes_to_pop;

    void init(int iid, int midx, int bytes)
    {
        push_interface = 0x68;  // push
        interface_id   = iid;
        push_method    = 0x68;  // push
        method_idx     = midx;
        call           = 0xe8;  // call
        func_offset    = (unsigned long)&ProxyImpl - (unsigned long)&add_esp;
        add_esp        = 0xc483;// add
        bytes_to_add   = 0x08;
        retn           = 0xc2;  // ret
        bytes_to_pop   = bytes; //(return and pop the normal arguments of the method)
        // FlushInstructionCache(GetCurrentProcess(), this, sizeof(thunk));
    }
    void* operator new(size_t)
    {
        return ::VirtualAlloc(NULL, PAGE_SIZE, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    }
    void operator delete(void* p)
    {
        ::VirtualFree(p, 0, MEM_RELEASE);
    }
};
#pragma pack(pop)

thunk은 앞의 "Thunk와 용법"에서 설명했기 때문에 자사한 부분을 설명하지 않겠다.

결론

Interface Proxy에서 thunk로 원하는 연산을 하기 위해서는 복잡하고 정교한 작업을 수행해야한다. 필자도 이 부분을 정리하면서 잘못된 위치로 함수 리턴이되고, 호출 규약이 맞지 않아서 에러가 발생하였다. 즉, 조그만한 실수로 크리티컬한 에러가 발생한다는 의미이다. 단순 에러값 리턴을 체크해서 처리할 수 있는 문제가 아니다.

그래서, 필자는 이런 방법은 그다지 좋게 보이지 않는다. 어렵다. 그래도 thunk를 활용할 수 있는 방법 중에 하나로서 참고할 만하다고 생각한다. 어느 누군가는 좀더 세련되게 변경해서 사용할 수도 있다고 생각한다.

이 글이 thunk 용법에 대한 마지막 글이 될 것 같다. 글을 쓰면서 나도 몰랐던 부분을 알 수 있었고 도움이 많이 되었다.

모두 즐프하기 바란다. ospace.

Resources

Source: test_interfaceProxy.cpp

참조

[1] John TWC, 2008, "Thunks and its uses", http://www.codeproject.com/KB/cpp/Thunk_uses.aspx

[2] 박재성, "함수호출 규약"

[3] What Is a Type Libary?, http://vb.mvps.org/hardcore/html/whatistypelibrary.htm

반응형

'3.구현 > C or C++' 카테고리의 다른 글

C++ Delegate 구현원리  (2) 2011.01.17
함수호출 규약  (2) 2011.01.12
Thunk와 용법2 - 콜백함수를 멤버함수로 사용하기  (0) 2011.01.12
Thunk와 용법1 - 다중 상속에서 가상함수  (2) 2011.01.12
가상함수 구조  (0) 2010.12.07