본문 바로가기

Programming/Windows System

[Windows System] Win32 실행파일(PE)의 구조

지금부터 우리가 흔히 실행 파일 또는 응용 애플리케이션이라고 부르는 EXE파일과 EXE 실행 시에 함께 로드되는 동적 라이브러리 파일인 DLL의 구조와 포맷에 대하여 상세하게 알아본다. 마이크로소프트는 이러한 EXE나 DLL 파일을 Win32 운영체제가 탑재된 플랫폼이라면 어떤 플랫폼에도 관계없이 실행된다는 의미에서 PE(Portable Executable)라고 부른다.따라서 마이크로소프트에서 Excutable이라고 할 때 이것은 EXE 파일뿐만 아니라 DLL까지 포함하는 의미이다. 즉, 인텔 프로세서(CPU) 기반의 윈도우가 탑재된 시스템에서 돌아가는 PE 프로그램은 DEC_ALPHA 프로세서를 탑재한 시스템 상에 인스톨된 Win32 운영체제 하에서도 실행 가능하다는 것이다.

사실 사용자가 식별할 수 있는 이상의 프로그램이 작동 중에 있따. 사용자들이 직접 띄운 프로그램뿐만아니라 윈도우가 기동되면서 자동으로 실행되어 트레이 바에 올라가 있는, 예를 들어 메신저 같은 그런 프로그램과 눈에 보이지 않는 데몬 프로그램들, 윈도우에선 서비스라고 하는 소위 백그라운드 프로세스가 NT 계열 OS라면 아마 적어도 10개 이상은 숨어서 실행되고 있을 것이다.

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

Win32 시스템의 여러 중요한 부분을 이해하지 못한 채 PE 파일 구조를 본다면 아마 제대로 이해하기 힘들 것이다. 하지만 반대로 이 PE 파일 구조를 통해 거꾸로 Win32의 심오한 부분까지 살펴볼 수 있는 계기가 될 수 있다. 한 예로 PE 파일과 메모리 매핑 파일 간의 관계를 간단히 살펴보자. 지금까지 이야기해온 실행 파일이니 DLL 파일 또는 PE 파일 구조니 해서 계속 파일이라고 명명하고 있다. 하지만 엄밀히 말하면 그냥 단순히 파일이 아니다. 이 글을 통해서 Win32 Platform SDK의 "WinNT.h" 헤더 파일을 자주 참조할 예정인데 이 "WinNT.h" 헤더 파일 내의 PE 관련 구조체들은 파일이라는 명칭 대신 이미지(Image)라는 명칭을 사용한다. 이때 말하는 Image라는 단어는 어원적 의미이다. 즉, 어떤 실체에 대한 그림자, 허상, 그것의 반영이라는 의미에서의 이미지이다. 그럼 무엇에 대한 그림자이고 무엇에 반영한 것이란 말인가? 사실 PE 파일은 하드디스크에 파일로 존재하지만 그것이 실행되기 위해 메모리로 로드될 때 일반 데이터 파일이 메모리에 로드되는 방식과는 전혀 다른 방식으로 로드된다. 한 프로세스에 할당되는 4기가바이트의 가상 주소 공간을 유지하기 위해 가상 메모리 관리자(Vitual Memory Manager;VMM)페이지 파일이라는 덩치 큰 스와핑 영역을 하드디스크에 유지하며 이 페이지 파일과의 적절한 스와핑, 매핑을 통해 프로세스에게 마치 4기가의 가상 공간을 실제로 제공해 주는 것처럼 그렇게 프로세스를 속이고 있는 것이다.

윈도우즈 기반 프로그램을 컴파일 하는 과정에서 디버그 모드와 릴리즈 모드 각각의 PE 구조의 내용은 다소 다르다. 프로그램의 PE구조를 살펴보면 헥사 값으로 0x4D, 0x5A로 시작하며 아스키 값이 "MZ"임을 알 수 있다. PE 파일은 이렇게 전부 "MZ"으로 시작한다.

PE 파일의 전체 구조



