x0FANSA-hk 님의 블로그

MJSEC) 4주차 - PE File format 개념 본문

MJSEC

MJSEC) 4주차 - PE File format 개념

x0fansa-hk 2026. 4. 30. 10:37

우선 제가 만든 프로그램입니다. 이것을 아마 자주 참조하게 될 것입니다. 만약 소스코드가 궁금하시면 이 글 맨 아래에 제가 올린 링크를 확인해 주세요.

 

갑니다.


1. PE파일이란?

 Windows운영체제에서만 단독으로 실행되는 파일형식입니다. DOS, NT환경 둘다에서 운영체제와 cpu가 받쳐준다면 사용가능합니다. 이러한 특징으로 운영체제에서 바로 실행되어야 하는 구조이기에 대부분 바이너리 형태입니다. (그래서 그런가 저희 동아리의 리버싱 시간에서 다룹니다. 이 글이 그 증거!)
 대표적으로 아래와 같은 형식들의 파일이 있습니다.

종류 주요 확장자
실행 계열 EXE, SCR
라이브러리 계열 DLL, OCX, CPL, DRV
드라이버 계열 SYS, VXD
오브젝트 파일 계열 OBJ

2. PE파일의 구조

크게 두가지로 나뉘게 됩니다. Header와 Body로요. 각각을 쉽게말하자면 목차와 데이터입니다.

 

I) Header: 우리가 나무X키같은 위키나 백과사전등을 읽으면 대부분의 문서가 맨 앞에 목차와 개요 등 간략한 정보가 있듯 PE파일도 이러한 정보를 담는 구조가 있습니다. 이러한 Header도 여러개가 있는데 헤더의 경우 (일종의 '나 windows 전용 프로그램이요.'인증 역할이기 때문에 일부분 생략가능한 Body과 다르게)반드시 전부 있어야 하는 필수요소 입니다. 이것들에 대해서는 아래에서 더 자세하게 다루겠습니다.

 

II) Body: 실제 데이터가 들어있는 곳 입니다. 여기에 프로그램을 구성하는 실제 내용물들이 용도별로 분류(Section)되어 있는 것이지요.

.text: 실행되는 기계어 코드가 들어 있는 공간입니다.
.data: 초기화된 전역 변수 등이 담겨 있습니다.
.rdata: 읽기 전용 데이터(문자열 상수 등)와 가져오기 정보(IAT)가 위치합니다.
.reloc: 재배치 정보가 들어 있어, 파일이 기본 주소에 로드되지 못할 때 사용됩니다.


3. PE Header

구체적으로 PE헤더에 대한 글을 쓰기 전에 우리는 PE헤더의 구조를 알아야 이해하기가 쉽습니다. PE라는 형식은 windows에서 제공하기에 windows운영체제에서 PE헤더의 구조를 저희는 알 수 있습니다. 이때 이 구조는 windows의 시스템 파일 중 winnt.h에 C언어의 typedef 구조체 형식으로 새로운 자료형으로 선언되어 있습니다. 여러분들이 현재 windows 10 이상의 환경을 사용하고 있다면 다음 경로에서 winnt.h의 파일을 얻어올 수 있습니다.(저작권 문제 상으로 여기에는 올리지 않았습니다.)

C:\Program Files (x86)\Windows Kits\10\Include\"빌드번호 - 기기마다 다름"\um\winnt.h

물론 이것을 직접 찾을려면 2000개가 넘는 파일을 파해쳐 나가야 하므로 제가직접 사용자의 C드라이브의 windows에서 winnt파일을 바탕화면으로  복사해오는 프로그램을 만들었습니다. 원클릭으로 얻을 수 있을 거에요. 

더보기
#include <stdio.h>
#include <windows.h>
#include <shlobj.h>
/*MAX_PATH is defined 260 by windows*/

