|
장성호 님이 쓰신 글 :
: 정말 감사합니다.
: 실력이 부족해 완전히 이해하지는 못했지만
: MakeObjectInstance의 흐름에 대해 어느정도는 이해하게 됬습니다.
:
: 추가로 올리신 내용도 MakeObjectInstance 를 보면서
: VCL말고 다른 MFC나 윈도우 Library에서는 어떻게 처리하는지 궁금해하고 있던 내용입니다.
:
:
: 덕택에 참 많이 배웁니다.
: 얼마전 MethodAddress 관한 질문에서도 그렇구...
:
: 요즘 틈나면 VCL을 조금씩 보는데 실력도 딸리고 Delphi문법도 익숙치 않아 많이 헤맵니다.
: 앞으로도 잘 부탁드립니다.
:
: 그럼
:
: 0 님이 쓰신 글 :
: : 장성호 님이 쓰신 글 :
: : : MakeObjectInstance에 대해 누가좀 가르쳐 주세요
: : :
: : :
: : : C++Builder를 이용해 프로그램하면서 Sub-classing이 필요한경우
: : : 폼에다가 TWndMethod 형의 메소드를 만들어서
: : : //typedef void __fastcall (__closure *TWndMethod)(Messages::TMessage &Message);
: : : WindowProc를 교체하여서 하곤 했었습니다.
: : :
: : : 그런데 얼마전 MakeObjectInstance 라는 VCL함수가 있는줄 알게되었습니다.
: : : 뭐 아래와 같은식으로 사용되더군요
: : :
: : : void *proc;
: : : FARPROC origProc;
: : : void TForm1::SubWndProc(TMessage &msg)
: : : {
: : : ...
: : : Msg.Result = CallWindowProc(fProc,Button1->Handle,Msg.Msg,Msg.WParam,Msg.LParam);
: : : };
: : : //----------------------------------------
: : : proc=MakeObjectInstance((SubWndProc);
: : : fProc=SetWindowLong(Button1->Handle, GWL_WNDPROC, (long)proc));
: : : //----------------------------------------
: : : SetWindowLong(Button1->Handle, GWL_WNDPROC, (long)fproc));
: : : FreeObjectInstance(proc);
: : :
: : : 사용하는 방법을 몰라서 질문 올리는것이 아니라
: : : 원리를 알고 싶습니다.
: : :
: : : MakeObjectInstance 함수의 소스코드를 보니까
: : :
: : : VirtualAlloc 이라는 kernel32.dll에 함수를 쓰더군요
: : : BlockCode[2]={0x59,0xE9}; //0x59 ={ POP ECX } , 0xE9 ={ JMP StdWndProc }
: : : BlockCode 라는 값을 확보한 메모리로 복사하고
: : : StdWndProc와 Offset값을 계산해서 설정하고..
: : : 뭐 이것저것 하던데요
: : :
: : : 실력이 딸려서 정확히 이해하지 못하겠습니다.
: : :
: : : 누가 MakeObjectInstance 함수에 대해 좀 가르쳐 주세요
: : : 왜 그렇게 사용하는지도 함께..
: :
: : - 윈도우 인스턴스: CreateWindow로 만든 윈도우
: : - 클래스 인스턴스: C++ 클래스의 인스턴스
: : - VCL의 윈도우 클래스: 윈도우를 다루는 VCL 클래스
: : - 윈도우 클래스: WNDCLASS
: :
: : MS Windows의 WNDCLASS 구조체의 lpfnWndProc 멤버에 지정된 게 해당 윈도우 클래스의 윈도우 프러시저이고
: : 이 윈도우 프러시저 함수가 해당 윈도우 클래스로 만들어진 모든 윈도우 인스턴스의 모든 메시지를 처리합니다.
: : lpfnWndProc 멤버는 WNDPROC 형인데 이는 함수 포인터형으로서 __closure와는 다릅니다.
: : LRESULT CALLBACK WindowProc(
: : HWND hwnd, // handle of window
: : UINT uMsg, // message identifier
: : WPARAM wParam, // first message parameter
: : LPARAM lParam // second message parameter
: : );
: : 이 윈도우 프러시저는 위와 같은 형태로서 C++의 멤버 함수가 아닌 C 언어의 일반 함수입니다.
: : 이를 보면 그 어디에도 클래스의 인스턴스 포인터와 그 윈도우 핸들을 연관시켜줄 만한 멤버가 없을뿐만아니라
: : 윈도우 인스턴스를 만들기 전에 등록해야 하는 것이므로 더더욱 불가능합니다.
: : (운영체제에게 인스턴스를 미리 알려줄 방법도 없습니다)
: : CreateWindow 호출로 얻은 윈도우 핸들을 가지고 별도의 특별한 작업을 해주어야
: : 비교나 탐색 없이 C++ 클래스의 인스턴스를 대상으로 멤버 함수인 윈도우 프러시저를 운영체제가 호출하게 하는 효과를 낼 수 있습니다.
: :
: : VCL의 윈도우 클래스들은 C++ 클래스로 되어있고 그 멤버 함수를 통하여 조작합니다.
: : 클래스의 멤버 함수를 호출하기 위해서는 클래스의 인스턴스를 지정해주어야 합니다.
: : 예를 들어, TForm1 *Form1; Form1->Show();에서 Form1이 클래스의 인스턴스 포인터이고 Show가 멤버 함수입니다.
: : 어떤 인스턴스에 대해 클래스의 어떤 멤버 함수를 호출할 것인가를 하나의 변수로 저장하기 위해 만든 것이 __closure인데 상위 4바이트는 클래스의 인스턴스 포인터이고 하위 4바이트는 해당 멤버 함수의 주소입니다.
: :
: : VCL의 윈도우 클래스에도 윈도우 프러시저가 있어야 윈도우 메시지를 처리할 수 있습니다.
: : 그런데, VCL의 윈도우 클래스는 C++ 클래스로서 윈도우 프러시저는 그 멤버 함수로 되어있습니다.
: : 이를 운영체제가 요구하는 실질적인 윈도우 프러시저로 등록하기 위해서는 별도의 조치가 있어야 합니다.
: : 이 조치가 MakeObjectInstance입니다.
: :
: : 1. MakeObjectInstance 함수
: : TObjectInstance 인스턴스 하나가 VCL 윈도우 클래스 인스턴스 하나를 처리합니다.
: : TInstanceBlock 인스턴스 하나에는 TObjectInstance 인스턴스가 314개 포함되어 있습니다.
: : ( PageSize-offsetof(TInstanceBlock,Instances)) div sizeof(TObjectInstance)
: : = (4096-10) div 13 = 314 )
: : TInstanceBlock 인스턴스가 하나도 없거나 현재 사용 중인 인스턴스(InstBlockList) 안의 TObjectInstance 인스턴스 314 개가 모두 사용되었으면 InstFreeList가 nil이 되며 이 때 또 하나의 TInstanceBlock 인스턴스를 만들고 InstBlockList에 그 번지(포인터)를 저장해둡니다.
: : TObjectInstance 인스턴스 314 개는 연결리스트로 연결해두는데 뒤쪽부터 앞쪽 순서로 사용하도록 연결해둡니다.
: : TInstanceBlock의 인스턴스들도 연결리스트로 연결해둡니다.
: : InstBlockList에 포함된 TObjectInstance 인스턴스 314 개 중 다음 번에 사용할 인스턴스의 번지를 InstFreeList에 저장해둡니다.
: :
: : TObjectInstance 인스턴스의 Code 멤버는 실행파일에서의 실행코드로서 함수 호출 코드입니다.
: : TObjectInstance 인스턴스의 Offset 멤버는 어떤 함수를 호출할 것인지 그 함수의 위치값으로서
: : 이 인스턴스를 포함하고 있는 TInstanceBlock 인스턴스의 Code를 호출하도록 설정됩니다.
: : TInstanceBlock 인스턴스의 Code는 스택에 저장된 값 하나를 꺼내서 ECX 레지스터에 저장하는 명령과
: : StdWndProc 함수로 실행점을 옮기는 JMP 명령으로 구성되어 있습니다.
: : 여기서, 스택에 저장된 값은 TObjectInstance 인스턴스의 Code(함수호출) 실행 시 스택에 자동으로 저장되는 값으로서
: : 함수 호출 후 되돌아왔을 때 실행될 코드의 위치값입니다.
: : 이 위치는 TObjectInstance 인스턴스의 Method 위치입니다.
: : TInstanceBlock 인스턴스의 Code에는 스택에 저장된 값 하나를 꺼내는 명령이 있으므로 위 위치로 되돌아갈 수 없게 됩니다.
: : 스택에 저장된 값 하나를 꺼내고 나서 StdWndProc 함수로 실행점이 옮겨집니다(JMP).
: :
: : 2. StdWndProc 함수
: : 운영체제가 윈도우 프러시저를 호출할 때는 먼저 그 매개변수들을 스택에 저장하며 호출 시 복귀위치가 자동으로 스택에 저장된 후 호출됩니다.
: : StdWndProc 함수의 매개변수들은 윈도우 프러시저로 등록된 TObjectInstance 인스턴스의 Code를 운영체제가 호출하기 직전에 스택에 저장해둔 것들입니다.
: : StdWndProc 함수 내의 PUSH 명령 4 개는 TWndMethod의 매개변수인 TMessage 형의 값을 스택에 만드는데 그 원천은 운영체제가 스택을 통하여 넘겨준 것입니다.
: : (매개변수 전달 방식 중 값에 의한 전달의 경우 스택에 그 값을 저장하여 전달함)
: : PUSH 반대 순서대로 TMessage의 Msg,WParam,LParam,Result가 됩니다.
: : MOV EAX,[ECX].Longint[4]
: : CALL [ECX].Pointer
: : StdWndProc 함수에서는 MakeObjectInstance 함수를 통하여 TObjectInstance 인스턴스의 Method에
: : 저장해둔 __closure(VCL 윈도우 클래스 인스턴스 포인터와 해당 멤버 함수의 주소가 함께 저장됨) 값을 가지고
: : 해당하는 VCL 윈도우 클래스 인스턴스에 대해 윈도우 프러시저를 호출합니다.
: : 위 MOVE 명령으로 EAX 레지스터에 저장되는 값이, 해당하는 VCL 윈도우 클래스 인스턴스의 윈도우 프러시저 내에서, this 포인터가 됩니다.
: : TObjectInstance 인스턴스의 Method 위치는 위에서 말한대로 ECX 레지스터에 저장되어 있습니다.
: : 마지막 명령(POP EAX)으로 StdWndProc 함수의 반환값은 EAX 레지스터에 저장되며
: : 마지막 두 명령으로 이 함수에서 사용한 스택 공간도 반환됩니다.
: : 함수 실행이 종료되면 TObjectInstance 인스턴스의 Method 위치로 return 하지 않고 바로 운영체제로 return 합니다.
: : (위에서 말한대로 스택에 저장된 복귀 위치 하나를 꺼내버렸기 때문에 현재 스택의 top에 저장된 것은 운영체제 쪽 복귀 위치임)
: :
: :
: : 첨기:
: : 아래와 같이 윈도우 인스턴스와 클래스 인스턴스를 연관시킬 수 있습니다.
: : 이 방식은 모든 메시지 처리시 해당 윈도우 클래스(WNDCLASS)의 전역(static) 윈도우 프러시저가 호출되며 매번
: : GetWindowLong 함수를 호출하여 클래스 인스턴스 포인터(this 포인터)를 가져와서 사용합니다.
: : 한편, MakeObjectInstance에서와 같이 하면 GetWindowLong 함수 호출 없이 this 포인터를 얻을 수 있고
: : 윈도우 프러시저가 static일 필요가 없으므로 윈도우 프러시저 내에서 바로 클래스의 멤버에 액세스할 수 있습니다.
: :
: : class SomeClass
: : {
: : public:
: :
: : private:
: : HWND m_hWnd;
: : static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam);
: : LRESULT OnPaint(void);
: : LRESULT OnCommand(WPARAM wParam, LPARAM lParam);
: : LRESULT OnCreate();
: : LRESULT OnDestroy();
: : .........
: : };
: :
: : int WINAPI WinMain(
: : HINSTANCE hInstance, // handle to current instance
: : HINSTANCE hPrevInstance, // handle to previous instance
: : LPSTR lpCmdLine, // pointer to command line
: : int nCmdShow // show state of window
: : )
: : {
: : SomeClass *pclass = new SomeClass();
: : WNDCLASS wc;
: : wc.lpfnWndProc = (WNDPROC)SomeClass::WndProc;
: : RegisterClass(&wc);
: : CreateWindow(,,,,,,,,,,pclass);
: : .......
: : }
: :
: : LRESULT CALLBACK SomeClass::WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
: : {
: : SomeClass *pThis = (SomeClass *)GetWindowLong(hWnd, GWL_USERDATA);
: : switch (uMessage)
: : {
: : case WM_NCCREATE:
: : LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lParam;
: : pThis = (SomeClass *)(lpcs->lpCreateParams);
: : SetWindowLong(hWnd, GWL_USERDATA, (LONG)pThis);
: : pThis->m_hWnd = hWnd;
: : break;
: : case WM_CREATE :
: : return pThis->OnCreate();
: : case .......
: : }
: : }
C++로 바꿔봤습니다.
이해에 도움이 되실지 모르겠습니다.
const InstanceCount = 313;
//{ Object instance management }
#pragma pack(push,1)
typedef struct TObjectInstance *PObjectInstance;
struct TObjectInstance
{
unsigned char Code;
int Offset;
union{
PObjectInstance Next;
TWndMethod Method;
};
};
typedef struct TInstanceBlock *PInstanceBlock;
struct TInstanceBlock
{
PInstanceBlock Next;
unsigned char Code[2];
void *WndProcPtr;
TObjectInstance Instances[InstanceCount+1];
};
#pragma pack(pop)
PInstanceBlock InstBlockList; // NULL로 초기화됨
PObjectInstance InstFreeList; // NULL로 초기화됨
/*
{ Standard window procedure }
{ In ECX = Address of method pointer }
{ Out EAX = Result }
*/
int _stdcall StdWndProc(HWND Window, int Message, int WParam, int LParam)
{
_asm
{
XOR EAX,EAX //EAX 레지스터의 값을 0으로 만듦
PUSH EAX //EAX 레지스터의 값 0을 스택에 넣음
PUSH LParam //매개변수 LParam을 스택에 넣음
PUSH WParam //매개변수 WParam을 스택에 넣음
PUSH Message //매개변수 Message를 스택에 넣음
// 위 PUSH 4개는 TMessage 구조체 인스턴스를 스택에 만들어 아래 Method 함수 호출 매개변수로 넘겨주는 것임
MOV EDX,ESP //ESP 레지스터의 값(스택의 top 위치값)을 EDX 레지스터에 저장.
MOV EAX, DWORD PTR [ECX+4] /* ECX 레지스터의 값을 번지로 보고 그 번지 더하기 4바이트 위치에 있는 DWORD 값을 EAX 레지스터에 복사.
이 위치에는 Method의 this 포인터가 들어있음.
즉, this 포인터를 EAX 레지스터를 통하여 전달하는 것임 */
CALL DWORD PTR [ECX] /* DWORD PTR [ECX] : ECX 레지스터의 값을 번지로 보고 그 번지에 저장된 DWORD 값을 구함.
이 위치에는 Method 함수의 번지가 들어있음. 즉 Method를 호출하는 것임 */
ADD ESP,12 //이 함수에서 사용한 스택 공간 12 바이트를 반환
POP EAX //Method 함수 호출결과 스택을 통하여 반환받은 값을 EAX 레지스터에 저장하여 이 함수의 반환 값으로 사용
}
// 이 함수를 컴파일하면 위 어셈블리어 외에 추가로 코드가 생성됨.
// 이에는 RET도 있음.
}
//{ Allocate an object instance }
int CalcJmpOffset(void* Src, void* Dest)
{
return ( (int)Dest - ((int)Src + 5) );
}
const PageSize = 4096;
void* MakeObjectInstance(TWndMethod Method)
{
const unsigned char BlockCode[2] = {
0x59, //{ POP ECX }
0xE9 //{ JMP StdWndProc }
};
PInstanceBlock Block;
PObjectInstance Instance;
if(NULL==InstFreeList)// 사용 가능한(사용중이지 않은) TObjectInstance 인스턴스가 없는 경우
{
Block = (PInstanceBlock)VirtualAlloc(NULL, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //이번에 사용할 TPInstanceBlock 인스턴스 만들기
Block->Next = InstBlockList; //이번에 사용할 TPInstanceBlock 인스턴스의 Next를 직전에 사용한 TPInstanceBlock 인스턴스 주소로 설정
//아래 두 문장은 스택의 4바이트 값을 ECX 레지스터에 꺼낸 후(Code[0])
//StdWndProc 함수로 실행점을 옮기도록(Code[1],WndProcPtr) 설정하는 것임
Move(BlockCode, Block->Code, sizeof(BlockCode)); // 코드 복사
Block->WndProcPtr = (void*)CalcJmpOffset(&Block->Code[1], StdWndProc); // StdWndProc 함수 까지의 거리 저장
Instance = Block->Instances; //TObjectInstance 인스턴스 배열 내의 첫번째 인스턴스의 주소
//현재 사용중인 TPInstanceBlock 인스턴스 내의 모든 TObjectInstance 인스턴스의 기본 값을 설정
do{
//아래 두 문장은 TPInstanceBlock 인스턴스의 Code를 함수로 보고 호출하도록 설정하는 것임
Instance->Code = 0xE8; //{ CALL NEAR PTR Offset } // Offset 멤버 변수의 값 위치에 있는 코드를 함수로 보고 호출하는 코드
Instance->Offset = CalcJmpOffset(Instance, Block->Code); // 각 인스턴스 마다 Block->Code 까지의 거리가 다르므로 각기 계산
Instance->Next = InstFreeList; // 현재 인스턴스의 Next를 직전 인스턴스 주소로 설정. 즉, 뒤에서 앞 방향으로 연결되고 그 순서로 사용될 것임.
InstFreeList = Instance; // 현재 인스턴스 주소를 기억.
Instance++; // 다음 라운드에 사용할 배열 내의 직후 인덱스의 인스턴스 주소
}while((int)Instance - (int)Block < sizeof(TInstanceBlock));
InstBlockList = Block; // 현재 사용중인 TPInstanceBlock 인스턴스를 기억
}
Instance = InstFreeList; // 현재 사용중인 TObjectInstance 인스턴스의 주소를 기억
InstFreeList = Instance->Next; // 다음 번에 사용할 TObjectInstance 인스턴스의 주소를 기억
Instance->Method = Method; //Method는 __closure 형인데 상위 4바이트에는 클래스 인스턴스의 주소, 하위 4바이트에는 함수의 주소가 저장됨
return Instance; //현재 사용중인 TObjectInstance 인스턴스의 주소를 반환. Instance가 가리키는 곳 선두에는 위에서 설정한 대로 실행코드가 있음
}
//{ Free an object instance }
void FreeObjectInstance(void *ObjectInstance)
{
if(NULL!=ObjectInstance)
{
// 전달받은 ObjectInstance를 다음 번에 사용할 TObjectInstance 인스턴스로 연결리스트에 끼워넣음
((PObjectInstance)ObjectInstance)->Next = InstFreeList;
InstFreeList = (PObjectInstance)ObjectInstance;
}
}
|