IMAGE_XXX_XXXX들은 전부 WinNT.h 헤더 파일에 정의된 구조체들이다. PE 파일은 처음에 MZ으로 시작하는 IMAGE_DOS_HEADER 구조체로 시작해서 그 다음에 도스 스텁을 담고 있다. 그 후에 실제 PE 포맷이 시작된다.

PE 포맷의 시작은 IMAGE_NT_HEADER라는 구조체의 헤더로 표현된다. IMAGE_NT_HEADER는 "PE"로 표현되는 시그니처를 시작으로 해서 차례대로 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER 구조체로 구성된다. IMAGE_FILE_HEADER는 PE 파일의 파일 정보를 나타내고 IMAGE_OPTIONAL_HEADER는 전혀 선택적이지 않은 PE 파일이 메모리에 로드될 때 필요한 모든 정보들을 담고 있다. 이 OPTIONAL_HEADER 내부에는 기본 필드들과 함께 주요 섹션들과 정보들의 위치와 크기를 나타내는 IMAGE_DATA_DIRECTORY 구조체의 배열들을 담고 있다. 이 IMAGE_OPTIONAL_HEADER 구조체 다음에는 섹션 테이블이라고 부르는 IMAGE_SECTION_HEADER 구조체의 배열이 펼쳐진다.

여기까지가 PE 파일의 헤더에 해당하고 이 헤더 다음부터 실제 내용들, 즉 코드나 데이터들은 그 성격에 따라서 각각 섹션이라고 부르는 블록별로 저장된다. 이 블록의 위치와 크기 그리고 속성을 담고 있는 것이 IMAGE_SECTION_HEADER이다.

각 섹션은 그 종류에 따라 자체의 포맷을 가진다. 대표적으로 실행 코드를 담고 있는 .text 섹션, 전역 데이터를 담고 있는 .data 섹션, 그리고 임포트 함수와 익스포트 함수에 대한 정보를 담고 있는 .idata, .edata 섹션, 다이얼로그나 아이콘, 메뉴 등의 리소스 데이터의 내용을 담고 있는 . rsrc 섹션 등이 있다.

PE 분석을 위한 개념 정리

- RVA(Relative Virtual Address)
: 먼저 말해둘 것은 PE 파일 내의 파일 오프셋과 RVA는 전혀 관련이 없다는 점이다. PE 파일 내의 번지에 관계된 값, 즉 가상 주소 공간 상의 번지 값들은 전부 RVA의 개념으로 표현된다. RVA는 이미지가 메모리에 , 더 정확히 표현하면 해당 프로세스의 가상 주소 공간 내에 로드되었을 때의 그 시작 주소에 대한 상대적 번지 개념으로 메모리 상에서의 PE의 시작 주소에 대한 오프셋으로 생각하면 된다.
각 섹션들은 섹션 헤더에서 지정하는 RVA를 참조하여 PE 이미지가 매핑된 시작 번지로부터 지정된 RVA에 정확하게 해당 섹션을 로드하게 된다. 따라서 해당 섹션의 PE 상의 파일 오프셋과 RVA는 같을 수도 있지만 대부분 다르게 나타난다.

- VAS(Virtual Address Space)와의 관계 : 프로그램이 실행되면 프로세스는 4GB의 가상주소공간을 할당 받는다. 이 4GB의 공간은 말 그대로 가상이다.  이 공간을 실제의 물리적인 기억 장치와 연결시켜 주는 것이 가상 메모리 관리자(VMM; Virtual Memory Manager)이다.
페이징 파일과 RAM 그리고 VAS는 VMM에 의해 관리되면서 프로세스에게는 마치 자기 혼자서 4GB의 선형 주소 공간을 가진 것처럼 착각하게 만드는 매트릭스를 제공한다. 프로세스에 속한 특정 스레드가 가상주소공간 내의 특정 번지를 액세스할 때 VMM은 해당 번지의 페이지를 페이징 파일과 매핑시켜 준다. 따라서 해당 가상 주소 공간에 무엇을 읽거나 쓰는 행위는 결국 해당 페이징 파일의 특정 페이지에 동일한 행위를 하는 것이 된다. 물론 이 페이징 파일 내에서 RAM과의 적절한 스와핑을 통해 실제로 물리적인 RAM에 액세스가 가능하도록 VMM은 보장해 준다.