메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

Win32 실행파일(PE)의 구조 - 1

한빛미디어

|

2005-05-30

|

by HANBIT

21,370

출처: Windows 시스템 실행파일의 구조와 원리(이호동 저) Chapter 1



우리가 컴퓨터를 기동해서 목적하는 작업을 하기 위해서는 언제나 특정 프로그램을 구동한다. 워드 작업을 위해서 MS-WORD나 아래아 한글을 띄운다든지, 개발을 위해 특정 개발 툴을 실행한다든지, 해당 작업의 데이터 파일을 찾아가거나 메일 확인을 위해 인터넷 익스플로러도 띄울 것이고, 작업하는 동안 미디어 플레이어를 실행시켜 음악도 들을 것이다.

우리가 하나의 작업을 하는 동안 멀티태스킹 운영체제 덕분에 동시에 여러 개의 프로그램이 부지불식간에 메모리 상에 로드되어 돌고 있다. 사실 여러분이 식별할 수 있는 이상의 프로그램이 작동 중에 있다. 여러분들이 직접 띄운 프로그램뿐만 아니라 윈도우가 기동되면서 자동으로 실행되어 트레이 바에 올라가 있는, 예를 들어 메신저 같은 그런 프로그램과 눈에 보이지 않는 데몬 프로그램들, 윈도우에선 서비스라고 하는 소위 백그라운드 프로세스가 NT 계열 OS라면 아마 적어도 10개 이상은 숨어서 실행되고 있을 것이다.

이렇게 실행되는 프로그램들은 흔히 하드디스크 상에 EXE라는 확장자를 가진 파일로 존재한다. 또한 이러한 실행 프로그램 또는 응용 애플리케이션이라고 불리는 EXE 파일 말고도 프로그램 실행을 위한 동적 링크 라이브러리라는 DLL 파일도 프로그램 실행 시에 같이 물려 메모리 상에 로드된다. 이러한 EXE 파일과 관련 DLL 파일들이 메모리 상에 로드되면서 비로소 프로그램이라는 것이 사용 가능하게 되고 이렇게 로드된 하나의 EXE와 여러 개의 관련 DLL들이 소위 운영체제론에서 이야기하는 하나의 프로세스(Process)를 구성하게 된다.

그러면 여러분들은 이러한 소위 프로그램이라는 것이 어떻게 실행되는가에 대한 의문을 가져본 적은 없는가? 특히 시스템 프로그래밍에 관심이 많은 독자라면 한번쯤은 여기에 대한 궁금증은 가져보았으리라 생각한다. 동시에 이런 실행 파일들의 구조는 어떻게 생겼을까? 라는 의문과 함께...

그리고 EXE 파일을 최소한 실수로라도 한번쯤은 헥사 에디터로 열어본 경험이 있을 것이다. 그런 적이 없다면 지금 한번 열어보기 바란다. 그렇게 열린 EXE 파일은 알아보지 못할 암호 같은 상당히 어지러운 문자들로 나열되어 우리 앞에 펼쳐질 것이다. 그래도 다행히 파일의 시작엔 항상 “MZ”이라는 식별 가능한 문자가 나올 것이다. 그리고 그 뒤의 이상한 문자들이란... 에구... 빨리 닫자. 어지럽다...

흔히 우리가 응용 프로그램이라고 부르는 EXE 파일 역시 나름대로의 포맷을 가진다. EXE 파일을 로드하는 역할을 담당하는 것이 Win32 운영체제의 서브시스템으로 존재하는 프로그램 로더이고 EXE의 파일 구조는 이 로더가 식별할 수 있는 방식으로 구성된다.

여러분이 특정 EXE를 실행하게 되면 이 로더가 해당 EXE 파일을 열고 그 파일 구조를 분석해서 적절하게 메모리에 로드하여 프로그램의 진입점으로 들어가게 한다. 또한 프로그램을 로드하는 동안 EXE 파일 내부의 임포트 정보를 통해 필요한 DLL 역시 찾아서 메모리 상에 로드하게 된다.

아무 DLL이나 하나 선택해서 헥사 에디터로 열어보라. 재미있는 것은 DLL 역시 “MZ”으로 시작함을 알 수 있을 것이다. 그러면 EXE와 DLL의 파일 구조가 어찌 같은 구조가 아닐까라고 예상할 수 있다. 구조가 같을까? 미리 대답하자면 EXE와 DLL은 같은 파일 구조를 지닌다. 마이크로소프트는 이러한 EXE와 DLL 등의 파일 구조를 PE 파일 포맷이라고 명명했다.