int main() {
    const char* baseDir="C:\\Program Files (x86)\\Windows Kits\\10\\Include\\*";
    char latestVersion[MAX_PATH]="";
    char sourcePath[MAX_PATH];
    char destPath[MAX_PATH];
    char desktopPath[MAX_PATH];
    WIN32_FIND_DATAA findData;
    HANDLE hFind=FindFirstFileA(baseDir, &findData);
    if (hFind==INVALID_HANDLE_VALUE) {printf("SDK is not found!\n"); return -1;}
	next_entry:
    if ((findData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)&&(findData.cFileName[0]!='.')) {
        if (strcmp(findData.cFileName,latestVersion)>0) strcpy_s(latestVersion, MAX_PATH, findData.cFileName);}
    if (FindNextFileA(hFind, &findData)) goto next_entry
    ;
	FindClose(hFind);
    if (strlen(latestVersion)==0) {printf("Version folder is not found!\n"); return -1;}
    SHGetFolderPathA(NULL, CSIDL_DESKTOP, NULL, 0, desktopPath);
    sprintf_s(sourcePath, MAX_PATH, "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\%s\\um\\winnt.h", latestVersion);
    sprintf_s(destPath, MAX_PATH, "%s\\winnt.h", desktopPath);
    printf("Latest Version folder: %s\n", latestVersion);
    if (CopyFileA(sourcePath, destPath, FALSE)) printf("ADRRESS: %s\nCopy Complete! Please Check Desktop folder.\n",sourcePath);
    else printf("Fail to Copy! Error Code: %lu\n", GetLastError());
    system("pause");
    return 0;
}

 

내가만듬!

winnt.h_Extract.c
0.00MB

 

그러면 여기 아래부터는 해당 헤더(winnt.h)가 있다고 가정을 하고 글을 작성하겠습니다.


winnt.h의 파일안에는 수많은 구조체로 정의된 자료형들이 있습니다. ctrl+F를 사용하여 자료형을 입력하면 저희가 원하는 헤더의 구조체를 찾을 수 있겠죠? 그중에서 오늘 여기서는 아래의 6개의 헤더들을 다룰 것입니다.

헤더이름 정의된 구조체 자료형 주목해야 할 멤버
DOS Header IMAGE_DOS_HEADER e_magic, e_lfanew
DOS Stub 구조체 형태가 아닙니다! (후술) -
NT Header IMAGE_NT_HEADERS Signature, FileHeader, OptionalHeader
File Header IMAGE_FILE_HEADER Machine, NumberOfSections, Characteristics
Optional Header IMAGE_OPTIONAL_HEADER Magic, AddressOfEntryPoint, ImageBase, SectionAlignment, FileAlignment, SizeOfImage, Subsystem, DataDirectory
DataDirectory IMAGE_DATA_DIRECTORY Index: 0, 1, 2, 3, 5, 9, 12

그러면 하나씩 시작해봅시다. 예시를 참고하실려면 글 최상단 exe파일을 HxD로 참고하며 보는것을 권장드립니다. (참고: 제 컴퓨터가 64비트이기에 구조체가 32비트, 64비트가 별도로 있다면 64비트 우선으로 적었습니다.)


3.1. DOS Header

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;

이 파일이 MS-DOS에서 실행 가능한 파일임을 알리고, 다음 PE헤더 구조체의 시작지점을 가르키는 헤더입니다. 특히 옛날 DOS가 주력이던 시절에는 DOS Header가 재배치 테이블, 스택 세팅, 전반적인 파일 구조를 모두 담는 등 매우 중요한 역할을 하였습니다.

 

그러나 좀 이상하죠? 저희가 NT환경에서 PE파일을 쓴다고 했는데... 이것은 그런고로 NT를 위한 세팅이 아닙니다. 만약 저희가 exe파일을 DOS환경에서 돌린다고 가정을 해 봅시다. 그러나 DOS의 경우 NT와 명령체계가 다르지만 실행파일의 확장자는 같고 결정적으로 시기상 약 40년전의 알고리즘이기에 상위호환을 아무리 예측해도 불가능합니다. 그러나 DOS환경에서도 exe가 실행파일입니다. 그러므로 만약 NT의 exe파일을 DOS에서 실행시켰으면 운좋으면 정상동작이 되지 않아 운영체제 선에서 멈추겠지만 운이 나쁘다면 이상하게 실행되어서 시스템이 큰일날 수도 있게 되는것이지요. 그래서 일단 DOS Header를 만들되 DOS환경에서는 실행하지 못하게 하기 위한 것입니다. 즉 NT환경 입장에서는 MZ시그니쳐 확인 후 e_lfanew의 주소로 점프하는 용도 그 이상 그 이하도 아닌 것이지요.

 

여기서 저희가 주목해야 할 멤버는 다음과 같습니다.

