2010년 4월 27일 화요일

Win32 실행파일 구조

2008/07/18 17:48
 

Win32 실행파일 구조

冊 이호동 지음, "Windows 시스템 실행파일의 구조와 원리", 한빛미디어 펴냄, 2005.5.26.


■ 기초 내용.

○ PE(Portable Executable) File format : Win32 운영체제가 탑재된 플랫폼이라면 어떤 플랫폼에서도 관계없이 실행되는 exe 또는 dll 파일. 보통 windows에서 실행파일 형식이라고 얘기하는 파일을 의미함.

○ 메모리 상에 로드된 하나의 Process = 하나의 EXE 파일 + 여러개의 관련 DLL 파일.

○ EXE와 DLL은 같은 파일 구조를 가지고 있다. 즉, PE file format이다. 여기에서는 EXE와 DLL을 'PE 파일'이라 칭하기로 한다.

○ PE 파일은 디스크에 파일형태로 존재하지만 사실 그 내용은 메모리 상에서 돌아가는 프로세스의 이미지이다. 즉, 메모리 상의 프로세스 이미지를 거의 그대로 파일 형태로 저장한 것이 PE파일이라는 것이다.


■ 메모리에서의 PE 구조

※ 메모리 내부의 'Win32 가상주소공간'에서의 구조
주소 내용물  
0번지 64KB 보호영역  
  Thread stack
 
  Process Heap  
0x00400000 .text

이 영역을 실행파일(PE)이미지 영역이라 함.


  .data
  .rdata
  .idata
  .didat
  .reloc
  .rsrc
  .tls, .debug... 기타 섹션
  ...중략...
0x10000000 DLL 및 .edata DLL도 PE구조이므로 각각의 .text와 .data를 가진다.
  ...중략...  
  64KB 보호영역  
0xFFFFFFFF
~ 0x80000000
상위 2GB : 커널영역  


■ 프로그램 관점의 메모리 구조와 PE구조

※ 메모리 내부의 'Win32 가상주소공간'에서의 구조
메모리 구조 메모리 주소 PE구조
Stack 영역

▶ 스택의 출입구는 SP; Stack Pointer 레지스터가 가리킨다.

▶ 지역변수, 매개변수, 함수들의 복귀주소 등이 담겨지는 곳.

0번지에서 64KB(보호영역)만큼 지난 부분부터 시작된다. 보통 Thread stack 이라는 명칭으로 실행시 생성된다. PE구조에서는 존재하지 않으며, 실행시 생성되는 영역이다.
Heap 영역

▶ new, malloc 등으로 동적인 메모리가 할당되는 영역.

Stack 영역에 바로 이어서 0x00400000 번지 이전까지 사용된다.

(즉, 약 4MB의 용량이 일반적으로 힙으로 사용가능한 용량이다.)

실행시에 메모리를 할당하게 되면 이 공간을 사용하게 된다. PE구조에서는 존재하지 않으며, 실행시 생성되는 영역이다.
코드 영역

▶ IP; Instruction Pointer 레지스터가 가리키는 곳.

0x00400000 번지부터 시작된다.

(실제로 PE가 시작되는 부분이다. 시작위치는 컴파일러 옵션을 사용해 변경 가능하다.)

.text 섹션이 이곳에 맵핑된다.

("MZ"문자열부터 시작해서 모두 메모리로 카피되어 맵핑된다.)

(DLL의 경우에는 .text가 0x10000000 번지에 맵핑된다.)

데이타 영역

▶ 전역/정적변수, 문자열 상수 등이 저장되는 곳.

PE구조가 계속 그대로 맵핑됨. .data와 .rdata 섹션 등이 이곳에 맵핑된다.

(.data 섹션은 읽기/쓰기 가능한 섹션이다. 단, 메모리 상에서만 그렇다. 파일 자체에 읽기 쓰기 하는 것이 아님.)

(.rdata 섹션은 readonly data 섹션, 즉 읽기 전용이다. 이 영역에는 상수로 선언된 것들과 리터럴들, 에러메시지들이 위치한다. 이 영역에 기록하려고 하면 프로그램은 예외를 발생시키며 종료되어 버린다.)