이때 PE는 Portable Executable의 약자로서 PE 구조로 된 PE 파일들은 플랫폼에 관계없이 Win32 운영체제가 돌아가는 시스템이면 어디서든 실행 가능하다는 의미에서 Portable Executable이라는 이름을 붙였다. 즉, 인텔 프로세서(CPU) 기반의 윈도우가 탑재된 시스템에서 돌아가는 PE 프로그램은 DEC-ALPHA 프로세서를 탑재한 시스템 상에 인스톨된 Win32 운영체제 하에서도 실행 가능하다는 것이다.

본 글에서 심도 깊게 다루고자 하는 것이 바로 이 PE 파일 구조이다. PE 파일 구조를 분석한다는 것은 단순히 그 파일 구조 자체에 국한된 것만은 아니다. PE 파일 구조를 통해 Win32 운영체제의 깊은 곳까지 파고 들어갈 수 있다. 만약 Win32 시스템의 여러 중요한 부분을 이해하지 못한 채 PE 파일 구조를 본다면 아마 제대로 이해하기 힘들 것이다.

하지만 반대로 필자가 이 글에서 제안하는 것이 이 PE 파일 구조를 통해 거꾸로 Win32의 심오한 부분까지 살펴볼 수 있는 계기를 만들자는 것이다. 한 예로 PE 파일과 메모리 매핑 파일 간의 관계를 간단히 살펴보자. 우리가 지금까지 이야기해온 실행 파일이니 DLL 파일 또는 PE 파일 구조니 해서 계속 파일이라고 명명하고 있다.

하지만 엄밀히 말하면 그냥 단순히 파일이 아니다. 이 글을 통해서 우리는 Win32 Platform SDK의 “WinNT.H” 헤더 파일을 자주 참조할 예정인데 이 “WinNT.H” 헤더 파일 내의 PE 관련 구조체들은 파일이라는 명칭 대신 이미지(Image)라는 명칭을 사용한다. 파일이 아니고 왜 이미지일까? 이때 말하는 이미지는 JPG나 BMP, GIF 등으로 표현되는 그림, 사진 등의 의미가 아니고 Image라는 단어의 어원적 의미이다. 즉, 어떤 실체에 대한 그림자, 허상, 그것의 반영이라는 의미에서의 이미지이다.

그럼 무엇에 대한 그림자이고 무엇을 반영한 것이란 말인가? 사실 PE 파일은 하드디스크에 파일로 존재하지만 그것이 실행되기 위해 메모리로 로드될 때 일반 데이터 파일이 메모리에 로드되는 방식과는 전혀 다른 방식으로 로드된다. 한 프로세스에 할당되는 4기가바이트의 가상 주소 공간을 유지하기 위해 가상 메모리 관리자(Virtual Memory Manager, 이하 VMM)는 페이지 파일이라는 덩치 큰 스와핑 영역을 하드디스크에 유지하며 이 페이지 파일과의 적절한 스와핑, 매핑을 통해 프로세스에게 마치 4기가의 가상 공간을 실제로 제공해 주는 것처럼 그렇게 프로세스를 속이고 있는 것이다. 프로세스는 그렇게 VMM이 만들어준 매트릭스 내에 살고 있는 것이다.

하지만 PE 파일이 로드될 때 VMM은 페이지 파일을 사용하지 않고 PE 파일 자체를 마치 페이지 파일처럼 가상 주소 공간에 그대로 매핑한다. Win32 프로그래머들이 프로세스간 통신을 위해 애용하는 “공유 메모리” 메커니즘을 통해 PE 파일 자체를 직접 가상 주소 공간과 매핑해 버리는 것이다. “공유 메모리”라고 알려진 IPC 통신 방식은 실제 메모리에 매핑된 파일(Memory Mapped File)이며 어찌 보면 원래 PE 파일을 효율적으로 메모리에 로드하기 위해 MS가 고안해낸 방식을 그 범위를 넓혀 IPC에도 이용할 수 있도록 다른 사용 용도를 제안한 것일 수도 있다.