I) e_magic: 대부분의 경우 "MZ"라는 문자열로 하드코딩되어있습니다. OS로더가 프로그램을 처음 실행하고 프로그램카운터가 맨 처음 프로그램의 메모리를 가르킬때 이 "MZ"라는 시그니처를 읽어서 "이 프로그램은 우리 운영체제에서 실행이 가능하겠구나"라는 준비를 할 수 있게 해 줍니다. 다시말해서 만약 맨 처음 2바이트가 "MZ"(아스키코드로 4D 5A)가 아니라면 이 프로그램을 일반데이터로 인식하고(PE파일로 인식하지 않고) 실행 자체를 거부합니다. (그만큼 매우매우 중요하다~~!)

II) e_lfanew: 다음다음에 소개할 NT Header의 위치를 가르킵니다. 사실 정상적으로는 다음에 소개되는 DOS Stub으로 갈 일이 없기에 바로 NT Header로 점프하는 코드 흐름을 맡는 멤버이지요.

 

자료형의 크기를 본다면 16개의 WORD, i=4의 WORD배열, i=10의 WORD배열, 1개의 LONG으로 WORD가 2바이트, LONG이 4바이트이기에 총 64바이트 입니다.(참고: 제가 이 글 최상단에 접힌글로 소스코드랑 실행화면을 올렸습니다. 여기서 IMAGE_DOS_HEADER형식의 변수를 하나 만들고 sizeof를 했는데 그곳에서도 64라고 출력된 점을 알 수 있습니다. 다른 헤더들도 마찬가지니까 참고하시면 좋을거에요.)

 

곰곰히 생각해 보신다면 이 구조체의 변수가 맨 처음 딱 하나 생기고 그 변수의 크기는 64바이트로 고정되어 있습니다. 다시말해서 PE파일의 맨 처음 64바이트는 정해진 형식에 맞게 고정되어있습니다. 그러므로 저희는 포인터 연산으로 필요한 값만 추출할 수도 있고 특정 멤버가 어디 주소에 있는지도 알 수 있습니다. (가령 e_lfanew의 경우 항상 0x3C에 위치합니다.)


3.2.  DOS Stub

DOS Header 바로 뒤에 위치합니다. "This program cannot be run in DOS mode"라는 메세지가 저장되어있는 영역입니다. 옆 의 HxD스크린샷을 본다면 무슨 의미인지 아실 수 있습니다. 즉 문자열이 저장된 영역이지, 파일의 데이터를 관리하는 헤더는 아닙니다.

 

PE가 NT에서 작동하도록 만들어진 프로그램인데... 만.약.
case DOS: 프로그램 카운터에 의해 DOS Header가 끝나고 도달하게 됩니다. 이때 저장된 문자열이 읽히고 출력이 됩니다.
case NT: NT는 일단 MZ시그니처와 e_lfanew에 있는 주소에서 PE시그니처를 확인합니다. 이것이 정상적으로 확인되면 DOS Header에서 설명을 했듯 바로 PE시그니처로 점프해 버리기에 해당 문자열이 출력될 일 자체가 없습니다.


3.3.  NT Header

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

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

DOS Header가 DOS환경에서 매우 중요하듯 이름만 봐도 NT환경에서 매우 중요해 보이는 NT Header입니다. 실제로 파일에 담긴 전반적인 데이터들을 관리하기 위해 존재하는 중요한 헤더입니다. NT에서 DOS Header를 읽는다면 e_lfanew를 통해 이곳의 시작주소로 바로 점프합니다. NT Header 자체만 본다면 구조체가 매우 단순하게 생겼습니다. 특히 구조체에서 볼 수 있듯, "PE"라는 시그니처가 존재하는 4바이트 다음부터는 바로 FILE Header가 시작됨을 체크할 수 있습니다. 즉 "PE"이후 구조체 내 포인터 연산으로 FILE Header와 Optional Header 둘다 접근이 가능한 것이지요.

 

멤버들을 살펴보겠습니다.

I) Signature: (DOS Header의 e_magic처럼)NT Header가 시작됨을 알리는 4바이트 문자열 입니다. 이곳에 "PE"가 기록 되있다면(아스키코드로는 50 45 00 00) 정상적인 NT Header라고 생각하고 읽기 시작합니다.

