PE 파일 포맷

지금 BaramOS는 커널을 Visual studio로 컴파일 하기 때문에 파일 포맷이 PE 포맷으로 생성된다. 그.래.서 또 PE 포맷이 어떻게 생겼는지를 확인해 봐야 한다. 역시나 쉬운게 없다. OS 만들기가 이리 힘들줄이야~ 여차 하면 컴파일러도(허~~세 작렬) 만들어 볼 생각인데 아마 더 죽어 버릴 듯하다. 그냥 컴파일러는 포기!!! 이거 하나 끝내기 힘드네..

여튼. PE 포맷 요놈이 어떻게 생겼는지를 알아야 한다. 그럼 PE 파일이 뭐냐~?~~

PE(Portable Executable) 포맷은 Windows 에서 만든 포맷으로 유닉스의 COFF(Common Object file format)을 기반으로 나왔다. 다양한 운영체제에서의 이식성을 보여준다는 뜻에서 이식이 가능한 실행 형식(Portable Executable)이라는 이름을 붙였다.                           - 위키백과

이식이 가능한 실행 형식이라는데 뭘 이식 한다는 건진 모르겠다. 찾아보니 파일을 다른곳에서 실행해도 실행이 될 수 있도록 규정한 형식이란다. 예전에는 파일이 만든 곳에서만 실행 되었나??? 흠.. 모르겠다. 패스~

갑자기 실행파일 이란 무엇일까 라는 의문이 들었다. 그냥 막연하게 프로그램을 실행시킬 수 있는 파일 이라고 머리속에서 떠올랐다. 그런데 이게 어떤 원리로?? (또 생각에 꼬리를 물기 시작한다 ㅡㅡ;;;)

나름 실행파일 이란 정의를 내려봤다. CPU가 연산, 제어 할수 있는 코드들을 파일안에 모아 놓고 또 연산, 제어만 하다 보니 데이터들이 필요 했을 것이다. 이 데이터들도 파일 안에 모아 놓았을 것이고.. 그럼 실행파일은 CPU 실행 코드와 데이터 코드들을 모아 놓은 파일. 이게 실행파일 이지 않을까 하고 생각해 봤다… 그런데 왜 굳이 PE포맷으로 실행파일들을 구성해 놨을까??? 음..동적 라이브러리 때문에??? 흠.. 실행파일 포맷이 만들어진 이유는 다음에 좀더 찾아 봐야 겠다. 너무 멀리 가는것 같으니깐…

PE 파일 포맷이라고 인터넷에서 검색해 보니 많은 글들이 찾아 진다. 그리고 자세히 분석 해 놓은 곳도 많다. PE파일에 관한 자료 찾는건 어렵지 않아 보인다. (아싸~) ㅎㅎ

파일 구조는

IMAGE_DOS_HEADER

IMAGE_NT_HEADER

IMAGE_SECTION_HEADER

.TEXT SECTION

DATA SECTION

IMAGE_DOS_HEADER 도스에서 쓰던 구조체란다. 총 64바이트로 구성되어 있단다.