이미지라는 것은 바로 메모리에 매핑된 하드디스크 상의 PE 파일 자체인 것이다. 실제는 가상 주소 공간에 로드된 PE이고 그것의 반영이 PE 파일로 존재하는 것이다. 이미지를 좀 더 명확하게 이해하기 위해 미리 이야기하자면 메모리 상에 로드된 PE 포맷이나 하드디스크 상에서의 파일로 존재하는 PE 포맷이나 거의 같다고 보면 된다. 이제 PE 파일 포맷 분석을 통해 우리는 “공유 메모리” 메커니즘을 공부할 수 있고 더 나아가서 VMM의 Win32 가상 주소 공간 관리 방식을 제대로 공부해야만 PE와 메모리의 관계를 제대로 이해할 수 있는 것이다.

PE 파일을 분석하기 위해 우선 간단한 EXE 프로그램을 하나 만들어보자. 분석을 위한 샘플을 간결하게 하기 위해서 MFC 같은 것을 사용하지 말고 C 코드로 된 네이티브 Win32 프로그램을 아래 소스와 같이 만들었다. 아마 윈도우 프로그래밍은 반드시 MFC를 사용해야 한다고 잘못 알고 있는 사람들이 많은데 원래 윈도우 표준 프로그래밍은 C로 구현되는 것이다. Win32 API 전체가 C로 제공되며 윈도우 애플리케이션 작성 뼈대 자체도 C로 되어 있다. 다음 소스는 BasicApp.C라는 아주 간단한 윈도우 응용 프로그램의 C 코드이다.

[소스 1-1] BasicApp.C

/*********************************************************************************
 * 파  일 : BasicApp.C
 * 종  류 : C 본체
 * 정  의 : WinMain - Win32 Entry Point.
 *        WndProc - Windows Procedure.
 * 내  용 : 가장 원초적인 윈도우즈 GUI 애플리케이션의 정의
 * ---------------------------------------------------------------
 * 저  자 : Yi HoDong
 * 날  짜 : 2001.11.06
 * 버  전 : 1.0
 * 회  사 : YHD Works Co. (http://www.yhdworks.com)
 ********************************************************************************/
#include 

TCHAR g_szAppName[] = “Basic GUI Application";

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/*********************************************************************************
 * 종  류 : WIN32 엔트리 포인트 함수
 * 소  속 : 없음
 * 이  름 : WinMain
 * 인  수 : HINSTANCE hInstance [IN] : 본 프로세스의 인스턴스 핸들(시작 번지)
 *        HINSTANCE hPrevInst [IN] : 언제나 0, 무시(16비트와의 호환을 위해)
 *        PSTR      szCmdLine [IN] : 커맨드 라인 파라미터
 *        int       iCmdShow  [IN] : 윈도우가 보여지는 형태
 * 반  환 : int : 
 * 내  용 : WIN32 엔트리 포인트 함수
 *        콘솔 프로그램(main)과는 다르게 GUI에서는 WinMain을 엔트리 포인트
 *        함수로 인식한다.
 * ---------------------------------------------------------------
 * 저  자 : Yi HoDong
 * 날  짜 : 2001.08.06
 * 버  전 : 1.0
 * 회  사 : YHD Works Co. (http://www.yhdworks.com)
 ********************************************************************************/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
                    PSTR szCmdLine, int iCmdShow)
{
      HWND hWnd;
      MSG msg;
      WNDCLASS wndclass;

       /////////////////////////////////////////////////////////////////////////////////////////////
       // 윈도우즈 클래스 등록
       /////////////////////////////////////////////////////////////////////////////////////////////
      wndclass.style = CS_HREDRAW|CS_VREDRAW;
      wndclass.lpfnWndProc = WndProc;
      wndclass.cbClsExtra = 0;
      wndclass.cbWndExtra = 0;
      wndclass.hInstance = hInstance;
      wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
      wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
      wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
      wndclass.lpszMenuName = NULL;
      wndclass.lpszClassName = g_szAppName;
      if(!RegisterClass(&wndclass))
      {
                    MessageBox(NULL, "Window Class Registeration Failure!!!",
                                  g_szAppName, MB_ICONERROR);
                    return 0 ;
      }
      /////////////////////////////////////////////////////////////////////////////////////////////

      /////////////////////////////////////////////////////////////////////////////////////////////
      // 윈도우즈 생성 & 갱신
      /////////////////////////////////////////////////////////////////////////////////////////////
      hWnd = CreateWindow(g_szAppName, // window class name
                                 g_szAppName, // window caption
                                 WS_OVERLAPPEDWINDOW, // window style
                                 CW_USEDEFAULT, // initial x position
                                 CW_USEDEFAULT, // initial y position
                                 400, // initial x size
                                 150, // initial y size
                                 NULL, // parent window handle
                                 NULL, // window menu handle
                                 hInstance, // program instance handle
                                 NULL); // creation parameters
      if(!hWnd)
      {
                    MessageBox(NULL, Window Creation Failure!!!",
                                 g_szAppName, MB_ICONERROR) ;
                    return 0 ;
      }
      ShowWindow(hWnd, iCmdShow);
      UpdateWindow(hWnd);
      /////////////////////////////////////////////////////////////////////////////////////////////

      /////////////////////////////////////////////////////////////////////////////////////////////
      // 메시지 루프
      /////////////////////////////////////////////////////////////////////////////////////////////
      while(GetMessage(&msg, NULL, 0, 0))

      {
               TranslateMessage(&msg);
               DispatchMessage(&msg);
      }
      /////////////////////////////////////////////////////////////////////////////////////////////

      return msg.wParam;
}
//////////////////////////////////////////////////////////////////////////////////////////////////


