2005. 9. 26. 11:26
PE 포맷 분석
이 문서는 msdn의 매트 피에트릭의 문서 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndebug/html/msdn_peeringpe.asp 를 번역한 것 입니다.
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
운영체제의 실행 파일의 포맷은 많은 부분에서 운영체제의 거울이라고 할 수 있다. 비록 실행 파일 포맷이 대부분의 프로그래머들이 공부해야 할 것중 높은 위치를 차지하지 않지만 중요한 지식을 이러한 방법을 통해서 모을 수 있다. 이 문서에서는 모든 Win32 기반의 시스템 -- Windows NT, Win32s, Windows 95 -- 에서 사용하기 위해서 디자인된 마이크로소프의 PE 파일 포맷에 관해서 알아볼 것이다. PE 파일 포맷은 Windows 2000을 포함한 가까운 장래에 나올 모든 마이크로소프트 운영체제에서 중요한 역할을 할 것이다. Win32s나 Windows NT를 사용하고 있다면, 벌써 PE 파일을 사용하고 있는 것이다. VC++을 사용해서 Windows 3.1만을 위한 프로그램을 작성한다고 할지라도 여전히 PE 파일(32비트 MS-DOS 확장)을 사용하고 있는 것이다. 짧게 말해서, PE는 벌써 보급되어 있으며 가까운 장래에 바뀌기는 힘들것이다. 지금이 이 새로운 실행 파일 타입이 운영체제 파티에 무엇을 가지고 왔는지 살펴볼 시간이다.
끝이 없는 헥사 덤프를 빤히 쳐다보고는 페이지 끝에 나오는 각각의 비트들의 중요성을 곱씹는 대신에 PE 파일 포맷이 포함하고 있는 켄셉과 매일 부딪히는 상황과 관련지어서 살펴보도록 하자. 예를들면 아래에 나오는 것 같은 쓰레드 지역 변수의 표시는
declspec(thread) int i;
실행 파일 내에서 우아하고 간결하게 어떻게 구현되는지 알기 전까지는 나를 미치게 만들었다. 많은 독자들이 16비트 Windows로 부터 넘어왔기 때문에, 나는 Win32 PE 파일 포맷의 구성요소들과 과거 16비트 NE 파일 포맷의 동등한 부분들을 연관지어서 설명할 것이다.
게다가 마이크로소프트사의 컴파일러와 어셈블러가 만들어 내는 새로운 오브젝트 모듈 포맷과의 차이점도 살펴볼 것이다. 이 새로운 OBJ 파일 포맷은 많은 부분에서 PE 파일과 유사하다. 나는 결국 새로운 OBJ 파일 포맷과 관련된 어떠한 문서도 없다는 것을 알게 되었다. 그래서 나는 스스로 그것들을 분석했고, 여기서 PE 포맷에 추가해서 일부를 설명할 것이다.
Windows NT가 VAX VMS와 UNIX로 부터 발전되었다는 것은 많이 알려진 사실이다. Windows NT을 작성한 많은 프로그래머들은 마이크로소프로 오기 전에 그러한 플랫폼에서의 코딩과 디자인을 했었다. Windows NT를 디자인 할때, 그들은 이전에 작성하고 테스트된 툴을 사용해서 부트스트랩 작성 시간을 줄이는 것은 자연스러운 것이었다. 이러한 툴들이 생성하고 같이 작업한 실행가능한 파일 포맷과 오브젝트 모듈 포맷은 COFF(Comm Object File Format의 머릿글자를 따서 지어졌다.)라 불렸다. COFF의 상대적인 나이는 8진수 형태로 필드를 구체화 시켰다는 점에서 알 수 있다. COFF 포맷은 그것 자체로는 좋은 시작 지점이었다. 그러나 Windows NT나 Windows 95와 같은 현대의 운영체제 시스템에서 요구하는 것을 충족시키기 위해서는 확장할 필요가 있었다. 이러한 업데이트의 결과가 PE 포맷이다. 그것은 "이식 가능하다고(prtable)" 불렸다. 왜냐하면 Windows NT의 모든 구현은 다양한 플랫폼에서 (x86, MIPS, Alpha, ...) 동일한 실행 파일 포맷을 사용하기 때문이다. 물론 CPU 명령어에따른 바이너리 인코딩의 차이는 있다. 중요한 점은 운영체제 로더와 프로그래밍 툴들은 각각의 새로운 CPU에 대해서 완전히 새롭게 작성할 필요가 없다는 점이다.
마이크로소프트 위원회가 윈도우 NT를 업그레이드 시키고 빠르게 동작시키기 위한 노력은 존재하는 32비트 툴과 파일 포맷을 포기했다는 점에서 찾을 수 있다. 16비트 윈도우의 가상 디바이스 드라이버는 윈도우 NT가 물망에 오르기 훨씬 전에 나온 다른 32비트 파일 구조 - LE 포맷 - 를 사용하고 있었다. 더 중요한 것은 OBJ 파일 포맷의 변화이다. 윈도우 NT 이전의 C 컴파일러, Win32환경에서 실행되는 모든 마이크로소프트 컴파일러는 COFF 포맷의 OBJ 파일을 생성한다. 볼랜드나 시만텍같은 몇몇 마이크로소프트 경쟁자들은 COFF 포맷을 보류하고 인텔의 OMF 포맷을 사용한다. 마침내 이것은 다양한 컴파일러에서 사용하기 위해서는 OBJ나 LIB 파일을 다른 컴파일러를 위한 별도의 버전을 제작해서 배포하는 수고가 필요가게된 것이다.
PE 포맷은 WINNT.H 헤더파일에 대충(성의없이) 문서화 되어있다. 대략 WINNT.H의 중간 부분 정도에 "Image Format"이라고 이름지어진 섹션이 있다. 이 섹션은 새로운 PE 정보로 옮겨가기전 오래되어 친숙한 MS-DOS MZ 포맷과 NE 포맷 헤더에서 나온 작은 tidbits로 시작한다. WINNT.H는 PE 파일에서 사용되는 가공되지않은 자료 구조들에 대한 정의를 제공한다. 그러나 단지 그 구조체들과 플래그들이 무엇을 의미하는지 이해하기 위한 아주 조금의 유용한 코멘트만 포함하고 있다. PE 포맷에 관한 헤더 파일을 작성했던 사람이 누구든지(Michael J. O'Leary가 떠오르고 있다), 그는 깊숙하게 중첩된 구조체와 매크로를 따라서 길고, 묘사적인 이름에 관한 신봉자임에 틀림없다. WINNT.H와 함께 코등힐때, 이러한 표현을 사용하는 것은 전혀 이상한 것이 아니다.
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
WINNT.H의 정보를 좀 더 쉽게 이해하기 위해서는 2001년 10월 이후에 배포된 MSDN 라이브러리 CD-ROM에서 구할 수 있는 Protable Executable and Common Object File Format 항목을 읽기를 권한다.
주제를 COFF 포맷 OBJ로 잠깐 돌려서, WINNT.H 헤더 파일은 COFF OBJ및 LIB 파일을 위한 typedefs와 구조체 정의를 포함하고 있다. 불행히도, 나는 위에 언급된 실행 파일 포맷과 마찬가지로 이것에 관한 어떠한 문서도 찾을 수 없었다. Since PE files and COFF OBJ files are so similar, I decided that it was time to bring these files out into the light and document them as well. PE파일과 COFF OBJ파일은 매우 유사하기 때문에 나는 이러한 파일들에 대한 가벼운 문서를 남기기기로 결정했다.???
PE파일이 무엇으로 구성되었는지에 관해서 알기보다는, 직접 이러한 컨셉을 보기위해서 몇몇 PE 파일들을 덤프하기를 원할 것이다. 만약 Win32 기반의 개발을 위해서 마이크로소프트 툴들을 사용하고 잇다면, DUMPBIN 프로그램이 PE 파일과 COFF OBJ/LIB 파일들을 분석해서 읽을수 있는 형태로 출력해준다. Of all the PE file dumpers, DUMPBIN is easily the most comprehensive. It even has a nifty option to disassemble the code sections in the file it's taking apart. 모든 PE 파일 덤프를 하는 프로그램 중에서, DUMPBIN은 가장 이해하기 쉽다. DUMPBIN은 자신이 분석하고 있는 파일의 코드 섹션을 디어셈블할 수 있는 멋진 옵션을 가지고 있다. 볼랜드 유저는 PE 실행 파일을 보기 위해서 TDUMP를 사용할 수 있다, 그러나 TDUMP는 COFF OBJ 파일들을 이해하지 못한다. 이것은 볼랜드 컴파일러가 COFF 포맷의 OBJ 파일을 만들지 않기 때문에 큰 문제가 되지 않는다.
나는 PE와 COFF OBJ파일을 덤프해주는 PEDUMP라는 프로그램을 만들었다.(Table 1을 보라) 나는 PEDUMP가 DUMPBIN에 비해서 더욱 이해하기 쉬운 출력을 제공한다고 생각한다. 비록 PEDUMP가 디어셈블러를 가지고 있지 않고 LIB 파일을 분석할 수 없지만, 다른 부분들은 DUMPBIN과 기능적으로는 동일하다. 그리고 생각해볼 가치가 있는 새로운 기능들도 추가되어있다. PEDUMP 소스코드는 MSJ 게시판에서 얻을 수 있기 때문에 전체 리스트를 여기에 보이지 않을 것이다. 대신에 내가 설명한 컨셉트들을 보여주기 위해서 PEDUMP의 샘플 출력을 보여줄 것이다.
Win32 와 PE의 기본적인 컨셉
PE 파일의 디자인의 골격을 이루고 있는 기초적인 아이디어들을 살펴보도록 하자. "모듈"이라는 용어는메모리에 로드된 실행 파일이나 DLL의 코드, 데이터 그리고 리소스를 나타낼때 사용할 것이다. 게다가 프로그램이 직접 사용하는 코드와 데이터뿐만 아니라 모듈은 코드와 데이터가 메모리 어느 위치에 있는지 결정하기 위해서 윈도우에서 사용하는 자료 구조로 구성된다. 16비트 윈도우에서는, 윈도우에서 사용하는 자료 구조가 모듈 데이터 베이스 내에 있었다(HMODULE에 의해 참조되는 세그먼트). Win32에서는 이러한 자료 구조들은 지금부터 간단히 설명할 PE 헤더내에 있다.
그림 1. PE 파일 포맷
PE 파일에 관해서 알아야할 첫번째로 가장 중요한 것은 디스크에 있는 실행 파일은 윈도우가 로드한 후의 모듈과 매우 유사하다는 점이다. 윈도우 로더는 디스크 파일로부터 프로세스를 생성하기 위해서 극도로 어려운 일들을 필요로 하지 않는다. 로더는 파일의 조각들을 가상 주소 공간으로 적절하게 매핑하기 위해서 메모리 맵 파일 메카니즘을 사용한다. 건축과 비교하자면 PE 파일을 조립식 주택에 비유할 수 있다. 조립식 주택으로 집을 지으려면 결국 각 부분을 한 번에 하나씩 집 지을 자리에 옮겨 놓은 다음 각 부분을 외부와 연결시키는 작업이 필요하다(PE에 DLL을 연결시키는 것처럼). 이러한 쉬운 로딩 방식은 PE 포맷 DLL에도 동일하게 적용된다. 모듈이 한번 로드되고 나면, 윈도우는 다른 메모리 맵 파일과 같이 효율적으로 다룰수 있게 된다.
이러한 로딩 방식은 16비트 윈도우에서의 상황과 완전히 대조적이다. 16비트 NE 파일 로더는 파일의 일부분을 읽고 메모리 내에 있는 모듈을 표현하기 위한 완전히 새로운 자료 구조를 생성한다. 코드나 데이터 세그먼트가 로드되어야 할 필요가 있을때, 로더는 전역 힙으로 부터 새로운 세그먼트를 할당받고 가공되지 않은 데이터들이 어디에 저장되어있는지 찾고, 그 위치로 이동한 후, 가공되지 않은 데이터를 읽는다. 그리고는 수정해야할 사항들을 적용한다. 게다가, 각각의 16비트 모듈은 세그먼트가 버렸는지에 상관없이 그것이 지금 사용하고 있는 모든 셀렉터들을 기억해야할 필요가 있다.
Win32 에서는, 코드, 데이터, 리소스, 임포트 테이블, 익스포트 테이블및 필요한 다른 모듈 자료 구조들을 위해서 모듈에서 사용되는 모든 메모리는 메모리의 인접한 블록에 위치해 있다. 이러한 상황에서 알아야할 모든것은 로더가 파일을 메모리의 어디로 맵핑했는지이다. 모듈의 모든 다양한 조각들을 아래에 설명하는 이미지의 일부로 저장되어 있는 포인터를 사용해서 쉽게 찾을 수 있다.
다른 가장 중요한 아이디어는 상대 가상 주소(RVA)이다. PE 파일의 많은 필드들은 RVA관점에서 표시하고 있다. RVA는간단하게 파일이 메모리 맵된 곳으로부터 상대적인 몇가지 아이템들의 오프셋이다. 예를들면, 로더가 PE 파일을 가상 주소 공간의 0x1000으로 시작되는 메모리로 맵핑했다고 가정해 보자. 그리고 이미지에 포함된 실제 테이블은 0x10464라는 주소에서 시작된다면, 그 테이블의 RVA는 0x464가 된다.
(Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464
RVA를 쓸모있는 포인터로 변환하기 위해서는, 간단하게 모듈의 베이스 주소에 RVA를 더해주면 된다. 베이스 주소는 EXE나 DLL의 메모리 맵 시작 주소이며 Win32에서 중요한 컨셉이다. 편의상, 윈도우 NT와 윈도우 95는 모듈의 인스턴스 핸들(HINSTANCE)로 모듈의 베이스 주소를 사용한다. Win32에서 모듈의 베이스 주소를 HINSTANCE라 부르는 것은 뭔가 혼란 스럽다. 왜냐하면 "인스턴스 핸들"이라는 용어는 16비트 윈도우에서 유래되었기 때문이다.
16비트 윈도우에서 각각의 애플리케이션 복사본은 다른 복사본과 구분되는 자신만의 독립된 데이터 세그먼트(전역 핸들과 연관되어 있다.)를 할당받는다. 여기에서 인스턴스 핸들이라는 용어가 나온것이다. Win32에서 애플리케이션은 다른 것들과 구분될 필요가 없다. 왜냐하면 동일한 주소 공간을 공유하지 않기 때문이다. 지금까지도, HISTANCE라는 용어는 16비트 윈도우와 32비트 윈도우의 연속성을 유지하기위해서 살아남아있다. Win32를 위해서 중요한 것은 프로세스가 모듈의 콤포넌트를 접근하기 위한 포인터를 얻기위해서 GetModuleHandle 호출할 수 있다는 것이다.
PE파일과 관련해서 마지막으로 알아야할 컨셉은 섹션이다. PE파일내의 섹션은 NE 파일에서의 세그먼트나 리소스와 크게는 비슷한 것이다. 섹션은 코드와 데이터를 모두 포함하고 있다. 세그먼트와 달리, 섹션은 사이즈의 제약없이 연속된 메모리 블록이다. 몇가지 섹션은 프로그램에서 직접 선언하고 사용하는 코드나 데이터를 포함한다. 반면에 다른 섹션은 링커와 라이브러리에 의해서 생성된 것이며 운영체제와 관련한 중요한 정보를 포함하고 있다. 몇몇 PE 포맷에 대한 설명에서, 섹션을 또한 오브젝트라고 부르기도 한다. 오브젝트라는 용어는 너무 많은 중첩된 의미를 가지고 있다. 그래서 나는 코드와 데이터 영역을 섹션이라고 부르는 것을 선호한다.
PE 헤더
모든 다른 파일 포맷과 마찬가지로, PE 파일도 파일의 나머지 부분이 어떻게 보일지 정의하는 알려진 (찾기 쉬운) 위치에 있는 필드들을 가지고 있다. 이 헤더는 코드와 데이터 영역의 위치와 사이즈, 운영체제에서 정한것으로 스택 크기와 앞으로 짧게 설명할 다른 중요한 정보 조각들의 정보를 포함하고 있다. 마이크로소프트사에서 만든 다른 실행 포맷처럼, 이 메인 헤더는 파일의 시작 부분에 있지 않다. 전형적인 PE파일의 처음 수백 바이트는 MS-DOS 스텁으로 구성된다. 이 스텁은 "This program cannot be run in MS-DOS mode."라는 것을 출력하고 그러한 효과를 지닌 작은 프로그램이다. 따라서 만약 Win32 기반의 프로그램을 Win32를 지원하지 않는 환경에서 실행하게 되면, 해당 메시지를 출력하게 된다. Win32 로더가 PE 파일을 메모리 맵할때, 메모리 맵 파일의 첫번째 바이트는 MS-DOS 스텁의 첫번째 바이트와 일치한다. 맞다. Win32 기반의 프로그램이 시작할때마다, MS-DOS 기반의 프로그램또한 공짜로 로드되는 것이다.
다른 마이크로소프트 실행 포맷과 같이, 진짜 헤더는 MS-DOS 스텁 헤더에 저장되어져있는 시작 오프셋을 조사해서 찾을수 있다. WINNT.H 파일은 PE 헤더가 어디서 시작하는지 찾기 쉽게 만들기 위해서 MS-DOS 스텁 헤더를 위한 구조체 정의를 포함하고 있다. e_lfanew 필드는 실질적인 PE 헤더에 대한 상대적인 오프셋(RVA) 이다. 메모리에서 PE 헤더를 가리키는 포인터를 얻기 위해서는, 단지 해당 필드의 값을 이미지 베이스 주소에 더하기만 하면 된다.
// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;
메인 PE 헤더에 대한 포인터를 한번 얻고나면, 재미있는 일들이 시작된다. 메인 PE 헤더는 WINNT.H에 정의된 IMAGE_NT_HEADERS라는 타입의 구조체이다. 이 구조체는 DWORD와 두개의 서브 구조체로 구성되고 아래에 나오는 것과 같은 순서로 놓여져있다.
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
아스키 텍스트로 보여지는 Singnature 필드는 "PE\0\0"이다. 16비트 윈도우의 NE파일에서는 MS-DOS헤더 내에 포함된 e_lfanew 필드를 사용해서 포인터를 구했을때, PE 대신 NE 시그니처를 볼 수 있다. 같은 이치로, 윈도우즈 3.x 가상 디바이스 드라이버 (VxD)에서는 시그니처 필드에 LE가 나타난다.
PE 헤더에서 PE 시그니쳐 DWORD 다음에 따라 나오는것은 IMAGE_FILE_HEADER 타입의 구조체이다. 이 구조체의 필드들은 단순히 파일에 관한 가장 기초적인 정보들만 포함하고 있다. 이 구조체는 오리지널 COFF 구현과 동일한 것으로 보인다. 게다가 PE 헤더의 일부일뿐만 아니라, 그것은 또한 마이크로소프트 Win32 컴파일러가 생성한 COFF OBJ의 시작 부분에도 나타난다. Table 2에 IMAGE_FILE_HEADER의 필드들이 표시되어 있다.
Table 2. IMAGE_FILE_HEADER 필드
WORD Machine
이 파일이 사용되도록 계획된 CPU. 아래의 CPU ID가 정의되어 있다.: 0x14d는 인텔 i860을 의미한다.
0x14c 인텔 i386 (486과 586에도 동일한 ID)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP
WORD NumberOfSections
파일에 있는 섹션 갯수
DWORD TimeDateStamp
링커(또는 OBJ파일을 생성한 컴파일러)가 파일을 생성한 시간. 이 필드는 1969-12-31 4:00 부터의 초를 카운트하는 숫자이다.
DWORD PointerToSymbolTable
COFF 심볼 테이블의 파일 오프셋
이 필드는 COFF 디버그 정보를 가진 OBJ 파일과 PE파일에서만 사용한다.
PE 파일은 다양한 디버그 포맷을 지원한다. 그래서 디버거는 데이타 디렉토리(후에 정의된) 에 있는IMAGE_DIRECTORY_ENTRY_DEBUG 엔트리를 참조 하기만 하면 된다.
DWORD NumberOfSymbols
COFF 심볼 테이블에 있는 심볼 갯수. 위의 내용 참고.
WORD SizeOfOptionalHeader
이 구조체 다음에 나올 수 있는 추가적인 헤더의 크기.
OBJ 파일에서, 이 필드는 0이다. 실행파일에서는, 이 구조체 뒤에 따라 나오는 IMAGE_OPTIONAL_HEADER 구조체의 크기이다.
WORD Characteristics
파일과 관련된 정보 플래그. 중요한 필드들: 0x0001 이 파일에서는 재배치가 없다.
0x0002 파일은 실행 가능한 이미지이다. (OBJ나 LIB가 아니다.)
0x2000 파일은 프로그램이 아닌 동적 연결 라이브러이다.
WINNT.H에 정의된 다른 필드들
PE 헤더의 세번째 컴포넌트는 IMAGE_OPTIONAL_HEADER 타입의 구조체 이다. PE 파일에서는 이 부분은 확실히 옵션이 아니다. COFF 포맷은 표준 IMAGE_FILE_HEADER와 관련된 추가적인 정보로 구성된 구조체를 정의하기 위해서 개개의 구현을 허용한다. IMAGE_OPTIONAL_HEADER에 있는 필드들은 PE 디자이너들이 IMAGE_FILE_HEADER에 있는 기초적인 정보 이상으로 중요한 정보라고 느낀 것들이다.
IMAGE_OPTIONAL_HEADER의 모든 필드들에 관해서 알 필요는 없다. (Figure 4를 보라) 알고있어야 할 더 중요한 것은 ImageBase와 Subsystem 필드이다. 필드에 관한 설명을 훑어보거나 건너뛰어도 된다.
Table 3. IMAGE_OPTIONAL_HEADER 필드
WORD Magic
어느정도 시그니쳐 워드(WORD)로 보임. 항상 0x010B로 셋팅되어 있다.
BYTE MinorLinkerVersion
이 피알을 생성한 링커의 버전.
숫자는 16진수가 아닌 10진 값으로 출력되어야 한다. 전형적인 링커 버전은 2.23이다.
DWORD SizeOfCode
모든 코드 섹션을 합한 사이즈.
일반적으로, 대부분의 파일은 하나의 코드 섹션을 가지고 있다, 그래서 이 필드는 .text 섹션의 크기와 일치한다.
DWORD SizeOfInitializedData
이것은 초기화된 데이터로 구성된 섹션의 전체 크기로 추정된다. (코드 세그먼트를 포함하지 않은)
그러나, 이것은 파일에서 무엇이 나오는지 일정하지 않다.
DWORD SizeOfUninitializedData
로더가 가상 주소 공간위에 프로그램을 위해서 할당한 영역위에 있는 섹션의 크기, 그러나 디스크 파일에서는 어떠한 공간도 가지지 않는다. 이 섹션들은 프로그램 시작시 특정한 값을 가질 필요가 없다. 그래서 초기화 되지 않은 데이터란 용어를 사용했다. 초기화되지않은 데이터는 일반적으로 .bss로 불리는 섹션으로 통한다.
DWORD AddressOfEntryPoint
로더가 실행을 시작하는 주소. 이것은 RVA이다. 그리고 일반적으로 .text섹션에서 발견된다.
DWORD BaseOfCode
파일의 코드 섹션들이 시작되는 곳의 RVA. 코드 섹션들은 전형적으로 메모리에서 data 섹션들 이전에 나타나며 PE 헤더 이후에 나타난다. 이 RVA는 마이크로소프트 링커가 생성한 EXE들에서는 보통 0x10000이다. 볼랜드의 TLINK32는 이미지 베이스 주소에 첫번째 코드 섹션의 RVA를 더한후에 그것을 이 필드에 저장한다.
DWORD BaseOfData
파일의 데이터 섹션이 시작하는 곳의 RVA. 데이터 섹션은 통상적으로 메모리에서 PE헤더와 코드 섹션 다음의 마지막 부분에 오게 된다.
DWORD ImageBase
링커가 실행 파일을 만들때, 링커는 파일이 메모리의 어느 위치에 메모리 맵될지 가정한다. 그 주소가 이 필드에 저장되어 있다. 로드 주소를 가정하는 것은 링커가 최적화를 할 수 있게한다. 만약 파일이 로더에 의해서 실제로 그 주소로 메모리 맵된다면, 코드는 실행되기 전에 어떠한 패치도 할 필요가 없게 된다. 윈도우 NT를 위해서 생성된 실행파일에서는, 기본 이미지 베이스 주소는 0x1000이다. DLL에서는 기본값이 0x400000이다. 윈도우 95에서는 0x10000주소는 32비트 EXE파일을 로드하는데 사용할 수 없다. 왜냐하면 선형적인 주소공간을 모든 프로세스가 공유해서 사용하기 때문이다. 이러한 이유때문에, 마이크로소프는 Win32 실행 파일의 기본 베이스 주소를 0x400000로 변경했다. 로더가 베이스 주소를 재배치해야할 필요가 있기 때문에, 0x10000을 기본 주소로 가정하고 링크된 오래된 프로그램은 Win95 환경 아래에서는 로드시간이 더 길게 된다.
DWORD SectionAlignment
메모리로 맵될때, 각각의 섹션은 이 값의 배수가 되는 가상 주소에서 시작된다는 것을 보장받는다. 페이징과 같은 목적 때문에, 기본 섹션 정렬 값은 0x1000이다.
DWORD FileAlignment
PE 파일에서, 각각의 섹션은 이 값의 배수가 되는 곳에서 시작하도록 보장받는데 합의한 값. 기본 값은 0x200 바이트이다. 아마도 섹션은 항상 디스크 섹터(이것 또한 0x200 바이트)의 시작 지점에서 시작한다는 것을 보장해야 하기 때문일 것이다. 이 필드는 NE 파일의 세그먼트/리소스 정렬 크기와 같다. NE 파일과는 다르게, PE 파일은 전형적으로 수백개의 섹션을 가지지 않는다. 따라서 파일 섹션 정렬때문에 낭비되는 공간은 아주 작다.
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
이 실행 파일을 실행하는데 요구되는 최소한의 운영체제 버전. 이 필드는 다소 난해하다. 서브시스템 필드(추후에 소개될)또한 같은 목적으로 예약된 것처럼 보이기 때문이다. 현재까지 모든 Win32 Exe 파일에서 이 필드의 기본값은 1.0이다.
WORD MajorImageVersion
WORD MinorImageVersion
사용자가 정의할 수 있는 필드이다. 이것은 EXE나 DLL을 다른 버전으로 만들 수 있게 허용한다. 링커의 /VERSION 스위치를 사용해서 이 필드를 설정할 수 있다. 예를 들면, "LINK /VERSION:2.0 myobj.obj".
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
실행 파일을 실행하는데 요구되는 서브시스템의 최소 버전번호를 포함하고 있다. 이 필드의 일반적인 값은 3.10이다. (윈도우 NT 3.1을 의미한다.)
DWORD Reserved1
항상 0으로 설정하는 것으로 보인다.
DWORD SizeOfImage
로더가 걱정하는 이미지 일부의 전체 크기로 추정된다. 이것은 이미지의 베이스 주소에서 시작해서 마지막 섹션 끝까지의 영역의 크기를 말한다. 마지막 섹션의 끝은 섹션 정렬의 배수 근처에 있게 된다.
DWORD SizeOfHeaders
The size of the PE header and the section (object) table. The raw data for the sections starts immediately after all the header components. PE 헤더와 (오브젝트) 테이블 섹션의 크기이다. 이 섹션의 바이너리 데이터는 모든 헤더 구성 요소 다음에 바로 위치한다.
DWORD CheckSum
아마도 파일의 CRC 체크섬인 것 같다. 다른 마이크로소프트의 실행 파일 포맷에서 이 필드는 0으로 설정되고 무시된다. 이 방법이 통하지 않는 한가는 예외는 신뢰된 서비스이다. 이 EXE 파일은 반드시 정확한 체크섬 값을 가지고 있어야 한다.
WORD Subsystem
실행 파일이 유저 인터페이스로 사용하는 서브시스템 타입. WINNT.H는 다음과 같은 값들을 정의하고 있다.
NATIVE 1 서브시스템을 요구하지 않는다.(디바이스 드라이버 같은것들)
WINDOWS_GUI 2 윈도우 GUI 서브시스템에서 실행된다.
WINDOWS_GUI 3 윈도우 문자 서브시스템에서 실행된다. (콘솔 애플리케이션)
OS2_CUI 5 OS/2 문자 서브시스템에서 실행된다. (OS/2 1.x 애플리케이션만 적용된다.)
POSIX_CUI 7 Posix 문자 서브시스템에서 실행된다.
WORD DllCharacteristics
DLL의 초기화 함수(DllMain과 같은)가 호출되는 환경과 관련된 플래그 집합. 이 값은 항상 0으로 설정되는 것으로 보인다. 그러나 운영체제는 여전히 DLL 초기화 함수를 항상 모든 4가지 이벤트에 맞추어서 호출한다. 아래와 같은 값들이 정의되어 있다.
1. DLL이 프로세스 주소 공간으로 처음 로딩될 때.
2. 쓰레드가 종료될 때.
3. 쓰레드가 시작될 때.
4. DLL이 종료될 때.
DWORD SizeOfStackReserve
초기 쓰레드의 스택을 위해서 예약해야할 가상 메모리의 크기. 다음 필드를 참고해 보면 이 메모리 전부가 실제 메모리로 사용되지 않는다는 것을 알 수 있다. 이 필드의 기본값은 0x100000 (1MB) 이다. 만약 CreateThread에서 스택 크기를 0으로 설정했다면 그 결과과 만들어진 쓰레드는 이것과 동일한 크기의 스택을 가지게 된다.
DWORD SizeOfStackCommit
초기 쓰레드의 스택을 위해서 사용하는 실제 메모리의 크기. 마이크로소프트 링커는 기본적으로 이 필드를 0x1000(1페이지) 바이트로 설정된다. 반면에 TLINK32는 2 페이지로 만든다.
DWORD SizeOfHeapReserve
초기 프로세스의 힙으로 예약해야할 가상 메모리의 크기. 이 힙 핸들은 GetProcessHeap함수를 호출해서 얻을 수 있다. 이 메모리 모두가 실제 메모리로 사용되지 않는다. (다음 필드를 보라)
DWORD SizeOfHeapCommit
초기 프로세스 힙으로 사용될 실제 메모리의 크기. 기본적으로 1페이지가 설정된다.
DWORD LoaderFlags
WINNT.H에 의하면 이것은 디버깅 지원과 관련된 필드로 보인다. 나는 이제껏 한번도 이 비트를 활성화 시켜둔 실행 파일을 본적이없을 뿐 아니라, 링커가 이 필드를 설정하게 하는 방법또한 확실치 않다. 다음과 같은 값들이 정의되어져 있다.
1. 프로세스를 시작하기 전에 브레이크 포인트 명령을 실행한다.
2. 프로세스가 로드된 이후에 디버거를 실행한다.
DWORD NumberOfRvaAndSizes
DataDirectory 배열 (아래에 나오는)에 있는 엔트리 갯수. 이 값은 현재까지 툴에서는 항상 16으로 설정한다.
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
IMAGE_DATA_DIRECTORY 구조체의 배열이다. 배열의 초기 값은 실행 파일의 중요한 부분의 RVA 시작 지점과 크기을 포함하고 있다. 배열 끝의 몇가지 항목은 현재까지는 사용되지 않는다. 배열의 첫번째 요소는 항상 익스포트 함수 테이블(만약 존재한다면) 의 주소와 크기를 가지고 있다. 두번째 배열의 항목은 임포트된 함수 테이블의 주소와 크기를 가지고 있다. 정의된 배열 항목에 대한 완전한 리스트는 WINNT.H에서 #define으로 선언된 IMAGE_DIRECTORY_ENTRY_XXX를 참고하면 된다. 이 배열은 각각의 이미지 섹션을 순회하면서 같은 이름이 있는지 비교하지 않고 로더가 이미지의 특정한 섹션을 빨리 찾을 수 있도록 도와준다. (예를들면, 임포트된 함수 테이블과 같은 것이다.) 대부분의 배열 항목은 전체 섹션 데이터에 대한 정보를 가지고 있다. 그러나, IMAGE_DIRECTORY_ENTRY_DEBUG 항목은 .rdata 섹션의 일부 바이트만을 포함한다.
섹션 테이블
PE 헤더와 바이너리 데이터 데이터 사이에는 이미지의 섹션을 위한 섹션 테이블이 위치해 있다. 섹션 테이블은 본질적으로 이미지에 포함된 각각의 섹션과 관련된 정보를 가지고 있는 전화번호부와 같은 역할을 한다. 이미지에 포함된 섹션은 알파벳순이 아닌, 시작 주소(RVA)에 의해서 정렬된다.
이제 섹션이 무엇인지 좀 더 명확하게 할 수 있다. NE 파일에서, 프로그램의 코드와 데이터는 파일의 구분된 "세그먼트"내에서 정렬되어진다. NE 헤더중의 일부는 프로그램이 사용하는 각각의 세그먼트의 구조체 배열이다. 배열속의 각각의 구조체는 하나의 세그먼트에 대한 정보를 가지고 있다. 저장되어져있는 정보는 세그먼트의 타입(코드냐 데이터냐), 크기, 그리고 파일에서 저장된 위치에 대한 것들을 포함한다. PE 파일에서, 섹션 테이블은 NE 파일의 세그먼트 테이블과 유사하다. 반면에 NE 파일의 세그먼트 테이블과는 대조적으로 각각의 섹션 테이블 항목은 파일의 바이너리 데이터가 메모리로 맵핑되는 위치에 대한 주소를 저장하고 있다. 섹션이 32비트 세그먼트와 유사하지만, 섹션이 실제로 각각의 세그먼트가 되는 것은 아니다. 그것들은 단지 프로세스의 가상 주소 공간에서 메모리 범위일 뿐이다.
또 다른 PE 파일이 NE 파일의 차이점은 프로그램이 사용하지 않고 운영체제에서 사용하는 지원(서포팅) 데이터들을 다루는 방법이다. 예를들면, 실행 파일이 사용하는 DLL의 목록이나 픽스업 테이블의 위치와 같은 것이다. NE 파일에서는, 리소스는 세그먼트로 고려되지 않는다. 비록 리소스에 할당된 셀렉터들이 있다고 하더라도, 리소스와 관련된 정보는 NE 헤더의 세그먼트 테이블에 저장되지 않는다. 대신에, 리소스는 NE 헤더의 마지막에 위차한 분리된 테이블내에 저장된다. 임포트되거나 익스포트된 함수와 관련된 정보 또한 독립된 세그먼트를 가질 권리가 없다. 해당 정보들은 NE 헤더에 포함된다.
PE파일에서는 이 모든 이야기가 틀리게 된다. 필수적인 코드와 데이터로 고려되는 모든것은 완전한 섹션속에 저장된다. 그래서 임포트된 함수와 관련된 정보는 독립된 섹션에 저장되며, 익스포트된 함수 테으블또한 그렇게 된다. 재배치 데이터와 관련된 것들도 동일한 방식으로 처리된다. 프로그램 또는 운영체제에서 필요한 어떠한 코드나 데이터도 그들의 독립적인 섹션을 가지게 된다.
특정 섹션과 관련된 논의를 하기 전에, 운영체제가 다루는 섹션과 관련된 데이터를 먼저 설명할 필요가 있다. 메모리에서 PE 헤더 바로 뒤에는 IMAGE_SECTION_HEADER의 배열이 오게 된다. 배열 항목의 갯수는 PE 헤더에서 알 수 있다. (IMAGE_NT_HEADER.FileHeader.NumberOfSections 필드) 섹션 테이블과 모든 섹션 필드와 속성을 출력하기 위해서 PEDUMP를 사용했다. 그림 5는 PEDUMP의 보편적인 EXE 파일의 섹션 테이블 출력 결과를 보여주고, 그림 6은 OBJ 파일의 섹션 테이블을 보여준다.
Table 4. A Typical Section Table from an EXE File
01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #'s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ
02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE
03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ
04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ[/code]
[color=red][b]Table 5. A Typical Section Table from an OBJ File [/b][/color]
[code]01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE
02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #'s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE
04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #'s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ
05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
각각의 IMAGE_SECTION_HEADER는 그림 7에서 묘사된 것과 같은 포맷을 가지고 있다. 각각의 섹션을 위해서 저장된 정보중에 무엇이 빠졌는지 알아내는 것은 흥미로운 작업이다. 첫째로, PRELOAD 속성을 위한 어떤 표시도 없다는 점이다. NE 파일 포맷은 PRELOAD 속성을 통해서 어떠한 세그먼트가 모듈이 로드되는 시점에 같이 로드되어야 하는지 결정한다.Os/2 2.0 LX 포맷은 8페이지까지 먼저 로드되어야 하는 것으로 지정할 수 있다는 점에서 유사하다. PE 포맷은 이런 유사한 것이 아무것도 없다. 마이크로소프트는 Win32 의존적인 페이지 로딩의 성능을 확신했음에 틀림없다.
Table 6. IMAGE_SECTION_HEADER 포맷
BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 이것은 8바이트 안시로된 섹션 이름이다(유니코드가 아니다). 대부분의 섹션명은 .로 시작된다. (예를들면 ".text"), 그러나 몇몇 믿을만한 PE 문서에서 보면 이것은 요구사항은 아니다. 어셈블리 언어에서는 segment 지시자를 사용해서 섹션명을 지을 수 있고, 마이크로소프트의 C/C++ 컴파일러에서는 "#pragma data_seg"와 "#pragama code_seg"를 사용해서 지을 수 있다. 섹션명이 8바이트를 넘을 경우에는 NULL 종료 문자가 없게 된다는 것을 알아야 한다. 만약 devotee라는 섹션명을 출력코자 할 경우에는, %.8s를 사용해서 추가로 다른 버퍼에 복사하지 않고 출력할 수 있다.
union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;
이 필드는 EXE와 OBJ 파일에서 다른 의미를 지니게 된다. EXE파일에서는, 이 필드는 코드와 데이터의 실제 사이즈를 가지고 있다. 이것은 파일 정렬 경계의 배수근처에서 반올림 되기전의 크기가 된다. 추후에 설명될 구조체의 SizeOfRawData 필드는 (약간 잘못된 명칭으로 보인다) 반올림된 값을 가지고 있다. 볼랜드 링커는 이 두 필드의 의미를 역으로 만들었으나 지금은 고쳐진 것으로 보인다. OBJ 파일에서는, 이 필드는 섹션의 물리적인 주소와 관련되어 있다. 첫번째 섹션은 0번지에서 시작한다. 따라서 OBJ 파일에서 다음 섹션의 물리적인 주소를 찾으려면, SizeOfRawData 값에 현재 섹션의 물리 주소를 더하면 된다.
DWORD VirtualAddress
EXE 파일에서는 로더가 섹션을 맵핑해야 하는 곳의 RVA를 저장하고 있다. 메모리에서 주어진 섹션의 실제 시작 주소를 계산하기 위해서는, 이미지의 베이스 주소에 이 필드에 저장된 섹션의 가상 주소를 더해주면 된다. 마이크로소프트 툴에서는 첫번째 섹션의 기본값은 0x1000 RVA로 되어 있다. OBJ 파일에서는 이 필드는 의미가 없으며 0으로 설정되어 있다.
DWORD SizeOfRawData
EXE 파일에서는, 이 필드는 파일 정렬 크기에서 반올림 된 후 섹션의 크기를 담고 있다. 예를 들면, 파일 정렬 크기가 0x200이라고 가정해 보자. 위에서 언급한 그 섹션에 대한 VirtualSize가 0x35A 바이트였다고 한다면, 이 필드는 0x400 바이트가 되는 것이다. OBJ 파일에서는, 이 필드는 컴파일러와 어셈블러에서 종결한 그 섹션의 정확한 크기를 담고 있다. 다시말하면, OBJ에서는 EXE 파일에서의 VirtualSize와 동일한 의미를 가진다는 것이다.
DWORD PointerToRawData
이 것은 컴파일러나 어셈블러가 종결한 바이너리 데이터를 찾을 수 있는 곳에 대한 파일 기반의 오프셋이다. 프로그램이 PE나 COFF 파일을 스스로 메모리 맵하려고 한다면(운영체제가 그것을 로드하는 대신에), 이 필드는 VIrtualAddress 필드보다 더 중요하다. 이 상황에서는 완전히 선형적인 파일 맵핑이 이루어지게 된다. 따라서 섹션 데이터를 VritaulAddress 필드가 가리키는 RVA에서 보다는 이 필드가 가리키는 오프셋에서 찾을 수 있다.
DWORD PointerToRelocations
OBJ 파일에서, 이것은 이 섹션에 대한 재배치 정보를 가리키는 파일 기반의 오프셋이다. 각각의 OBJ 섹션에 대한 재배치 정보는 해당 섹션에 대한 바이너리 데이터 바로 뒤에 따라온다. EXE 파일에서, 이 필드는 (아래에 나오는 필드또한) 의미가 없으며, 0으로 설정된다. 링커가 EXE파일을 만들때, 베이스 주소 재배치와 로드 타임에 해결되어야 할 임포트된 함수를 남겨둔 나머지 대부분의 수정 사항을 모두 수정한다. 베이스 주소 재배치와 임포트된 함수와 관련된 정보는 각각의 섹션에 저장되게 된다. 따라서 EXE 파일이 바이너리 섹션 데이터에 따라 나오는 섹션별 재배치 데이터를 가질 필요가 없는 것이다.
DWORD PointerToLinenumbers
이것은 라인 번호 테이블의 파일 기반 오프셋이다. 라인 번호 테이블은 소스 파일의 라인 번호와 주어진 라인에 대해서 생성된 코드의 주소를 서로 연관시킨다. CodeView 포맷과 같은 현대의 디버그 포맷에서는, 라인 번호 정보는 디버그 정보의 일부로 저장된다. 그러나 COFF 디버그 포맷에서는 라인 번호 정보는 심볼 이름/타입 정보와 분리되어서 저장된다. 일반적으로 단지 코드 섹션(.text같은)만이 라인 번호를 가진다. EXE 파일에서는, 라인 번호는 섹션 바이너리 데이터 다음부터 파일 끝까지 수집되게 된다. OBJ 파일에서는, 섹션의 라인 번호 테이블은 바이너리 섹션 데이터와 섹션 재배치 테이블 다음에 오게 된다.
WORD NumberOfRelocations
섹션에 포함된 재배치 테이블(위의 PointerToRelocations 필드) 안의 재배치 갯수. 이 필드는 단지 OBJ 파일에서만 유효한 것으로 보인다.
WORD NumberOfLinenumbers
섹션 라인 번호 테이블(위에서 말한 PointerToLinenumbers)에서 라인 번호 갯수
DWORD Characteristics
대부분의 프로그래머가 플래그라 부르는 것을 COFF/PE 포맷에서는 속성(characteristics)라 부른다. 이 필드는 섹션 속성과 관련된 플래그 집합(코드/데이터냐?, 읽기가능? 쓰기가능?)이다. 모든 가능한 섹션 속성은 WNNT.H에서 IMAGE_SCN_XXX_XXX형태로 #define 된 것으로 찾을 수 있다. 몇가지 중요한 속성으로 다음과 같은 것들이 있다.
0x00000020 이 섹션은 코드를 포함하고 있다. 일반적으로 실행 가능 플래그(0x80000000)와 결합해서 사용된다.
0x00000040 이 섹션은 초기화된 데이터를 포함하고 있다. 실행 가능하지않은 거의 대부분의 섹션과 .bss 섹션은 이 플래그가 설정되어져 있다.
0x00000080 이 섹션은 초기화되지 않은 데이터를 포함하고 있다(예를들면 .bss 섹션).
0x00000200 이 섹션은 코멘트나 다른 타입의 정보를 포함하고 있다. 이 플래그를 사용하는 일반적인 섹션은 컴파일러에 의해서 선언된 .drective 섹션이다. .directive 섹션은 링커를 위한 커맨드를 포함하고 있다.
0x00000800 이 섹션의 내용은 EXE 파일에는 담겨서는 안된다. 이 섹션들은 컴파일러와/어셈블러가 링커에게 정보를 전달하기 위해서 사용한다.
0x02000000 이 섹션은 프로세스가 한번 로드되고 나면 더 이상 필요하지 않기 때문에 무시될 수 있다. 대부분의 무시될 수 있는 것은 베이스 재배치(.reloc) 섹션이다.
0x10000000 이 섹션은 공유가능하다. DLL과 함께 사용될때, 이 섹션에 포함된 데이터는 DLL을 사용하는 프로세스 간에 공유될 수 있다. 데이터 섹션의 기본값은 공유하지 않는(nonshared) 이다. DLL을 사용하는 각각의 프로세스는 섹션에 포함된 데이터의 자신만의 복사본을 가진다는 의미이다. 좀더 기술적인 용어로 설명하면, 공유 섹션은 메모리 매니저에게 이 섹션의 페이지 맵핑을 DLL을 사용하는 모든 프로세스가 메모리내의 동일한 물리적 페이지를 참조하도록 설정하는 것이다. 섹션을 공유가능하도록 만들기 위해서, SHARED 속성을 링크 타임에 사용할 수 있다. 예를들면,
LINK /SECTION:MYDATA,RWS ...
링커에게 MYDATA라는 섹션은 읽기, 쓰기, 공유가 가능하다고 알려주는 것이다.
0x20000000 이 섹션은 실행가능하다. 이 플래그는 보통 "코드 포함" 플래그 (0x00000020)가 설정될때마다 설정된다.
0x40000000 이 섹션은 읽기 가능하다. 이 플래는 EXE 파일내에 포함된 거의 모든 섹션에 대해서 설정된다.
0x80000000 이 섹션은 쓰기 가능하다. 만약 EXE 파일의 섹션에 이 플래그가 설정되어 있지 않다면, 로더는 메모리 맵 페이지를 읽기 전용 내지는 실행 전용으로 마크한다. 일반적으로 이 속성을 가진 섹션은 .data와 .bss다. 흥미롭게도, .idata 섹션은 또한 이 속성을 설정하고 있다.
또한 PE 포맷에서 제외된것으로 페이지 테이블이 있다. LX 포맷의 IMAGE_SECTION_HEADER에 대한 OS/2의 동치는 파일에서 발견될 수 있는 코드나 데이터 섹션이 있는 곳을 직접적으로 가리키진 않는다. 대신에 그것은 섹션내에 있는 페이지의 구체적인 범위의 위치와 속성을 가지고 있는 페이지 룩업 테이블을 참조한다. PE 포맷은 그러한 모든 것들을 하지않아도 되게 한다. 그리고 섹션 데이터가 파일내에서 연속적으로 저장되도록 보장한다. 두가지 포맷에서, LX가 취하는 방법은 유연성을 허용한다. 그러나 PE 스타일은 작업하기가 더욱 간단하고 쉽다. 두가지 포맷을 위한 파일 덤퍼를 작성해야 할때, 나는 이것을 보장할 수 있다.
PE 포맷에서 좋아진 또다른 점으로 저장된 아이템의 위치가 간단하게 DWORD 오프셋이라는 것을 들 수 있다. NE 포맷에서는, 거의 대부분의 위치는 섹터 값으로 저장되어 진다. 실제 오프셋을 찾기 위해서 첫째로 NE 헤더의 정렬 유닛 크기를 보고 그것을 섹터 크기로 변환해야 한다. (일반적으로 16내지는 512 바이트) 그리고 나서는 실제 파일 오프셋을 구하기 위해서 섹터 크기에 지정된 섹터 오프셋을 곱할 필요가 있다. NE 파일에서 섹터 오프셋으로 저장되어져 있지 않은 어떤것이 있다면, 그것은 아마도 NE 헤더부터의 상대 오프셋으로 저장된 것이다. NE 헤더가 파일의 시작 부분에 있지 않기 때문에, 코드에서 NE 헤더의 파일 오프셋을 사용해야 할 필요가 있다. 대체로, PE 포맷은 NE, LX내지는 LE 포맷보다 다루기가 쉽다(메모리 맵 파일을 사용할 수 있다는 가정하에서)
이 문서는 msdn의 매트 피에트릭의 문서 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndebug/html/msdn_peeringpe.asp 를 번역한 것 입니다.
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
운영체제의 실행 파일의 포맷은 많은 부분에서 운영체제의 거울이라고 할 수 있다. 비록 실행 파일 포맷이 대부분의 프로그래머들이 공부해야 할 것중 높은 위치를 차지하지 않지만 중요한 지식을 이러한 방법을 통해서 모을 수 있다. 이 문서에서는 모든 Win32 기반의 시스템 -- Windows NT, Win32s, Windows 95 -- 에서 사용하기 위해서 디자인된 마이크로소프의 PE 파일 포맷에 관해서 알아볼 것이다. PE 파일 포맷은 Windows 2000을 포함한 가까운 장래에 나올 모든 마이크로소프트 운영체제에서 중요한 역할을 할 것이다. Win32s나 Windows NT를 사용하고 있다면, 벌써 PE 파일을 사용하고 있는 것이다. VC++을 사용해서 Windows 3.1만을 위한 프로그램을 작성한다고 할지라도 여전히 PE 파일(32비트 MS-DOS 확장)을 사용하고 있는 것이다. 짧게 말해서, PE는 벌써 보급되어 있으며 가까운 장래에 바뀌기는 힘들것이다. 지금이 이 새로운 실행 파일 타입이 운영체제 파티에 무엇을 가지고 왔는지 살펴볼 시간이다.
끝이 없는 헥사 덤프를 빤히 쳐다보고는 페이지 끝에 나오는 각각의 비트들의 중요성을 곱씹는 대신에 PE 파일 포맷이 포함하고 있는 켄셉과 매일 부딪히는 상황과 관련지어서 살펴보도록 하자. 예를들면 아래에 나오는 것 같은 쓰레드 지역 변수의 표시는
declspec(thread) int i;
실행 파일 내에서 우아하고 간결하게 어떻게 구현되는지 알기 전까지는 나를 미치게 만들었다. 많은 독자들이 16비트 Windows로 부터 넘어왔기 때문에, 나는 Win32 PE 파일 포맷의 구성요소들과 과거 16비트 NE 파일 포맷의 동등한 부분들을 연관지어서 설명할 것이다.
게다가 마이크로소프트사의 컴파일러와 어셈블러가 만들어 내는 새로운 오브젝트 모듈 포맷과의 차이점도 살펴볼 것이다. 이 새로운 OBJ 파일 포맷은 많은 부분에서 PE 파일과 유사하다. 나는 결국 새로운 OBJ 파일 포맷과 관련된 어떠한 문서도 없다는 것을 알게 되었다. 그래서 나는 스스로 그것들을 분석했고, 여기서 PE 포맷에 추가해서 일부를 설명할 것이다.
Windows NT가 VAX VMS와 UNIX로 부터 발전되었다는 것은 많이 알려진 사실이다. Windows NT을 작성한 많은 프로그래머들은 마이크로소프로 오기 전에 그러한 플랫폼에서의 코딩과 디자인을 했었다. Windows NT를 디자인 할때, 그들은 이전에 작성하고 테스트된 툴을 사용해서 부트스트랩 작성 시간을 줄이는 것은 자연스러운 것이었다. 이러한 툴들이 생성하고 같이 작업한 실행가능한 파일 포맷과 오브젝트 모듈 포맷은 COFF(Comm Object File Format의 머릿글자를 따서 지어졌다.)라 불렸다. COFF의 상대적인 나이는 8진수 형태로 필드를 구체화 시켰다는 점에서 알 수 있다. COFF 포맷은 그것 자체로는 좋은 시작 지점이었다. 그러나 Windows NT나 Windows 95와 같은 현대의 운영체제 시스템에서 요구하는 것을 충족시키기 위해서는 확장할 필요가 있었다. 이러한 업데이트의 결과가 PE 포맷이다. 그것은 "이식 가능하다고(prtable)" 불렸다. 왜냐하면 Windows NT의 모든 구현은 다양한 플랫폼에서 (x86, MIPS, Alpha, ...) 동일한 실행 파일 포맷을 사용하기 때문이다. 물론 CPU 명령어에따른 바이너리 인코딩의 차이는 있다. 중요한 점은 운영체제 로더와 프로그래밍 툴들은 각각의 새로운 CPU에 대해서 완전히 새롭게 작성할 필요가 없다는 점이다.
마이크로소프트 위원회가 윈도우 NT를 업그레이드 시키고 빠르게 동작시키기 위한 노력은 존재하는 32비트 툴과 파일 포맷을 포기했다는 점에서 찾을 수 있다. 16비트 윈도우의 가상 디바이스 드라이버는 윈도우 NT가 물망에 오르기 훨씬 전에 나온 다른 32비트 파일 구조 - LE 포맷 - 를 사용하고 있었다. 더 중요한 것은 OBJ 파일 포맷의 변화이다. 윈도우 NT 이전의 C 컴파일러, Win32환경에서 실행되는 모든 마이크로소프트 컴파일러는 COFF 포맷의 OBJ 파일을 생성한다. 볼랜드나 시만텍같은 몇몇 마이크로소프트 경쟁자들은 COFF 포맷을 보류하고 인텔의 OMF 포맷을 사용한다. 마침내 이것은 다양한 컴파일러에서 사용하기 위해서는 OBJ나 LIB 파일을 다른 컴파일러를 위한 별도의 버전을 제작해서 배포하는 수고가 필요가게된 것이다.
PE 포맷은 WINNT.H 헤더파일에 대충(성의없이) 문서화 되어있다. 대략 WINNT.H의 중간 부분 정도에 "Image Format"이라고 이름지어진 섹션이 있다. 이 섹션은 새로운 PE 정보로 옮겨가기전 오래되어 친숙한 MS-DOS MZ 포맷과 NE 포맷 헤더에서 나온 작은 tidbits로 시작한다. WINNT.H는 PE 파일에서 사용되는 가공되지않은 자료 구조들에 대한 정의를 제공한다. 그러나 단지 그 구조체들과 플래그들이 무엇을 의미하는지 이해하기 위한 아주 조금의 유용한 코멘트만 포함하고 있다. PE 포맷에 관한 헤더 파일을 작성했던 사람이 누구든지(Michael J. O'Leary가 떠오르고 있다), 그는 깊숙하게 중첩된 구조체와 매크로를 따라서 길고, 묘사적인 이름에 관한 신봉자임에 틀림없다. WINNT.H와 함께 코등힐때, 이러한 표현을 사용하는 것은 전혀 이상한 것이 아니다.
pNTHeader->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
WINNT.H의 정보를 좀 더 쉽게 이해하기 위해서는 2001년 10월 이후에 배포된 MSDN 라이브러리 CD-ROM에서 구할 수 있는 Protable Executable and Common Object File Format 항목을 읽기를 권한다.
주제를 COFF 포맷 OBJ로 잠깐 돌려서, WINNT.H 헤더 파일은 COFF OBJ및 LIB 파일을 위한 typedefs와 구조체 정의를 포함하고 있다. 불행히도, 나는 위에 언급된 실행 파일 포맷과 마찬가지로 이것에 관한 어떠한 문서도 찾을 수 없었다. Since PE files and COFF OBJ files are so similar, I decided that it was time to bring these files out into the light and document them as well. PE파일과 COFF OBJ파일은 매우 유사하기 때문에 나는 이러한 파일들에 대한 가벼운 문서를 남기기기로 결정했다.???
PE파일이 무엇으로 구성되었는지에 관해서 알기보다는, 직접 이러한 컨셉을 보기위해서 몇몇 PE 파일들을 덤프하기를 원할 것이다. 만약 Win32 기반의 개발을 위해서 마이크로소프트 툴들을 사용하고 잇다면, DUMPBIN 프로그램이 PE 파일과 COFF OBJ/LIB 파일들을 분석해서 읽을수 있는 형태로 출력해준다. Of all the PE file dumpers, DUMPBIN is easily the most comprehensive. It even has a nifty option to disassemble the code sections in the file it's taking apart. 모든 PE 파일 덤프를 하는 프로그램 중에서, DUMPBIN은 가장 이해하기 쉽다. DUMPBIN은 자신이 분석하고 있는 파일의 코드 섹션을 디어셈블할 수 있는 멋진 옵션을 가지고 있다. 볼랜드 유저는 PE 실행 파일을 보기 위해서 TDUMP를 사용할 수 있다, 그러나 TDUMP는 COFF OBJ 파일들을 이해하지 못한다. 이것은 볼랜드 컴파일러가 COFF 포맷의 OBJ 파일을 만들지 않기 때문에 큰 문제가 되지 않는다.
나는 PE와 COFF OBJ파일을 덤프해주는 PEDUMP라는 프로그램을 만들었다.(Table 1을 보라) 나는 PEDUMP가 DUMPBIN에 비해서 더욱 이해하기 쉬운 출력을 제공한다고 생각한다. 비록 PEDUMP가 디어셈블러를 가지고 있지 않고 LIB 파일을 분석할 수 없지만, 다른 부분들은 DUMPBIN과 기능적으로는 동일하다. 그리고 생각해볼 가치가 있는 새로운 기능들도 추가되어있다. PEDUMP 소스코드는 MSJ 게시판에서 얻을 수 있기 때문에 전체 리스트를 여기에 보이지 않을 것이다. 대신에 내가 설명한 컨셉트들을 보여주기 위해서 PEDUMP의 샘플 출력을 보여줄 것이다.
Win32 와 PE의 기본적인 컨셉
PE 파일의 디자인의 골격을 이루고 있는 기초적인 아이디어들을 살펴보도록 하자. "모듈"이라는 용어는메모리에 로드된 실행 파일이나 DLL의 코드, 데이터 그리고 리소스를 나타낼때 사용할 것이다. 게다가 프로그램이 직접 사용하는 코드와 데이터뿐만 아니라 모듈은 코드와 데이터가 메모리 어느 위치에 있는지 결정하기 위해서 윈도우에서 사용하는 자료 구조로 구성된다. 16비트 윈도우에서는, 윈도우에서 사용하는 자료 구조가 모듈 데이터 베이스 내에 있었다(HMODULE에 의해 참조되는 세그먼트). Win32에서는 이러한 자료 구조들은 지금부터 간단히 설명할 PE 헤더내에 있다.
그림 1. PE 파일 포맷
PE 파일에 관해서 알아야할 첫번째로 가장 중요한 것은 디스크에 있는 실행 파일은 윈도우가 로드한 후의 모듈과 매우 유사하다는 점이다. 윈도우 로더는 디스크 파일로부터 프로세스를 생성하기 위해서 극도로 어려운 일들을 필요로 하지 않는다. 로더는 파일의 조각들을 가상 주소 공간으로 적절하게 매핑하기 위해서 메모리 맵 파일 메카니즘을 사용한다. 건축과 비교하자면 PE 파일을 조립식 주택에 비유할 수 있다. 조립식 주택으로 집을 지으려면 결국 각 부분을 한 번에 하나씩 집 지을 자리에 옮겨 놓은 다음 각 부분을 외부와 연결시키는 작업이 필요하다(PE에 DLL을 연결시키는 것처럼). 이러한 쉬운 로딩 방식은 PE 포맷 DLL에도 동일하게 적용된다. 모듈이 한번 로드되고 나면, 윈도우는 다른 메모리 맵 파일과 같이 효율적으로 다룰수 있게 된다.
이러한 로딩 방식은 16비트 윈도우에서의 상황과 완전히 대조적이다. 16비트 NE 파일 로더는 파일의 일부분을 읽고 메모리 내에 있는 모듈을 표현하기 위한 완전히 새로운 자료 구조를 생성한다. 코드나 데이터 세그먼트가 로드되어야 할 필요가 있을때, 로더는 전역 힙으로 부터 새로운 세그먼트를 할당받고 가공되지 않은 데이터들이 어디에 저장되어있는지 찾고, 그 위치로 이동한 후, 가공되지 않은 데이터를 읽는다. 그리고는 수정해야할 사항들을 적용한다. 게다가, 각각의 16비트 모듈은 세그먼트가 버렸는지에 상관없이 그것이 지금 사용하고 있는 모든 셀렉터들을 기억해야할 필요가 있다.
Win32 에서는, 코드, 데이터, 리소스, 임포트 테이블, 익스포트 테이블및 필요한 다른 모듈 자료 구조들을 위해서 모듈에서 사용되는 모든 메모리는 메모리의 인접한 블록에 위치해 있다. 이러한 상황에서 알아야할 모든것은 로더가 파일을 메모리의 어디로 맵핑했는지이다. 모듈의 모든 다양한 조각들을 아래에 설명하는 이미지의 일부로 저장되어 있는 포인터를 사용해서 쉽게 찾을 수 있다.
다른 가장 중요한 아이디어는 상대 가상 주소(RVA)이다. PE 파일의 많은 필드들은 RVA관점에서 표시하고 있다. RVA는간단하게 파일이 메모리 맵된 곳으로부터 상대적인 몇가지 아이템들의 오프셋이다. 예를들면, 로더가 PE 파일을 가상 주소 공간의 0x1000으로 시작되는 메모리로 맵핑했다고 가정해 보자. 그리고 이미지에 포함된 실제 테이블은 0x10464라는 주소에서 시작된다면, 그 테이블의 RVA는 0x464가 된다.
(Virtual address 0x10464)-(base address 0x10000) = RVA 0x00464
RVA를 쓸모있는 포인터로 변환하기 위해서는, 간단하게 모듈의 베이스 주소에 RVA를 더해주면 된다. 베이스 주소는 EXE나 DLL의 메모리 맵 시작 주소이며 Win32에서 중요한 컨셉이다. 편의상, 윈도우 NT와 윈도우 95는 모듈의 인스턴스 핸들(HINSTANCE)로 모듈의 베이스 주소를 사용한다. Win32에서 모듈의 베이스 주소를 HINSTANCE라 부르는 것은 뭔가 혼란 스럽다. 왜냐하면 "인스턴스 핸들"이라는 용어는 16비트 윈도우에서 유래되었기 때문이다.
16비트 윈도우에서 각각의 애플리케이션 복사본은 다른 복사본과 구분되는 자신만의 독립된 데이터 세그먼트(전역 핸들과 연관되어 있다.)를 할당받는다. 여기에서 인스턴스 핸들이라는 용어가 나온것이다. Win32에서 애플리케이션은 다른 것들과 구분될 필요가 없다. 왜냐하면 동일한 주소 공간을 공유하지 않기 때문이다. 지금까지도, HISTANCE라는 용어는 16비트 윈도우와 32비트 윈도우의 연속성을 유지하기위해서 살아남아있다. Win32를 위해서 중요한 것은 프로세스가 모듈의 콤포넌트를 접근하기 위한 포인터를 얻기위해서 GetModuleHandle 호출할 수 있다는 것이다.
PE파일과 관련해서 마지막으로 알아야할 컨셉은 섹션이다. PE파일내의 섹션은 NE 파일에서의 세그먼트나 리소스와 크게는 비슷한 것이다. 섹션은 코드와 데이터를 모두 포함하고 있다. 세그먼트와 달리, 섹션은 사이즈의 제약없이 연속된 메모리 블록이다. 몇가지 섹션은 프로그램에서 직접 선언하고 사용하는 코드나 데이터를 포함한다. 반면에 다른 섹션은 링커와 라이브러리에 의해서 생성된 것이며 운영체제와 관련한 중요한 정보를 포함하고 있다. 몇몇 PE 포맷에 대한 설명에서, 섹션을 또한 오브젝트라고 부르기도 한다. 오브젝트라는 용어는 너무 많은 중첩된 의미를 가지고 있다. 그래서 나는 코드와 데이터 영역을 섹션이라고 부르는 것을 선호한다.
PE 헤더
모든 다른 파일 포맷과 마찬가지로, PE 파일도 파일의 나머지 부분이 어떻게 보일지 정의하는 알려진 (찾기 쉬운) 위치에 있는 필드들을 가지고 있다. 이 헤더는 코드와 데이터 영역의 위치와 사이즈, 운영체제에서 정한것으로 스택 크기와 앞으로 짧게 설명할 다른 중요한 정보 조각들의 정보를 포함하고 있다. 마이크로소프트사에서 만든 다른 실행 포맷처럼, 이 메인 헤더는 파일의 시작 부분에 있지 않다. 전형적인 PE파일의 처음 수백 바이트는 MS-DOS 스텁으로 구성된다. 이 스텁은 "This program cannot be run in MS-DOS mode."라는 것을 출력하고 그러한 효과를 지닌 작은 프로그램이다. 따라서 만약 Win32 기반의 프로그램을 Win32를 지원하지 않는 환경에서 실행하게 되면, 해당 메시지를 출력하게 된다. Win32 로더가 PE 파일을 메모리 맵할때, 메모리 맵 파일의 첫번째 바이트는 MS-DOS 스텁의 첫번째 바이트와 일치한다. 맞다. Win32 기반의 프로그램이 시작할때마다, MS-DOS 기반의 프로그램또한 공짜로 로드되는 것이다.
다른 마이크로소프트 실행 포맷과 같이, 진짜 헤더는 MS-DOS 스텁 헤더에 저장되어져있는 시작 오프셋을 조사해서 찾을수 있다. WINNT.H 파일은 PE 헤더가 어디서 시작하는지 찾기 쉽게 만들기 위해서 MS-DOS 스텁 헤더를 위한 구조체 정의를 포함하고 있다. e_lfanew 필드는 실질적인 PE 헤더에 대한 상대적인 오프셋(RVA) 이다. 메모리에서 PE 헤더를 가리키는 포인터를 얻기 위해서는, 단지 해당 필드의 값을 이미지 베이스 주소에 더하기만 하면 된다.
// Ignoring typecasts and pointer conversion issues for clarity...
pNTHeader = dosHeader + dosHeader->e_lfanew;
메인 PE 헤더에 대한 포인터를 한번 얻고나면, 재미있는 일들이 시작된다. 메인 PE 헤더는 WINNT.H에 정의된 IMAGE_NT_HEADERS라는 타입의 구조체이다. 이 구조체는 DWORD와 두개의 서브 구조체로 구성되고 아래에 나오는 것과 같은 순서로 놓여져있다.
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
아스키 텍스트로 보여지는 Singnature 필드는 "PE\0\0"이다. 16비트 윈도우의 NE파일에서는 MS-DOS헤더 내에 포함된 e_lfanew 필드를 사용해서 포인터를 구했을때, PE 대신 NE 시그니처를 볼 수 있다. 같은 이치로, 윈도우즈 3.x 가상 디바이스 드라이버 (VxD)에서는 시그니처 필드에 LE가 나타난다.
PE 헤더에서 PE 시그니쳐 DWORD 다음에 따라 나오는것은 IMAGE_FILE_HEADER 타입의 구조체이다. 이 구조체의 필드들은 단순히 파일에 관한 가장 기초적인 정보들만 포함하고 있다. 이 구조체는 오리지널 COFF 구현과 동일한 것으로 보인다. 게다가 PE 헤더의 일부일뿐만 아니라, 그것은 또한 마이크로소프트 Win32 컴파일러가 생성한 COFF OBJ의 시작 부분에도 나타난다. Table 2에 IMAGE_FILE_HEADER의 필드들이 표시되어 있다.
Table 2. IMAGE_FILE_HEADER 필드
WORD Machine
이 파일이 사용되도록 계획된 CPU. 아래의 CPU ID가 정의되어 있다.: 0x14d는 인텔 i860을 의미한다.
0x14c 인텔 i386 (486과 586에도 동일한 ID)
0x162 MIPS R3000
0x166 MIPS R4000
0x183 DEC Alpha AXP
WORD NumberOfSections
파일에 있는 섹션 갯수
DWORD TimeDateStamp
링커(또는 OBJ파일을 생성한 컴파일러)가 파일을 생성한 시간. 이 필드는 1969-12-31 4:00 부터의 초를 카운트하는 숫자이다.
DWORD PointerToSymbolTable
COFF 심볼 테이블의 파일 오프셋
이 필드는 COFF 디버그 정보를 가진 OBJ 파일과 PE파일에서만 사용한다.
PE 파일은 다양한 디버그 포맷을 지원한다. 그래서 디버거는 데이타 디렉토리(후에 정의된) 에 있는IMAGE_DIRECTORY_ENTRY_DEBUG 엔트리를 참조 하기만 하면 된다.
DWORD NumberOfSymbols
COFF 심볼 테이블에 있는 심볼 갯수. 위의 내용 참고.
WORD SizeOfOptionalHeader
이 구조체 다음에 나올 수 있는 추가적인 헤더의 크기.
OBJ 파일에서, 이 필드는 0이다. 실행파일에서는, 이 구조체 뒤에 따라 나오는 IMAGE_OPTIONAL_HEADER 구조체의 크기이다.
WORD Characteristics
파일과 관련된 정보 플래그. 중요한 필드들: 0x0001 이 파일에서는 재배치가 없다.
0x0002 파일은 실행 가능한 이미지이다. (OBJ나 LIB가 아니다.)
0x2000 파일은 프로그램이 아닌 동적 연결 라이브러이다.
WINNT.H에 정의된 다른 필드들
PE 헤더의 세번째 컴포넌트는 IMAGE_OPTIONAL_HEADER 타입의 구조체 이다. PE 파일에서는 이 부분은 확실히 옵션이 아니다. COFF 포맷은 표준 IMAGE_FILE_HEADER와 관련된 추가적인 정보로 구성된 구조체를 정의하기 위해서 개개의 구현을 허용한다. IMAGE_OPTIONAL_HEADER에 있는 필드들은 PE 디자이너들이 IMAGE_FILE_HEADER에 있는 기초적인 정보 이상으로 중요한 정보라고 느낀 것들이다.
IMAGE_OPTIONAL_HEADER의 모든 필드들에 관해서 알 필요는 없다. (Figure 4를 보라) 알고있어야 할 더 중요한 것은 ImageBase와 Subsystem 필드이다. 필드에 관한 설명을 훑어보거나 건너뛰어도 된다.
Table 3. IMAGE_OPTIONAL_HEADER 필드
WORD Magic
어느정도 시그니쳐 워드(WORD)로 보임. 항상 0x010B로 셋팅되어 있다.
BYTE MinorLinkerVersion
이 피알을 생성한 링커의 버전.
숫자는 16진수가 아닌 10진 값으로 출력되어야 한다. 전형적인 링커 버전은 2.23이다.
DWORD SizeOfCode
모든 코드 섹션을 합한 사이즈.
일반적으로, 대부분의 파일은 하나의 코드 섹션을 가지고 있다, 그래서 이 필드는 .text 섹션의 크기와 일치한다.
DWORD SizeOfInitializedData
이것은 초기화된 데이터로 구성된 섹션의 전체 크기로 추정된다. (코드 세그먼트를 포함하지 않은)
그러나, 이것은 파일에서 무엇이 나오는지 일정하지 않다.
DWORD SizeOfUninitializedData
로더가 가상 주소 공간위에 프로그램을 위해서 할당한 영역위에 있는 섹션의 크기, 그러나 디스크 파일에서는 어떠한 공간도 가지지 않는다. 이 섹션들은 프로그램 시작시 특정한 값을 가질 필요가 없다. 그래서 초기화 되지 않은 데이터란 용어를 사용했다. 초기화되지않은 데이터는 일반적으로 .bss로 불리는 섹션으로 통한다.
DWORD AddressOfEntryPoint
로더가 실행을 시작하는 주소. 이것은 RVA이다. 그리고 일반적으로 .text섹션에서 발견된다.
DWORD BaseOfCode
파일의 코드 섹션들이 시작되는 곳의 RVA. 코드 섹션들은 전형적으로 메모리에서 data 섹션들 이전에 나타나며 PE 헤더 이후에 나타난다. 이 RVA는 마이크로소프트 링커가 생성한 EXE들에서는 보통 0x10000이다. 볼랜드의 TLINK32는 이미지 베이스 주소에 첫번째 코드 섹션의 RVA를 더한후에 그것을 이 필드에 저장한다.
DWORD BaseOfData
파일의 데이터 섹션이 시작하는 곳의 RVA. 데이터 섹션은 통상적으로 메모리에서 PE헤더와 코드 섹션 다음의 마지막 부분에 오게 된다.
DWORD ImageBase
링커가 실행 파일을 만들때, 링커는 파일이 메모리의 어느 위치에 메모리 맵될지 가정한다. 그 주소가 이 필드에 저장되어 있다. 로드 주소를 가정하는 것은 링커가 최적화를 할 수 있게한다. 만약 파일이 로더에 의해서 실제로 그 주소로 메모리 맵된다면, 코드는 실행되기 전에 어떠한 패치도 할 필요가 없게 된다. 윈도우 NT를 위해서 생성된 실행파일에서는, 기본 이미지 베이스 주소는 0x1000이다. DLL에서는 기본값이 0x400000이다. 윈도우 95에서는 0x10000주소는 32비트 EXE파일을 로드하는데 사용할 수 없다. 왜냐하면 선형적인 주소공간을 모든 프로세스가 공유해서 사용하기 때문이다. 이러한 이유때문에, 마이크로소프는 Win32 실행 파일의 기본 베이스 주소를 0x400000로 변경했다. 로더가 베이스 주소를 재배치해야할 필요가 있기 때문에, 0x10000을 기본 주소로 가정하고 링크된 오래된 프로그램은 Win95 환경 아래에서는 로드시간이 더 길게 된다.
DWORD SectionAlignment
메모리로 맵될때, 각각의 섹션은 이 값의 배수가 되는 가상 주소에서 시작된다는 것을 보장받는다. 페이징과 같은 목적 때문에, 기본 섹션 정렬 값은 0x1000이다.
DWORD FileAlignment
PE 파일에서, 각각의 섹션은 이 값의 배수가 되는 곳에서 시작하도록 보장받는데 합의한 값. 기본 값은 0x200 바이트이다. 아마도 섹션은 항상 디스크 섹터(이것 또한 0x200 바이트)의 시작 지점에서 시작한다는 것을 보장해야 하기 때문일 것이다. 이 필드는 NE 파일의 세그먼트/리소스 정렬 크기와 같다. NE 파일과는 다르게, PE 파일은 전형적으로 수백개의 섹션을 가지지 않는다. 따라서 파일 섹션 정렬때문에 낭비되는 공간은 아주 작다.
WORD MajorOperatingSystemVersion
WORD MinorOperatingSystemVersion
이 실행 파일을 실행하는데 요구되는 최소한의 운영체제 버전. 이 필드는 다소 난해하다. 서브시스템 필드(추후에 소개될)또한 같은 목적으로 예약된 것처럼 보이기 때문이다. 현재까지 모든 Win32 Exe 파일에서 이 필드의 기본값은 1.0이다.
WORD MajorImageVersion
WORD MinorImageVersion
사용자가 정의할 수 있는 필드이다. 이것은 EXE나 DLL을 다른 버전으로 만들 수 있게 허용한다. 링커의 /VERSION 스위치를 사용해서 이 필드를 설정할 수 있다. 예를 들면, "LINK /VERSION:2.0 myobj.obj".
WORD MajorSubsystemVersion
WORD MinorSubsystemVersion
실행 파일을 실행하는데 요구되는 서브시스템의 최소 버전번호를 포함하고 있다. 이 필드의 일반적인 값은 3.10이다. (윈도우 NT 3.1을 의미한다.)
DWORD Reserved1
항상 0으로 설정하는 것으로 보인다.
DWORD SizeOfImage
로더가 걱정하는 이미지 일부의 전체 크기로 추정된다. 이것은 이미지의 베이스 주소에서 시작해서 마지막 섹션 끝까지의 영역의 크기를 말한다. 마지막 섹션의 끝은 섹션 정렬의 배수 근처에 있게 된다.
DWORD SizeOfHeaders
The size of the PE header and the section (object) table. The raw data for the sections starts immediately after all the header components. PE 헤더와 (오브젝트) 테이블 섹션의 크기이다. 이 섹션의 바이너리 데이터는 모든 헤더 구성 요소 다음에 바로 위치한다.
DWORD CheckSum
아마도 파일의 CRC 체크섬인 것 같다. 다른 마이크로소프트의 실행 파일 포맷에서 이 필드는 0으로 설정되고 무시된다. 이 방법이 통하지 않는 한가는 예외는 신뢰된 서비스이다. 이 EXE 파일은 반드시 정확한 체크섬 값을 가지고 있어야 한다.
WORD Subsystem
실행 파일이 유저 인터페이스로 사용하는 서브시스템 타입. WINNT.H는 다음과 같은 값들을 정의하고 있다.
NATIVE 1 서브시스템을 요구하지 않는다.(디바이스 드라이버 같은것들)
WINDOWS_GUI 2 윈도우 GUI 서브시스템에서 실행된다.
WINDOWS_GUI 3 윈도우 문자 서브시스템에서 실행된다. (콘솔 애플리케이션)
OS2_CUI 5 OS/2 문자 서브시스템에서 실행된다. (OS/2 1.x 애플리케이션만 적용된다.)
POSIX_CUI 7 Posix 문자 서브시스템에서 실행된다.
WORD DllCharacteristics
DLL의 초기화 함수(DllMain과 같은)가 호출되는 환경과 관련된 플래그 집합. 이 값은 항상 0으로 설정되는 것으로 보인다. 그러나 운영체제는 여전히 DLL 초기화 함수를 항상 모든 4가지 이벤트에 맞추어서 호출한다. 아래와 같은 값들이 정의되어 있다.
1. DLL이 프로세스 주소 공간으로 처음 로딩될 때.
2. 쓰레드가 종료될 때.
3. 쓰레드가 시작될 때.
4. DLL이 종료될 때.
DWORD SizeOfStackReserve
초기 쓰레드의 스택을 위해서 예약해야할 가상 메모리의 크기. 다음 필드를 참고해 보면 이 메모리 전부가 실제 메모리로 사용되지 않는다는 것을 알 수 있다. 이 필드의 기본값은 0x100000 (1MB) 이다. 만약 CreateThread에서 스택 크기를 0으로 설정했다면 그 결과과 만들어진 쓰레드는 이것과 동일한 크기의 스택을 가지게 된다.
DWORD SizeOfStackCommit
초기 쓰레드의 스택을 위해서 사용하는 실제 메모리의 크기. 마이크로소프트 링커는 기본적으로 이 필드를 0x1000(1페이지) 바이트로 설정된다. 반면에 TLINK32는 2 페이지로 만든다.
DWORD SizeOfHeapReserve
초기 프로세스의 힙으로 예약해야할 가상 메모리의 크기. 이 힙 핸들은 GetProcessHeap함수를 호출해서 얻을 수 있다. 이 메모리 모두가 실제 메모리로 사용되지 않는다. (다음 필드를 보라)
DWORD SizeOfHeapCommit
초기 프로세스 힙으로 사용될 실제 메모리의 크기. 기본적으로 1페이지가 설정된다.
DWORD LoaderFlags
WINNT.H에 의하면 이것은 디버깅 지원과 관련된 필드로 보인다. 나는 이제껏 한번도 이 비트를 활성화 시켜둔 실행 파일을 본적이없을 뿐 아니라, 링커가 이 필드를 설정하게 하는 방법또한 확실치 않다. 다음과 같은 값들이 정의되어져 있다.
1. 프로세스를 시작하기 전에 브레이크 포인트 명령을 실행한다.
2. 프로세스가 로드된 이후에 디버거를 실행한다.
DWORD NumberOfRvaAndSizes
DataDirectory 배열 (아래에 나오는)에 있는 엔트리 갯수. 이 값은 현재까지 툴에서는 항상 16으로 설정한다.
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
IMAGE_DATA_DIRECTORY 구조체의 배열이다. 배열의 초기 값은 실행 파일의 중요한 부분의 RVA 시작 지점과 크기을 포함하고 있다. 배열 끝의 몇가지 항목은 현재까지는 사용되지 않는다. 배열의 첫번째 요소는 항상 익스포트 함수 테이블(만약 존재한다면) 의 주소와 크기를 가지고 있다. 두번째 배열의 항목은 임포트된 함수 테이블의 주소와 크기를 가지고 있다. 정의된 배열 항목에 대한 완전한 리스트는 WINNT.H에서 #define으로 선언된 IMAGE_DIRECTORY_ENTRY_XXX를 참고하면 된다. 이 배열은 각각의 이미지 섹션을 순회하면서 같은 이름이 있는지 비교하지 않고 로더가 이미지의 특정한 섹션을 빨리 찾을 수 있도록 도와준다. (예를들면, 임포트된 함수 테이블과 같은 것이다.) 대부분의 배열 항목은 전체 섹션 데이터에 대한 정보를 가지고 있다. 그러나, IMAGE_DIRECTORY_ENTRY_DEBUG 항목은 .rdata 섹션의 일부 바이트만을 포함한다.
섹션 테이블
PE 헤더와 바이너리 데이터 데이터 사이에는 이미지의 섹션을 위한 섹션 테이블이 위치해 있다. 섹션 테이블은 본질적으로 이미지에 포함된 각각의 섹션과 관련된 정보를 가지고 있는 전화번호부와 같은 역할을 한다. 이미지에 포함된 섹션은 알파벳순이 아닌, 시작 주소(RVA)에 의해서 정렬된다.
이제 섹션이 무엇인지 좀 더 명확하게 할 수 있다. NE 파일에서, 프로그램의 코드와 데이터는 파일의 구분된 "세그먼트"내에서 정렬되어진다. NE 헤더중의 일부는 프로그램이 사용하는 각각의 세그먼트의 구조체 배열이다. 배열속의 각각의 구조체는 하나의 세그먼트에 대한 정보를 가지고 있다. 저장되어져있는 정보는 세그먼트의 타입(코드냐 데이터냐), 크기, 그리고 파일에서 저장된 위치에 대한 것들을 포함한다. PE 파일에서, 섹션 테이블은 NE 파일의 세그먼트 테이블과 유사하다. 반면에 NE 파일의 세그먼트 테이블과는 대조적으로 각각의 섹션 테이블 항목은 파일의 바이너리 데이터가 메모리로 맵핑되는 위치에 대한 주소를 저장하고 있다. 섹션이 32비트 세그먼트와 유사하지만, 섹션이 실제로 각각의 세그먼트가 되는 것은 아니다. 그것들은 단지 프로세스의 가상 주소 공간에서 메모리 범위일 뿐이다.
또 다른 PE 파일이 NE 파일의 차이점은 프로그램이 사용하지 않고 운영체제에서 사용하는 지원(서포팅) 데이터들을 다루는 방법이다. 예를들면, 실행 파일이 사용하는 DLL의 목록이나 픽스업 테이블의 위치와 같은 것이다. NE 파일에서는, 리소스는 세그먼트로 고려되지 않는다. 비록 리소스에 할당된 셀렉터들이 있다고 하더라도, 리소스와 관련된 정보는 NE 헤더의 세그먼트 테이블에 저장되지 않는다. 대신에, 리소스는 NE 헤더의 마지막에 위차한 분리된 테이블내에 저장된다. 임포트되거나 익스포트된 함수와 관련된 정보 또한 독립된 세그먼트를 가질 권리가 없다. 해당 정보들은 NE 헤더에 포함된다.
PE파일에서는 이 모든 이야기가 틀리게 된다. 필수적인 코드와 데이터로 고려되는 모든것은 완전한 섹션속에 저장된다. 그래서 임포트된 함수와 관련된 정보는 독립된 섹션에 저장되며, 익스포트된 함수 테으블또한 그렇게 된다. 재배치 데이터와 관련된 것들도 동일한 방식으로 처리된다. 프로그램 또는 운영체제에서 필요한 어떠한 코드나 데이터도 그들의 독립적인 섹션을 가지게 된다.
특정 섹션과 관련된 논의를 하기 전에, 운영체제가 다루는 섹션과 관련된 데이터를 먼저 설명할 필요가 있다. 메모리에서 PE 헤더 바로 뒤에는 IMAGE_SECTION_HEADER의 배열이 오게 된다. 배열 항목의 갯수는 PE 헤더에서 알 수 있다. (IMAGE_NT_HEADER.FileHeader.NumberOfSections 필드) 섹션 테이블과 모든 섹션 필드와 속성을 출력하기 위해서 PEDUMP를 사용했다. 그림 5는 PEDUMP의 보편적인 EXE 파일의 섹션 테이블 출력 결과를 보여주고, 그림 6은 OBJ 파일의 섹션 테이블을 보여준다.
Table 4. A Typical Section Table from an EXE File
01 .text VirtSize: 00005AFA VirtAddr: 00001000
raw data offs: 00000400 raw data size: 00005C00
relocation offs: 00000000 relocations: 00000000
line # offs: 00009220 line #'s: 0000020C
characteristics: 60000020
CODE MEM_EXECUTE MEM_READ
02 .bss VirtSize: 00001438 VirtAddr: 00007000
raw data offs: 00000000 raw data size: 00001600
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000080
UNINITIALIZED_DATA MEM_READ MEM_WRITE
03 .rdata VirtSize: 0000015C VirtAddr: 00009000
raw data offs: 00006000 raw data size: 00000200
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 40000040
INITIALIZED_DATA MEM_READ
04 .data VirtSize: 0000239C VirtAddr: 0000A000
raw data offs: 00006200 raw data size: 00002400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
05 .idata VirtSize: 0000033E VirtAddr: 0000D000
raw data offs: 00008600 raw data size: 00000400
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: C0000040
INITIALIZED_DATA MEM_READ MEM_WRITE
06 .reloc VirtSize: 000006CE VirtAddr: 0000E000
raw data offs: 00008A00 raw data size: 00000800
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42000040
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ[/code]
[color=red][b]Table 5. A Typical Section Table from an OBJ File [/b][/color]
[code]01 .drectve PhysAddr: 00000000 VirtAddr: 00000000
raw data offs: 000000DC raw data size: 00000026
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 00100A00
LNK_INFO LNK_REMOVE
02 .debug$S PhysAddr: 00000026 VirtAddr: 00000000
raw data offs: 00000102 raw data size: 000016D0
relocation offs: 000017D2 relocations: 00000032
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
03 .data PhysAddr: 000016F6 VirtAddr: 00000000
raw data offs: 000019C6 raw data size: 00000D87
relocation offs: 0000274D relocations: 00000045
line # offs: 00000000 line #'s: 00000000
characteristics: C0400040
INITIALIZED_DATA MEM_READ MEM_WRITE
04 .text PhysAddr: 0000247D VirtAddr: 00000000
raw data offs: 000029FF raw data size: 000010DA
relocation offs: 00003AD9 relocations: 000000E9
line # offs: 000043F3 line #'s: 000000D9
characteristics: 60500020
CODE MEM_EXECUTE MEM_READ
05 .debug$T PhysAddr: 00003557 VirtAddr: 00000000
raw data offs: 00004909 raw data size: 00000030
relocation offs: 00000000 relocations: 00000000
line # offs: 00000000 line #'s: 00000000
characteristics: 42100048
INITIALIZED_DATA MEM_DISCARDABLE MEM_READ
각각의 IMAGE_SECTION_HEADER는 그림 7에서 묘사된 것과 같은 포맷을 가지고 있다. 각각의 섹션을 위해서 저장된 정보중에 무엇이 빠졌는지 알아내는 것은 흥미로운 작업이다. 첫째로, PRELOAD 속성을 위한 어떤 표시도 없다는 점이다. NE 파일 포맷은 PRELOAD 속성을 통해서 어떠한 세그먼트가 모듈이 로드되는 시점에 같이 로드되어야 하는지 결정한다.Os/2 2.0 LX 포맷은 8페이지까지 먼저 로드되어야 하는 것으로 지정할 수 있다는 점에서 유사하다. PE 포맷은 이런 유사한 것이 아무것도 없다. 마이크로소프트는 Win32 의존적인 페이지 로딩의 성능을 확신했음에 틀림없다.
Table 6. IMAGE_SECTION_HEADER 포맷
BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 이것은 8바이트 안시로된 섹션 이름이다(유니코드가 아니다). 대부분의 섹션명은 .로 시작된다. (예를들면 ".text"), 그러나 몇몇 믿을만한 PE 문서에서 보면 이것은 요구사항은 아니다. 어셈블리 언어에서는 segment 지시자를 사용해서 섹션명을 지을 수 있고, 마이크로소프트의 C/C++ 컴파일러에서는 "#pragma data_seg"와 "#pragama code_seg"를 사용해서 지을 수 있다. 섹션명이 8바이트를 넘을 경우에는 NULL 종료 문자가 없게 된다는 것을 알아야 한다. 만약 devotee라는 섹션명을 출력코자 할 경우에는, %.8s를 사용해서 추가로 다른 버퍼에 복사하지 않고 출력할 수 있다.
union {
DWORD PhysicalAddress
DWORD VirtualSize
} Misc;
이 필드는 EXE와 OBJ 파일에서 다른 의미를 지니게 된다. EXE파일에서는, 이 필드는 코드와 데이터의 실제 사이즈를 가지고 있다. 이것은 파일 정렬 경계의 배수근처에서 반올림 되기전의 크기가 된다. 추후에 설명될 구조체의 SizeOfRawData 필드는 (약간 잘못된 명칭으로 보인다) 반올림된 값을 가지고 있다. 볼랜드 링커는 이 두 필드의 의미를 역으로 만들었으나 지금은 고쳐진 것으로 보인다. OBJ 파일에서는, 이 필드는 섹션의 물리적인 주소와 관련되어 있다. 첫번째 섹션은 0번지에서 시작한다. 따라서 OBJ 파일에서 다음 섹션의 물리적인 주소를 찾으려면, SizeOfRawData 값에 현재 섹션의 물리 주소를 더하면 된다.
DWORD VirtualAddress
EXE 파일에서는 로더가 섹션을 맵핑해야 하는 곳의 RVA를 저장하고 있다. 메모리에서 주어진 섹션의 실제 시작 주소를 계산하기 위해서는, 이미지의 베이스 주소에 이 필드에 저장된 섹션의 가상 주소를 더해주면 된다. 마이크로소프트 툴에서는 첫번째 섹션의 기본값은 0x1000 RVA로 되어 있다. OBJ 파일에서는 이 필드는 의미가 없으며 0으로 설정되어 있다.
DWORD SizeOfRawData
EXE 파일에서는, 이 필드는 파일 정렬 크기에서 반올림 된 후 섹션의 크기를 담고 있다. 예를 들면, 파일 정렬 크기가 0x200이라고 가정해 보자. 위에서 언급한 그 섹션에 대한 VirtualSize가 0x35A 바이트였다고 한다면, 이 필드는 0x400 바이트가 되는 것이다. OBJ 파일에서는, 이 필드는 컴파일러와 어셈블러에서 종결한 그 섹션의 정확한 크기를 담고 있다. 다시말하면, OBJ에서는 EXE 파일에서의 VirtualSize와 동일한 의미를 가진다는 것이다.
DWORD PointerToRawData
이 것은 컴파일러나 어셈블러가 종결한 바이너리 데이터를 찾을 수 있는 곳에 대한 파일 기반의 오프셋이다. 프로그램이 PE나 COFF 파일을 스스로 메모리 맵하려고 한다면(운영체제가 그것을 로드하는 대신에), 이 필드는 VIrtualAddress 필드보다 더 중요하다. 이 상황에서는 완전히 선형적인 파일 맵핑이 이루어지게 된다. 따라서 섹션 데이터를 VritaulAddress 필드가 가리키는 RVA에서 보다는 이 필드가 가리키는 오프셋에서 찾을 수 있다.
DWORD PointerToRelocations
OBJ 파일에서, 이것은 이 섹션에 대한 재배치 정보를 가리키는 파일 기반의 오프셋이다. 각각의 OBJ 섹션에 대한 재배치 정보는 해당 섹션에 대한 바이너리 데이터 바로 뒤에 따라온다. EXE 파일에서, 이 필드는 (아래에 나오는 필드또한) 의미가 없으며, 0으로 설정된다. 링커가 EXE파일을 만들때, 베이스 주소 재배치와 로드 타임에 해결되어야 할 임포트된 함수를 남겨둔 나머지 대부분의 수정 사항을 모두 수정한다. 베이스 주소 재배치와 임포트된 함수와 관련된 정보는 각각의 섹션에 저장되게 된다. 따라서 EXE 파일이 바이너리 섹션 데이터에 따라 나오는 섹션별 재배치 데이터를 가질 필요가 없는 것이다.
DWORD PointerToLinenumbers
이것은 라인 번호 테이블의 파일 기반 오프셋이다. 라인 번호 테이블은 소스 파일의 라인 번호와 주어진 라인에 대해서 생성된 코드의 주소를 서로 연관시킨다. CodeView 포맷과 같은 현대의 디버그 포맷에서는, 라인 번호 정보는 디버그 정보의 일부로 저장된다. 그러나 COFF 디버그 포맷에서는 라인 번호 정보는 심볼 이름/타입 정보와 분리되어서 저장된다. 일반적으로 단지 코드 섹션(.text같은)만이 라인 번호를 가진다. EXE 파일에서는, 라인 번호는 섹션 바이너리 데이터 다음부터 파일 끝까지 수집되게 된다. OBJ 파일에서는, 섹션의 라인 번호 테이블은 바이너리 섹션 데이터와 섹션 재배치 테이블 다음에 오게 된다.
WORD NumberOfRelocations
섹션에 포함된 재배치 테이블(위의 PointerToRelocations 필드) 안의 재배치 갯수. 이 필드는 단지 OBJ 파일에서만 유효한 것으로 보인다.
WORD NumberOfLinenumbers
섹션 라인 번호 테이블(위에서 말한 PointerToLinenumbers)에서 라인 번호 갯수
DWORD Characteristics
대부분의 프로그래머가 플래그라 부르는 것을 COFF/PE 포맷에서는 속성(characteristics)라 부른다. 이 필드는 섹션 속성과 관련된 플래그 집합(코드/데이터냐?, 읽기가능? 쓰기가능?)이다. 모든 가능한 섹션 속성은 WNNT.H에서 IMAGE_SCN_XXX_XXX형태로 #define 된 것으로 찾을 수 있다. 몇가지 중요한 속성으로 다음과 같은 것들이 있다.
0x00000020 이 섹션은 코드를 포함하고 있다. 일반적으로 실행 가능 플래그(0x80000000)와 결합해서 사용된다.
0x00000040 이 섹션은 초기화된 데이터를 포함하고 있다. 실행 가능하지않은 거의 대부분의 섹션과 .bss 섹션은 이 플래그가 설정되어져 있다.
0x00000080 이 섹션은 초기화되지 않은 데이터를 포함하고 있다(예를들면 .bss 섹션).
0x00000200 이 섹션은 코멘트나 다른 타입의 정보를 포함하고 있다. 이 플래그를 사용하는 일반적인 섹션은 컴파일러에 의해서 선언된 .drective 섹션이다. .directive 섹션은 링커를 위한 커맨드를 포함하고 있다.
0x00000800 이 섹션의 내용은 EXE 파일에는 담겨서는 안된다. 이 섹션들은 컴파일러와/어셈블러가 링커에게 정보를 전달하기 위해서 사용한다.
0x02000000 이 섹션은 프로세스가 한번 로드되고 나면 더 이상 필요하지 않기 때문에 무시될 수 있다. 대부분의 무시될 수 있는 것은 베이스 재배치(.reloc) 섹션이다.
0x10000000 이 섹션은 공유가능하다. DLL과 함께 사용될때, 이 섹션에 포함된 데이터는 DLL을 사용하는 프로세스 간에 공유될 수 있다. 데이터 섹션의 기본값은 공유하지 않는(nonshared) 이다. DLL을 사용하는 각각의 프로세스는 섹션에 포함된 데이터의 자신만의 복사본을 가진다는 의미이다. 좀더 기술적인 용어로 설명하면, 공유 섹션은 메모리 매니저에게 이 섹션의 페이지 맵핑을 DLL을 사용하는 모든 프로세스가 메모리내의 동일한 물리적 페이지를 참조하도록 설정하는 것이다. 섹션을 공유가능하도록 만들기 위해서, SHARED 속성을 링크 타임에 사용할 수 있다. 예를들면,
LINK /SECTION:MYDATA,RWS ...
링커에게 MYDATA라는 섹션은 읽기, 쓰기, 공유가 가능하다고 알려주는 것이다.
0x20000000 이 섹션은 실행가능하다. 이 플래그는 보통 "코드 포함" 플래그 (0x00000020)가 설정될때마다 설정된다.
0x40000000 이 섹션은 읽기 가능하다. 이 플래는 EXE 파일내에 포함된 거의 모든 섹션에 대해서 설정된다.
0x80000000 이 섹션은 쓰기 가능하다. 만약 EXE 파일의 섹션에 이 플래그가 설정되어 있지 않다면, 로더는 메모리 맵 페이지를 읽기 전용 내지는 실행 전용으로 마크한다. 일반적으로 이 속성을 가진 섹션은 .data와 .bss다. 흥미롭게도, .idata 섹션은 또한 이 속성을 설정하고 있다.
또한 PE 포맷에서 제외된것으로 페이지 테이블이 있다. LX 포맷의 IMAGE_SECTION_HEADER에 대한 OS/2의 동치는 파일에서 발견될 수 있는 코드나 데이터 섹션이 있는 곳을 직접적으로 가리키진 않는다. 대신에 그것은 섹션내에 있는 페이지의 구체적인 범위의 위치와 속성을 가지고 있는 페이지 룩업 테이블을 참조한다. PE 포맷은 그러한 모든 것들을 하지않아도 되게 한다. 그리고 섹션 데이터가 파일내에서 연속적으로 저장되도록 보장한다. 두가지 포맷에서, LX가 취하는 방법은 유연성을 허용한다. 그러나 PE 스타일은 작업하기가 더욱 간단하고 쉽다. 두가지 포맷을 위한 파일 덤퍼를 작성해야 할때, 나는 이것을 보장할 수 있다.
PE 포맷에서 좋아진 또다른 점으로 저장된 아이템의 위치가 간단하게 DWORD 오프셋이라는 것을 들 수 있다. NE 포맷에서는, 거의 대부분의 위치는 섹터 값으로 저장되어 진다. 실제 오프셋을 찾기 위해서 첫째로 NE 헤더의 정렬 유닛 크기를 보고 그것을 섹터 크기로 변환해야 한다. (일반적으로 16내지는 512 바이트) 그리고 나서는 실제 파일 오프셋을 구하기 위해서 섹터 크기에 지정된 섹터 오프셋을 곱할 필요가 있다. NE 파일에서 섹터 오프셋으로 저장되어져 있지 않은 어떤것이 있다면, 그것은 아마도 NE 헤더부터의 상대 오프셋으로 저장된 것이다. NE 헤더가 파일의 시작 부분에 있지 않기 때문에, 코드에서 NE 헤더의 파일 오프셋을 사용해야 할 필요가 있다. 대체로, PE 포맷은 NE, LX내지는 LE 포맷보다 다루기가 쉽다(메모리 맵 파일을 사용할 수 있다는 가정하에서)