II) IMAGE_FILE_HEADER: 물리적인 파일의 특성을 담습니다. 자세한 것은 후술하겠습니다.

III) IMAGE_OPTIONAL_HEADER: 메모리 로딩 시 필요한 상세 설정(논리적 구조)을 담습니다. 역시 자세한 것은 후술하겠습니다.

 

맨 처음 이미지상으로는 264바이트라고 합니다. 이것은 DWORD 1개가 차지하는 4바이트를 제외하면 260바이트가 IMAGE_FILE_HEADER와 IMAGE_OPTIONAL_HEADER64로 차지된다고 볼 수 있겠죠?


3.4. File Header

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;

이 파일이 어떤 CPU에서 돌아가는지, 섹션은 몇개인지 등 파일이 하드웨어로 해석될 때 필요한 정보들를 담고 있는 헤더입니다.

 

IMAGE_FILE_HEADER에서 중요하게 볼 것들은 아래와 같습니다.

I) Machine: 이 파일이 어떤 CPU에서 실행가능하도록 설계되었는지를 담고 있습니다.

II) NumberOfSections: PE파일은 Body부분에 여러 종류의 Section을 가지고 있습니다. 이때 이 Section의 종류가 몇가지인지를 담습니다. 이게 단순히 담는것으로 끝나는 것이 아니라 Section의 갯수를 알게되어 각 섹션의 헤더가 몇개인지를 알 수 있고 이것으로 포인터 곱연산으로 Section Table이 얼마나 길어질지를 예측하고 접근할 수 있습니다.

III) Characteristics: 파일의 속성을 나타냅니다. 예를들어 여기가 0x0002라면 exe파일로 바이너리를 해석하고, 0x2000이면 dll파일로 함수 참조를 진행하게 됩니다. 

 

4개의 WORD, 3개의 DWORD로 이루어진 구조체 입니다. WORD가 2바이트, DWORD가 4바이트이기에 총 20바이트 입니다.


3.5. Optional Header

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_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

파일이 메모리에 로드될 때 필요한 주소값, 크기, 진입점 등의 정보를 담고 있습니다. 따라서 다양한 형태의 포인터를 다른 자료형에 담아놓은 것 이라고 이해를 한다면 쉽습니다.

 

역시 이번에도 중요한 파라미터들을 아래에 정리하였습니다.

Magic: 32비트(10B)인지 64비트(20B) 파일인지 구분합니다.

AddressOfEntryPoint: 프로그램이 실행될 때 가장 먼저 시작되는 지점의 주소(EP)입니다.

ImageBase: 파일이 메모리에 로딩되길 원하는 가상 메모리 시작 주소입니다.

SectionAlignment: 메모리 상에서 각 섹션이 배치되는 최소 단위(배수)입니다.

FileAlignment: 디스크 상에서 각 섹션이 저장되는 최소 단위입니다.

SizeOfImage: 파일이 메모리에 로딩되었을 때 차지하는 전체 크기입니다.

Subsystem: 프로그램의 종류를 구분합니다. (1: Driver, 2: GUI, 3: CUI)

DataDirectory: Export/Import 테이블 등 중요한 정보들이 위치한 배열입니다.

 

현재 제가 사용중인 64비트를 기준으로 자료형의 크기를 계산해 봅시다. 그러면...

2개의 BYTE,

9개의 WORD,

13개의 DWORD,

5개의 ULONGLONG,

i=IMAGE_NUMVEROF_DIRECTORY_ENTRIES의 IMAGE_DATA_DIRECTORY배열 (어휴 겁나 많네)

로 이루어진 구조체 입니다. BYTE가 1바이트, WORD가 2바이트, DWORD가 4바이트, ULONGLONG이 8바이트, IMAGE_DATA_DIRECTORY구조체(밑에 나와요)가 8바이트이며, IMAGE_NUMVEROF_DIRECTORY_ENTRIES은 windows에서 16으로 정의된 상수이기에 이 구조체는 총 240바이트 입니다.

그러면 저희가 아까 전 NT Header에서 예상한것 처럼 IMAGE_FILE_HEADER와 IMAGE_OPTIONAL_HEADER64가 각각 20바이트, 240바이트로 총 260바이트를 만족함을 증명할 수 있습니다.


3.6. DataDirectory

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