/*********************************************************************************
 * 종  류 : 윈도우즈 프로시저 함수(콜백 함수)
 * 소  속 : 없음
 * 이  름 : WndProc
 * 인  수 : HWND hWnd [IN] : 메시지를 받을 윈도우 핸들
 *        UINT   uMsg [IN] : 메시지
 *        WPARAM wParam [IN] : 인자1
 *        LPARAM lParam [IN] : 인자2
 * 반  환 : LRESULT : 
 * 내  용 : 윈도우즈 프로시저
 *        WINDOWCLASS의 lpfnWndProc 필드에 이 함수의 번지를 등록해야
 *        윈도우의 동작을 이 함수 내에서 컨트롤한다.
 * ---------------------------------------------------------------
 * 저  자 : Yi HoDong
 * 날  짜 : 2001.08.06
 * 버  전 : 1.0
 * 회  사 : YHD Works Co. (http://www.yhdworks.com)
 ********************************************************************************/
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
      static char szOutBuf[] = "The Most Simple Windows GUI Program by YHD.";
      HDC          hDC;
      PAINTSTRUCT  ps;
      RECT         rc;

      switch(uMsg)
      {
           case WM_PAINT:
                   hDC = BeginPaint(hWnd, &ps);
                   GetClientRect(hWnd, &rc);
                   DrawText(hDC, 
                               szOutBuf, 
                               lstrlen(szOutBuf), 
                               &rc, 
                               DT_SINGLELINE|DT_CENTER|DT_VCENTER);
                   EndPaint (hWnd, &ps) ;
            return 0;

            case WM_DESTROY:
                  PostQuitMessage(0);
            return 0;
     }

     return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
//////////////////////////////////////////////////////////////////////////////////////////////////


위 소스는 윈도우 애플리케이션 프로그래밍의 전형적인 패턴이다. 위 소스를 컴파일한 후 실행하게 되면 다음 그림과 같은 아주 단순한 형태의 윈도우가 나타날 것이다.




[그림 1-1] BasicApp.exe의 실행


위 소스를 디버그 모드와 릴리즈 모드 둘 다 컴파일하기 바란다. 디버그 모드와 릴리즈 모드일 때 PE 구조의 내용이 다소 다르기 때문에 둘을 비교해 보는 것도 이해에 도움이 된다. 컴파일은 비주얼 스튜디오에서 프로젝트 파일을 만들어 컴파일을 하든지 아니면 커맨드 라인 명령을 이용하면 된다.

다음 그림은 컴파일과 링크의 결과물인 BasicApp.exe의 릴리즈 판을 헥사 에디터로 덤프한 결과의 처음 부분이다. 파일 시작 부분의 반전된 부분을 보면 헥사 값으로 0x4D, 0x5A로 시작하며 그것의 아스키 값이 “MZ”임을 알 수 있다. PE 파일은 이렇게 전부 “MZ”으로 시작한다.




[그림 1-2] PE 파일 덤프


보기에는 상당히 복잡해 보이는 암호 코드 같은 바이너리 덤프이지만 정해진 포맷을 가지고 있다. 이것을 PE 포맷이라고 하자. 이 책에서 다루는 내용은 결국 위 그림의 바이너리 덤프의 구조를 자세히 살펴보는 것이다. 다음 장부터 차근차근 위 덤프의 구조를 상세히 파고들어갈 것이다.
TAG :
댓글 입력
자료실

최근 본 책0