typedef struct _IMAGE_DOS_HEADER {     // DOS .EXE header
    WORD   e_magic;                  // Magic number
    WORD   e_cblp;                    // Bytes on last page of file
    WORD   e_cp;                       // Pages in file
    WORD   e_crlc;                     // Relocations
    WORD   e_cparhdr;               // Size of header in paragraphs
    WORD   e_minalloc;              // Minimum extra paragraphs needed
    WORD   e_maxalloc;             // Maximum extra paragraphs needed
    WORD   e_ss;                      // Initial (relative) SS value
    WORD   e_sp;                      // Initial SP value
    WORD   e_csum;                 // Checksum
    WORD   e_ip;                       // Initial IP value
    WORD   e_cs;                      // Initial (relative) CS value
    WORD   e_lfarlc;                   // File address of relocation table
    WORD   e_ovno;                   // Overlay number
    WORD   e_res[4];                 // Reserved words
    WORD   e_oemid;                 // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;              // OEM information; e_oemid specific
    WORD   e_res2[10];             // Reserved words
    LONG   e_lfanew;                // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

이 실행파일 첫부분에 64바이트를 위와 같은 구조로 저장되어 있다.

여기서 필요한건 e_magic 과 e_lfanew 이 두 변수만 필요하단다.

e_magic 은 Dos Signature로 4D5A 기본값이 저장되어 있다. 이게 아스키 값으로 “MZ” 이라는데 만든 사람 Mark zbikowski 이니셜이라고 한다.

e_lfanew는 IMAGE_NT_HEADER 구조체 위치란다. ???  IMAGE_DOS_HEADER 64바이트 다음에 IMAGE_NT_HEADER 아닌가?? 라고 생각해서 검색해 봤더니 IMAGE_DOS_HEADER와 IMAGE_NT_HEADER 사이에 도스에서 실행되는 코드가 있다고 한다..DOS Stub 란다. 16비트 명령어로 구성되어 있어서 32비트 윈도우즈에서는 실행되지 않는단다. (윈도우즈 프로그램을 도스에서 실행시킬 때 “This is program cannot be run in DOS mode” 이렇게 화면에 출력하는 코드) e_lfanew는 파일에서 IMAGE_NT_HEADER가 있는 절대 위치를 가르킨단다.

윈도우즈 실행파일 로더는 프로그램의 첫 2바이트의 MZ 이니셜을 찾고 IMAGE_NT_HEADER로 건너 뛴단다.(물론 도스모드에서 실행하면 바로 에러 메시지 출력하고 종료…)

자 그럼 IMAGE_NT_HEADER 구조체..

이 구조체가 본격적으로 PE 구조체란다.. (아…귀찮다..)

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32

Signature는 단어 그대로 올바른 PE 포맷인지 확인하기 위한 Signature란다. 위에 IMAGE_DOS_HEADER의 첫 MZ와 마찬가지로 PE 포맷을 나타내는 PE00 (0x00004550)이 기록되어 있어야 한단다. 이게 없으면 실행한된단다.

IMAGE_FILE_HEADER는 현재 이 파일이 DLL(동적 라이브러리)인지 EXE 실행 파일인지 구분 여부와 실행될 플랫폼(CPU), 섹션등이 저장된 구조체 란다. (점점… 복잡하게 추적해야 할 구조체가 많아진다.. ㅡㅡ)

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine 은 어떤 CPU 에서 실행되는지 정보. 뭐 Intel 386인지, Intel64,MIPS, ALPHA 에 관한 정보가 기록된단다.

NumberOfSections은 단어 그대로 섹션의 갯수가 기록되어 있단다. 자. 그럼 섹션은 무엇일까??? 실행파일은 적어도 코드 와 데이터 이렇게 구분된단다. 이걸 코드와 데이터로 분류해서 섹션?이라는 이름으로 저장하는걸 말한단다. 기본적으로 코드(text), 데이터(data), 리소스(.rsrs) 이렇게 이름이 된다고 한다. 이게 프로그램이 크면 섹션이 많이 존재하게 되는데 이 섹션의 갯수를 말한단다.

TimeDataStamp는 파일이 생성된 날짜와 시간이 기록된단다.

PointerToSymbolTable, NumberOfSymbols 은 유닉스 시스템에서 사용하던 것으로 지금은 사용 안한단다.

SizeOfOptionHeader요놈은 IMAGE_OPTIONAL_HEADER32구조체의 크기이다. 배열도 아니고 포인터도 아닌데 왜 크기를 명시해야 한단다. 왜? 그냥 찾아보긴 했는데 명확한 곳은 없고 64비트 일 경우에 IMAGE_OPTIONAL_HEADER32구조체가 크기가 다르단다. (뭐, 64비트 일 경우 IMAGE_OPTIONAL_HEADER64 구조체를 사용할텐데….) 그래서 크기를 명시해 놔야 한다는데 맞는건진 모르겠다.

Characteristics 파일 속성 값으로 DLL 인지 실행파일인지 등을 정의 해 놓은 속성이란다.

자… IMAGE_FILE_HEADER 했으니 IMAGE_OPTIONAL_HEADER32 해야 겠네… 이 구조체는 PE 헤더 구조체중 제일 크기가 크단다. 이 말은 알아야 할게 많다는 말이지..흠..

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

많다….….

Magic 이 구조체가 32비트인지(IMAGE_OPTIONAL_HEADER32) 64비트인지 (IMAGE_OPTIONAL_HEADER64) 구분자란다. 32비트는 10Bh, 64비트는 20Bh .

MajorLinkerVersion, MinorLinkerVersion 컴파일러 버전 정보.

sizeOfCode는 코드 영역에 전체 크기를 말한단다.

sizeOfInitializedData 초기화 된 데이터 영역 전체 크기를 말한단다.

sizeOfUnInitializedData 초기화 되지 않은 데이터 영역 전체 크기를 말한다.

AddressOfEntryPoint는 Entry Point는 프로그램의 시작 위치이다. 그 시작 위치의 RVA값이란다. RVA(Relative Virtual Address)로 상대적 가상 주소를 말한다. 상대적 가상 주소.. 단어 어렵네… 가상의 주소 인데 상대적이란다. 뭐에 대한 상대적??? 요 밑에서 언급 될 ImageBase의 상대적 가상 주소 란다. 즉 ImageBase에서 AddressOfEntryPoint값 만큼 떨어진 위치에 Entry Point가 있다는 말이다. ImageBase에서 RVA만큼 떨어진.. 그럼 Offset 과 동일한 개념이라고 보면 되는건가? 음.

BaseOfCode 또한 RVA 값이란다. 이것도 ImageBase에서 RVA 값 만큼 위치에 Code의 시작 위치가 있다는 말인가 보다.

BaseOfData 위에 코드랑 똑같이 RVA 값.

ImageBase는 프로그램이 메모리에 적재될 위치를 가르킨단다. 보통 0x400000은 실행파일, 0x10000000은 DLL이란다. 물론 다른 값으로 지정될 수 있단다. 그러니까 메모리 로딩 시작 위치에서 RVA로 각 Entry Point, Code, Data위치를 기록하나 보다. (오프셋 개념인듯…). 아. DLL은 0x10000000인데 만약 여러 DLL들이 0x10000000로 ImageBase가 기록되어 있으면(미리 하나의 DLL이 메모리에 선점되어 있으면) 다른 메모리 주소로 PE 로더가 재배치 한단다.

SectionAlignment는 메모리에서 섹션(.TEXT, .DATA)의 최소 단위 크기란다. 이 크기보다 섹션의 크기가 커지면 이 섹션 크기 배수만큼 크기여야 한단다.

FileAlignment는 PE 파일 내에서 섹션의 최소 단위 크기.

뭐 기타 Version 정보는 건너 뛰고~~~ ^^

SizeOfImage는 PE 파일을 메모리에 올렸을 때 전체 크기를 말한단다. 파일 크기가 달라 질수 있는데 이건 SectionAlignment의 영향을 받는단다. (섹션 다음에 패딩이 붙는 다든지.. 등등)

SizeOfHeaders는 PE 포맷의 모든 헤더의 크기를 말한단다. 즉 파일 시작 위치에서 SizeOfHeaders 오프셋 뒤에 첫번째 섹션이 존재한단다. 이 SizeOfHeaders의 값은 FileAlignment값의 배수가 되어야 한단다.

Checksum 파일의 변경 유무 판단할 때 사용.

SubSystem은 파일이 드라이버 파일(1)인지 GUI 프로그램인지(2), CUI 프로그램인지를 나타내는 값.

SizeOfStackReserve 프로그램에서 사용할 스택 예약 크기.

SizeOfStackCommit 프로그램에서 사용할 스택 크기.

SizeOfHeapReserve 프로그램에서 사용할 힙 예약 크기.

SizeOfHeapCommit 프로그램에서 사용할 힙 크기.

LoaderFlags 사용하지 않음.

NumberOfRvaAndSizes 밑에 IMAGE_DATA_DIRECTORY구조체 배열 DataDirectory 크기.

DataDirectory 는…음.. IMAGE_DATA_DIRECTORY 구조체의 배열이다.

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

IMAGE_DATA_DIRECTORY구조체에는 VirtualAddress변수와 Size가 있다. 이게 Export, Import, Resource, Exception 등등의 위치와 크기를 정의하는 것이라는데.. 별로 커널쪽에선 중요하진 않을 것 같아 패쓰~

휴~~~

IMAGE_NT_HEADER 파일 뒤에는 섹션에 관련된 IMAGE_SECTION_HEADER가 따라 온다. 뭐 섹션 테이블이라고도 불리운다고 한단다.

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

IMAGE_SECTION_HEADER 구조체 데이터가 IMAGE_FILE_HEADER구조체의 NumberOfSections값만큼 있단다..(섹션의 갯수만큼..)

Name은 섹셕의 이름인데 NULL도 채워도 되나 보통 .text, .data, .rdata, .bss, .idata, .edata 등이 보통 관례대로 사용된단다.

VirtualSize 섹션이 메모리에 올라갈 때 섹션 크기. (SectionAlignment 배수)

VirtualAddress 섹션이 메모리에 올라 갈 메모리 위치. RVA 값이다. 즉 ImageBase + VirtualAddress .

SizeOfRawData 메모리가 아닌 실제 PE 파일내에서 섹션의 크기. (FileAlignment 배수)

PointerToRawData PE 파일내에서 섹션이 시작하는 위치(파일 오프셋).

PointerToRelocation obj 파일에서만 사용되고 실제 실행파일에서는 0 이 된단다.

PointerToLinenumbers COFF 스타일의 라인번호를 위한 파일 오프셋(??) . 모르겠다 이건.

NumberOfRelocation 요것도 0.

PointerToLinenumbers COFF 라인번호가 PE 에 첨부되었을 때만 사용.(???) 진짜 모르겠다.

Characteristics 섹션의 속성. (CODE 인지 DATA, 실행, 읽기, 쓰기) OR 연산.

     0x00000020 코드 섹션
     0x00000040 초기화 된 데이터 섹션
     0x00000080 초기화 되지 않은 데이터 섹션
     0x20000000 실행 가능한 섹션
     0x40000000 읽기 가능한 섹션
     0x80000000 쓰기 가능한 섹션.


자… 이제 필요한건 다 했다. IAT, EAT 이런것들은 아직… 필요 없다고 판단되었다. 필요 할때 다시 살펴볼 생각… IMAGE_SECTION_HEADER 다음에 실제 섹션 데이터들이 온단다. 뭐 이제 PE 포맷 구조에 대충 훑어 봤다. 휴~~~ 시간이 많이 걸렸다..(노느니라~~~)

정리가 필요하다….정리…

실행파일은 PE 포맷으로 구성되어 있다. 뭐 이런 저런 잡다한 것 다 빼고…

코드, 데이터 섹션 이런식의 섹션별로 실행 코드, 데이터를 분류해 놓았다.

이걸, IMAGE_NT_HEADER 구조체 안에 있는 IMAGE_OPTIONAL_HEADER 구조체의 ImageBase 값. 메모리에 실행파일을 로딩해 놓는다.

이 때, 각 섹션은 ImageBase + VirtualAddress(IMAGE_SECTION_HEADER 구조체 내) 의 RVA 위치에 섹션을 배치한다.

다시… 정리하면..

실행파일에서 ImageBase 값을 구하고 섹션의 갯수를 구한 다음. 각 섹션을 ImageBase + RVA 위치에 메모리에 올린다.

라고 정리가 된다. 흠…

 

'프로그래밍 > OS 만들기' 카테고리의 다른 글

OS 만들기 #14 PE 재배치  (0) 2014.03.28
OS 만들기 #13 보호모드 진입  (0) 2014.03.27
OS 만들기 #12 - PE 파일 포맷  (0) 2013.09.10
OS 만들기 #11 - 리얼모드 & 보호모드  (0) 2013.08.14
OS 만들기 #10  (0) 2013.08.14
OS 만들기 #9 - 부트로더  (0) 2013.08.14

+ Recent posts