방금 소개했던 DataDirectory배열의 구조체 입니다. PE파일이 실행되는데 핵심 정보들이 어디에(VirtualAddress) 얼마나 크게(Size) 저장되어있냐를 보여주는 것이죠.

각 파라미터를 해석하면...

I) VirtualAddress: 해당 데이터가 시작되는 상대적 가상주소(RVA)입니다.

II) Size: 해당 데이터 구조의 크기입니다.

입니다. 즉 VirtualAddress에 Size만큼 포인터 합 연산을 한다면 해당 데이터의 맨 끝으로 이동할 수 있습니다.

 

그런데 Optional Header안에는 이것이 무려 16개나 선언되었었습니다. 그 이유는 PE파일을 크게 16파트로 나누어서 각 인덱스마다 고유한 역할을 하는 부분을 가르키기 때문입니다. 이중 특히 중요한 인덱스들은 아래와 같습니다.

INDEX 이름 설명
0 Export Directory 외부로 노출하는 함수 정보 (주로 DLL 파일에서 중요)
1 Import Directory 실행을 위해 외부에서 가져오는 함수 목록 (IAT 관련)
2 Resource Directory 아이콘, 메뉴, 다이얼로그 등 리소스 위치
3 Exception Directory 예외 처리 관련 정보
5 Relocation Directory 재배치(Relocation) 정보 (ASLR 작동 시 필수)
9 TLS Directory Thread Local Storage 관련 정보
12 Import Address Table 실제 함수 주소가 채워지는 IAT의 시작점

 

구조체 형태를 본다면 2개의 DWORD로 이루어진, 총 8바이트의 단순한 자료형 입니다.


4. PE Body

지금까지 소개한 Header가 파일이 잘 동작하기 위한 정보들을 담은 것이라면 Body의 경우 파일이 그래서 실제로 무엇을 할 것인지를 담는 실질적 내용이 있는 부분입니다. 그렇기에 Body는 파일에 따라 크기가 천차만별이어서.. 정형화되어있고, 최대 크기가 예측가능하고, 정보만 있으면 포인터연산이 쉬운 Header와 다르게 Body는 가변적이고, 포인터 연산이 더 복잡하기에(고려해야할 요소가 많아져요) Header의 정보를 많이 참조해서 접근해야 합니다.


5. Linking

우리가 C언어를 하다보면 라이브러리를 대부분의 경우 참조를 합니다. 하다못해 기본적으로 하는 printf("Hello, World!");조차 stdio.h를 사용해야 합니다. 그러나 이를 컴파일하여 실행파일로 만든다면 대부분의 운영체제서는 .h나 .c같은 라이브러리는 필요가 없습니다. 전처리기와 링커가 컴파일시 필요한 부분만 포함시켜주기 때문이죠. 이때 포함하는 방법에 따라 두가지로 나뉘게 됩니다.

정적링크: 라이브러리의 기계어 코드를 실행파일안에 아예 내장시켜버립니다. 덕분에 단일파일 하나로 어디서든 같은 기능을 단독실행할수 있게 합니다. 이것을 효율적으로 사용한다면 좋은 스탠드얼론 바이너리를 만들수도 있겠죠?

동적링크: 사실 현대의 대부분의 OS는 기본적으로 동적링크로 설계됩니다. 만약 모든 요소를 정적링크를 한다면 파일 자체의 크기가 매우 커지게 되고, 분석이 어려워지며(그냥 많아져요), 그나마 운영체제가 유동적으로 대응가능한 동적링크와 다르게 하드웨어에 따른 차이가 심해지게 되기 때문이죠.

 

이러한 이유로 저희가 프로그램 소스를 만들고 컴파일을 한다면 특수한 경우가 아닌이상 거의 동적링크로 결과물이 나오게 된다고 생각하셔도 됩니다. 제가 갑자기 이 이야기를 하게된 이유는 이때 동적링크를 하기 위해서는 함수가 어떤 기능인지는 몰라도 되지만 최소한 어떻게 생겼는지 이름이 뭐인지는 프로그램측에서 알아야 합니다. 마치 C언어에서 extern을 하거나 프로토타입을 만드는 것 처럼 말이죠. 그렇기에 PE에서는 아래와 같은 3가지 요소를 사용하게 됩니다.

 