(.textbss 섹션은 초기화 되지 않는 데이타를 보관하는 섹션으로 디버깅 모드에서만 생성된다. 참고로 VC++6.0 이전에는 .bss 섹션이라는 이름으로 사용되었다)

... 이하 생략 ... 이하의 구조는 윗 그림을 참고할 것.  


■ 파일에서의 PE구조

※ 디스크에 있는 PE파일의 내부 구조
  (여기에서의 구조체는 Win32 SDK의 WinNT.h에 모두 정의되어 있다)
파일 내부 위치 내용물 설명
0번지~40바이트까지 IMAGE_DOS_HEADER 구조체
 
0x5A4D(="MZ"문자열)로 시작됨

참고로 MZ는 도스를 설계한 사람중의 한 사람인 Mark Zbikowski의 이니셜이다.

...... DOS 호환 더미 여기까지가 도스 호환을 위한 영역임.
  IMAGE_NT_HEADER 구조체 : PE파일의 정보를 나타냄. 자세한 내부 구조는 아래의 아기 표와 같음.
'PE 시그너쳐' 인 0x00004550(="PX\0\0" 문자열)로 시작함.
IMAGE_FILE_HEADER 구조체 (20바이트)
IMAGE_OPTIONAL_HEADER 구조체 (96바이트)
Data Directory 구조체 배열 (128바이트) : 기본필드들과 함께 주요 섹션들과 정보들의 위치와 크기를 나타내는 구조체 배열.

 
여기서부터가 진짜 PE구조의 시작.
  IMAGE_SECTION_HEADER 구조체 배열 : 보통 섹션 테이블이라고 칭함. 각 섹션들(.text, .data, .edata, .idata, .reloc...)의 실제 위치 정보를 담고 있는 파일포인터의 역할을 하고 있는 구조체 배열임. 세부구조는 아래의 아기 표와 같음.
IMAGE_SECTION_HEADER[0] -> .text 섹션을 포인팅
IMAGE_SECTION_HEADER[1] -> .data 섹션을 포인팅
IMAGE_SECTION_HEADER[2] -> .edata 섹션을 포인팅
......
 
이하 section data 영역. .text 실행코드를 담고 있는 영역. CPU 레지스터의 명령 포인터인 ip는 이 섹션 내에 존재하는 번지 값을 담게 된다.

이하 각 섹션들은 8바이트 이하의 인식 가능한 아스키코드로 된 섹션 이름으로 시작한다. 그래서 텍스트에디터로 열어도 각 섹션의 시작위치는 파악이 가능하게 된다.

  .data 전역 데이타들을 담고 있는 영역.
  .edata export 함수에 대한 정보를 담고 있는 영역
  .idata import! 함수에 대한 정보를 담고 있는 영역
  .reloc  
  .rsrc 다이얼로그, 아이콘, 메뉴 등의 리소스 데이터의 내용을 담고 있는 영역.
  .tls Thread Local Storage; 스레드 지역 저장소

각 스레드에 고유한 전역 변수 공간. 멀티스레딩 환경에서 런타임 라이브러리의 전역 변수 등을 각 스레드 사이에서 보호하기 위해 사용된다.

명시적 DLL 로딩 방식에서는 TLS가 작동하지 않는다.

  ......  

■ 프로그램의 구동.

○ 프로그램을 실행시키면 콘솔 응용프로그램은 main()이 실행되고, GUI는 WinMain()이 실행된다.

○ main()은 간단하게 arguments만 넘겨서 실행하므로 여기에서는 WinMain()을 대상으로 그 실행과정을 살펴본다.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, PSTR szCmdLine, int iCmdShow);

○ 각 매개변수 설명

  • HINSTANCE hInstance : 해당 프로그램의 인스턴스 핸들.
    • 이 인스턴스는 프로세스에 속한 여러 리소스들(메뉴, 아이콘, 커서, 대화상자 등등)의 준거점이 된다. 이러한 리소스들을 로드할 때에는 언제나 이 인스턴스를 요구한다.
    • 실제로 이 값은 0x00400000 으로 설정되어 있다. 즉 .text 섹션의 시작 위치를 가리키고 있는 것이다.
    • PE헤더의 IMAGE_OPTIONAL_HEADER의 ImageBase 필드에 지정되어 있는 값이므로 DLL의 경우에는 0x10000000 이 된다.
  • HINSTANCE hPreInst : 의미없음. Win16과 호환을 위해 남겨진 부분. 항상 "NULL".
  • PSTR szCmdLine : Command-line arguments에 대한 문자열 포인터.
  • int iCmdShow : 처음 실행시 윈도가 화면에 나타나는 방법.