I) INT: 프로그램이 실행되기 위해 필요한 외부 함수의 이름들이 나열된 목록입니다. 
II) IAT: 실제로 호출될 함수의 메모리 주소가 저장되는 목록입니다. 

 

★ 여기서 구분을 잘 해야 하는것이 있습니다. 프로그램을 실행을 하던 말던 일단 만들었다면 INT는 해당 프로그램을 동작하기 위한 함수 이름을 가지고 있습니다. 이것은 실행 전/후 변하지 않는 상수값이죠. 그러나, IAT는 다릅니다. IAT는 프로그램을 실행하기 전에는, 즉 어쩌면 만들기만 한 상태에서는 IAT는 비어있거나 의미없는 값을 가지고 있습니다. 그러다 프로그램이 실행이 된다면 INT에 있는 함수를 운영체제에서 찾아서 어디있는지 그 위치를 저장합니다. 즉, IAT는 실행전과 실행후가 다른 변수인 것이지요. ★

 

III) EAT: IAT랑 반대입니다. 해당 파일에 어떤 함수들이 있고 이것들의 주소는 이 파일의 여기어딘가 라는 주소를 알려줍니다. IAT가 INT를 참고하여 일치하는 함수의 주소를 EAT에서 얻어온다고 보면 되는 것입니다. 이러한 특성으로 주로 DLL에서 쓰이게 됩니다.

 

INT, IAT, EAT의 실제 데이터는 Body의 Section에 있기에 바로 찾기는 어려우며, 데이터 자체가 비교적 큽니다. 그렇기에 저희는 

Header를 통해서 접근을 합니다. 이전에 저희가 DataDirectory의 배열을 간략하게 다루었습니다. 여기중 특정 index에 이것들의 주소가 있습니다.

DataDirectory[0]: EAT가 있는곳의 주소 → 이곳에 가면 IMAGE_EXPORT_DIRECTORY라는 구조체가 있습니다. 여기에는 함수 이름과 번호가 구조체로 있습니다  INT에 있는 함수 이름을 로더가 읽어서 DLL의 EAT에 있는 함수이름배열과 이름과 대조하여 동일한 이름의 인덱스 번호를 확보합니다. → 이때 얻은 번호와 같은 인덱스의 AddressOfNameOrdinals 배열의 원소의 값을 가져옵니다. → 다시 이 값을 AddressOfFunctions배열의 해당 인덱스를 참조하여 해당 주소로 넘어가면 함수의 진짜 기능이 정의되어있습니다.

DataDirectory[1]: INT가 있는곳의 주소 → 이곳에 가면 IMAGE_IMPORT_DESCRIPTOR라는 구조체가 있습니다. 여기에서 다시 한번 INT와 IAT를 주소로 넘어간다면 진짜 데이터를 찾을 수 있습니다.

DataDirectory[12]: IAT가 있는곳의 주소, 실행의 효율성을 높이기 위해 IAT영역의 주소를 통째로 파악한 후 저장하는 지름길 같은것 입니다.

 


6. File과 Memory의 주소

File은 변하는 경우가 거의 없고 고정되있다고 봐도 무방합니다. 그렇기에 효율적으로 정형화되어 Header와 Body가 잘 정렬되어 있습니다. 문제는 만약 이 파일을 실행시킨다면 Memory에 올리게 되는데 Memory의 상황에 따라 시작되는 주소 자체가 (거의 대부분)바뀌고 각 Header나 Section사이의 NULL만 있는 구간(NULL패딩)이 바뀌게 됩니다. 그러므로 File과 Memory를 자유롭게 오가기 위해서는 File을 오프셋으로 잡고 포인터연산을 하는 방법이 가장 선호됩니다.

 


 

 

내용이 너무 길어져서 오늘 내용을 예시를 가지고 해석해 보는것은 다른 글에 쓰게될거 같아요. 아래 주소를 참고해주시면 됩니다. 감사합니다.

https://x0fansa-hk.tistory.com/17 

'MJSEC' 카테고리의 다른 글

MJSEC) 4주차 - PE File format 예제  (0) 2026.05.01
MJSEC) 3주차 - 스택프레임 / 함수호출규약  (0) 2026.04.30
MJSEC) 1-2주차 통합 과제  (0) 2026.04.30
MJSEC) 신입 부원 과제  (0) 2026.04.30