○ WINAPI : 이것은 스택 호출방법을 설정하는 것으로 내부적으로 __stdcall (PASCAL 방식의 호출)으로 정의되어 있다(WINDEF.H에 define되어 있다).

  • 인자(parameter) 개수를 가변적으로 받을 수 있고(printf처럼), 인자 스택을 함수 측(호출 받은 측)에서 해제한다.
  • 즉, 인자 개수의 유연성과 빠른 스택소멸을 위한 매크로 상수이다.
  • VC++에서는 파스칼 방식이 표준 호출 방식으로 되어 있다. 만약 WINAPI를 명시하지 않으면 디폴트로 __cdecl로 인식해서 C선언 방식으로 컴파일 된다. 두가지 방식 중에서 어느 것을 쓰던지 상관없지만, 일치되지 않으면 스택 포인터의 불일치로 인해 대혼란에 빠지게 된다. 그리고 이런 오류는 링크시에도 오류를 발생시켜 주므로 사전에 발견할 수 있다.
  • 참고로 WINAPI 키워드를 사용하지 않으면 'C선언 방식(__cdecl)' 으로 작동된다. 즉, 호출한 측에서 스택을 제거시킨다.
  • 그리고 스택 push 순서는 parameter의 왼쪽에서 오른쪽 매개변수 순서로 push 되도록 통일되어 있다. 예전에는 C선언방식의 경우 매개변수를 오른쪽에서 왼쪽으로 push, 파스칼 선언방식의 경우 왼쪽에서 오른쪽으로 차례대로 push하도록 되어 있어 차이가 있었다.

○ 실행순서

  1. 운영체제의 프로그램 로더는 PE의 헤더에 있는 IMAGE_OPTIONAL_HEADER 구조체의 AddressOfEntryPoint 필드를 참고해 그 주소의 코드(.text 섹션 내부에 위치)를 실행시킨다.
  2. AddressOfEntryPoint가 가리키는 지점에는 WinMainCRTStartup()함수가 있다. 이 함수는 WinMain()이 필요로 하는 실행환경을 만들고, 각 매개변수로 전달될 값들을 구해서 WinMain()함수를 호출한다.
  3. WinMain()함수는 WinMainCRTStartup()함수로 부터 넘겨받은 arguments를 이용해 프로그램 내부적인 동작을 실행한다.
  4. WinMain()함수의 동작이 종료되면 실행 포인터는 WinMainCRTStartup()함수로 return되고, 이 함수는 다시 WinMain()의 리턴값을 exit함수의 parameter로 넘겨준다.
  5. exit()함수는 여러가지 마무리 작업을 하고, Win32 ExitProcess() 함수를 호출하고 WinMain()의 리턴값을 운영체제로 넘기고 프로그램을 종료한다.

■ 리소스

○ 리소스 섹션은 PE구조에서 .rsrc 섹션 영역에 존재한다.

  • 프로그램에서 *.cpp가 컴파일 되어 .obj가 되는 것처럼, 리소스는 .rc 파일이 리소스 컴파일러를 통해 .res로 만들어진다. 이후 링커를 통해 실행파일에 합쳐진다.
  • 당연히 리소스 파일이 없이도 윈도 프로그램은 제작 가능하다.
  • MFC를 사용하면 기본적으로 항상 몇가지의 리소스는 포함된다. Accelerator, Dialog, Icon, Menu, String table, Bitmap, Font, Cursor, Version 등등...

○ 리소스 섹션은 트리 구조로 되어 있다. leaf node에 실제 리소스가 링크되어 있다.


---------------------------------------


■ .Net의 PE 구조

○ 기존의 각 섹션 개념은 그대로 가져왔지만 구조는 크게 달라졌다.

○ 웬만한 정보는 .text 섹션으로 모두 통합되었다. 다만 그 내부에서 각 섹션 개념별로 구분되어 관리될 뿐이다.

○ 자세한 내용은 생략... (나중에 별도로 책이 나온다고 함)

댓글 없음:

댓글 쓰기