MZ 실행 파일
1. 개요
MZ 실행 파일은 도스(DOS) 운영체제에서 사용되는 실행 파일 형식이다. 이 파일 형식은 기본 머리(Header)와 재배치 정보(Relocation Table)로 구성되며, 파일의 속성과 실행에 필요한 정보를 담고 있다. 기본 머리에는 시그니처, 페이지 수, 재배치 항목 수, 머리 크기 등의 정보가 포함된다. MZ 실행 파일은 세그먼트 구조를 가지며, 코드, 데이터, 스택 세그먼트를 별도로 관리한다. MS-DOS 1.x 및 2.x API를 통해 프로그램 종료 방식을 제공하며, FASM 어셈블러를 사용하여 MZ 실행 파일을 생성할 수 있다. MZ 실행 파일은 DOS 및 윈도우 9x에서 실행 가능하며, 윈도우 NT 계열에서는 가상 도스 머신을 통해 실행할 수 있지만, 64비트 윈도우에서는 직접 실행이 불가능하다.
| 확장자: .exe, .com, .dll | |
| MIME 형식 | 해당 없음 |
|---|---|
| uniform type identifier | 해당 없음 |
| 소유자 | 해당 없음 |
| 매직 넘버 | MZ 또는 ZM |
| 종류 | 바이너리, 실행 파일 |
| 컨테이너 포맷 | 해당 없음 |
| 포함 대상 | 해당 없음 |
| 확장 대상 | New Executable Linear Executable Portable Executable |
| 표준 | 해당 없음 |
2. 기본 구조
MZ 실행 파일은 기본 머리(Header)와 재배치 정보(Relocation Table)로 구성된다.
| OFFSET | TYPE | 설명 |
|---|---|---|
| 0000h | c char | 'MZ'(0x4d, 0x5a) |
| 0002h | 1 word | 마지막 페이지(블록)의 바이트 수. (페이지 = 512바이트) |
| 0004h | 1 word | 파일의 총 페이지 수. |
| 0006h | 1 word | 재배치 항목의 수 |
| 0008h | 1 word | 단락 단위의 머리 크기. (단락 = 16바이트) |
| 000Ah | 1 word | 추가로 확보되어야 할 최소 단락 수 |
| 000Ch | 1 word | 추가로 확보되어야 할 최대 단락 수 (보통 65535, 1MB) |
| 000Eh | 1 word | 초기 SS (실행 시작 시점 기준) |
| 0010h | 1 word | 초기 SP |
| 0012h | 1 word | 실행 파일 체크섬 (또는 0) |
| 0014h | 1 word | 초기 IP (실행 시작 시점 기준) |
| 0016h | 1 word | 초기 CS (실행 시작 시점 기준) |
| 0018h | 1 word | 재배치 정보표의 시작 위치 (new-EXE: 40h) |
| 001Ah | 1 word | 오버레이 번호 (0h = 메인 프로그램) |
파일 크기는 16바이트 단락(Paragraph)과 512바이트 페이지 단위로 표현된다. 파일 전체 크기는 (pages - 1) * 512 + extrabytes로 계산되며, 머리와 재배치 정보도 포함된다.
COM 파일은 64KiB 내에서만 동작하므로 모든 주소가 명확하다. 메모리 위치와 관계없이 시작점을 세그먼트로 지정하면 실행에 문제가 없다.
MZ 형식은 64KiB 제한을 극복하고자 여러 세그먼트를 사용한다. 프로그램 실행 시점에 실제 메모리 위치를 알 수 없으므로, 0번지에 올려지는 것처럼 주소를 계산하고, 실제 로드 시 모든 세그먼트에 시작점을 더하는 재배치(Relocation)를 수행한다.
MZ 실행 파일은 재배치가 필요한 세그먼트 사용 위치를 재배치 표에 모아둔다. 도스는 프로그램을 로드할 때 재배치 표를 참조하여 실제 시작 주소(시작 세그먼트)를 더해준다.
재배치 표는 세그먼트가 사용된 주소(Segment:Offset)를 나열한다.
| OFFSET | | 설명 | |
|---|---|---|
| 0000h | 1 word | 오프셋 |
| 0002h | 1 word | 재배치 세그먼트 |
재배치 항목은 Offset과 Segment (각 1 word, 총 4바이트)로 구성되며, 머리의 relocations 수만큼 존재한다.
2.1. 기본 머리 (Header)
기본 머리(Header)는 파일의 속성과 실행에 필요한 정보를 담고 있다. 주요 필드로는 시그니처('MZ'), 파일 크기, 재배치 항목 수, 머리 크기, 최소/최대 메모리 요구량, 초기 스택 포인터(SP) 및 코드 세그먼트(CS) 값, 재배치 정보 위치 등이 있다.
| 필드 | 설명 |
|---|---|
| signature | EXE 서명 (MZ 또는 ZM) |
| extrabytes | 마지막 페이지의 바이트 수 |
| pages | 파일 내 페이지 수 |
| relocations | 파일 내 릴로케이션 수 |
| headersize | 헤더 내 문단 수 |
| minmemory | 최소 메모리 양 |
| maxmemory | 최대 메모리 양 |
| initSS | 초기 SS (실행 시작 시점 기준) |
| initSP | 초기 SP |
| checksum | 실행 파일 체크섬 (또는 0) |
| initIP | 초기 IP (실행 시작 시점 기준) |
| initCS | 초기 CS (실행 시작 시점 기준) |
| reloctable | 재배치 정보표의 시작 위치 (40h는 new-(NE,LE,LX,W3,PE etc.) 실행 파일) |
| overlay | 오버레이 번호 (0h = 메인 프로그램) |
파일 크기는 16바이트인 단락(Paragraph)과 512바이트인 페이지 단위로 표현한다. 파일 전체 크기는 (pages - 1) * 512 + extrabytes로 계산할 수 있으며, 여기에는 머리와 재배치 정보도 포함된다.
코드 부분이 페이지 단위로 정렬될 수 있도록 머리 크기를 페이지 단위로 맞추는 경우가 많다. 예를 들어 재배치 정보가 없더라도 headersize = 32 (32 * 16 = 512, 페이지 하나 크기)가 될 수 있다.
DOS에서 실행되는 EXE 프로그램 환경은 해당 프로그램의 프로그램 세그먼트 접두사에서 찾을 수 있다.
EXE 파일은 보통 코드, 데이터, 스택에 대해 별도 세그먼트를 가진다. 프로그램 실행은 코드 세그먼트 주소 0에서 시작하고, 스택 포인터 레지스터는 머리 정보에 포함된 값으로 설정된다. (머리가 512바이트 스택을 지정하면 스택 포인터는 200h) 별도 스택 세그먼트를 사용하지 않고 코드 세그먼트를 스택으로 사용할 수도 있다.
DS(데이터 세그먼트) 레지스터는 보통 CS(코드 세그먼트) 레지스터와 같은 값을 가지며, EXE 파일 초기화 시 데이터 세그먼트의 실제 세그먼트 주소로 로드되지 않는다. 프로그래머가 직접 설정해야 한다.
2.2. 덧붙인 머리 (Extended Header)
다양한 개발 도구들이 MZ 형식의 기본 머리 뒤에 추가적인 정보를 덧붙여 사용하기도 한다. 마이크로소프트의 PE(Portable Executable) 형식도 MZ 형식을 기반으로 하므로, 덧붙인 머리에 PE 머리 정보를 포함한다. 다음은 MS SDK에 포함된 덧붙인 머리를 포함한 전체 머리 구조체의 예시이다.
```c
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;
```
앞 부분은 변수명만 다르게 정의되었을 뿐 기본 머리와 같다. PE에서 실질적인 의미가 있는 것은 맨 마지막의 e_lfanew 뿐이다. 새 형식의 머리가 어디에 있는지에 대한 정보다. 나머지 덧붙인 정보들은 PE가 만들어질 때까지 여러 제품들이 추가했던 정보들을 모두 반영하기 위한 형식적인 것이다.
2.3. 재배치 정보 (Relocation Table)
재배치 정보는 프로그램이 메모리에 로드될 때 세그먼트 주소를 조정해야 하는 위치를 기록해 둔 표이다. 각 재배치 항목은 오프셋(Offset)과 세그먼트(Segment) 값으로 구성된다. 도스(DOS)는 프로그램을 메모리에 로드할 때 재배치 표를 참조하여, 프로그램 내에서 사용된 세그먼트 값들을 실제 로드된 주소에 맞게 수정한다.
MZ 형식은 여러 세그먼트를 가질 수 있도록 설계되었는데, 프로그램이 만들어질 당시에는 이 세그먼트들의 실제 메모리 위치를 알 수 없다. 따라서 프로그램은 자신이 0번지에 올려지는 것처럼 주소를 계산하고, 실제로 특정 메모리에 로드될 때 모든 세그먼트에 시작점을 더해주는 방식(재배치)을 사용한다.
MZ 실행 파일은 재배치가 필요한 세그먼트가 사용된 위치를 모두 모아 재배치 표를 만든다. 도스는 프로그램을 메모리로 올릴 때, 재배치 표에 기록된 위치(프로그램에서 세그먼트가 사용된 곳)를 찾아 실제 프로그램이 시작하는 주소, 즉 시작 세그먼트를 더해준다.
재배치 표는 프로그램에서 세그먼트가 사용된 곳의 주소(Segment:Offset)를 나열한 것이다. 재배치 항목 하나는 오프셋과 세그먼트가 각각 한 워드씩, 총 4바이트를 차지한다. 이러한 항목들이 머리(header)에 기록된 relocations 수만큼 존재한다.
| 오프셋(OFFSET) | 타입(TYPE) | 설명 |
|---|---|---|
| 0000h | 1 word | 오프셋 |
| 0002h | 1 word | 재배치 세그먼트 |
DOS가 실행하는 EXE 프로그램의 환경은 해당 프로그램의 프로그램 세그먼트 접두사에서 찾을 수 있다.
3. 세그먼트 처리
EXE 파일은 일반적으로 코드, 데이터, 스택 등 여러 세그먼트를 가진다. 프로그램 실행은 코드 세그먼트(CS)의 0번지에서 시작하며, 스택 포인터(SP) 레지스터는 헤더에 지정된 값으로 설정된다. 예를 들어, 헤더가 512바이트 스택을 지정하면 스택 포인터는 200h로 설정된다. 필요한 경우 별도의 스택 세그먼트를 사용하지 않고 코드 세그먼트를 스택으로 사용할 수도 있다.
데이터 세그먼트(DS) 레지스터는 보통 CS 레지스터와 같은 값을 가지며, EXE 파일이 초기화될 때 데이터 세그먼트의 실제 세그먼트 주소로 로드되지 않는다. 프로그래머가 직접 다음과 같은 명령을 통해 DS 레지스터를 설정해야 한다.
MOV AX, @DATA
MOV DS, AX
4. 프로그램 종료
MS-DOS 1.x API에서는 프로그램 종료 시 CS 레지스터가 PSP가 있는 세그먼트를 가리키도록 하고, RETF 명령어를 통해 종료했다.
```nasm
PUSH DS
XOR AX, AX
PUSH AX
```
REFT 명령어는 스택에서 PSP가 있는 원래 세그먼트 주소를 가져와 주소 0으로 점프했는데, 주소 0에는 INT 20h 명령어가 있었다.
MS-DOS 2.x API에서는 INT 21h Function 4Ch을 호출하여 프로그램을 종료하는 새로운 기능이 도입되었다. 이 기능을 사용하면 프로그램 시작 시 PSP 세그먼트 주소를 저장할 필요가 없었으며, 마이크로소프트는 이전의 DOS 1.x 방식의 사용을 권장하지 않았다.
5. FASM 예제
assembly
format MZ
entry _text:main
stack 80h
segment _text
main:
mov ax,_data
mov ds,ax
mov ah,9
mov dx,hello
int 21h
mov ax,4c00h
int 21h
segment _data
hello db 'Hello world!', 24h
```
FASM을 사용하면 이 프로그램은 77바이트의 아주 작은 MZ 실행 파일이 된다. 이 예제는 "Hello world!"를 출력하는 간단한 프로그램이다.
기본 머리 부분을 읽기 쉽게 바꾸면 다음과 같다.
```c
signature="MZ"
extrabytes=77
pages=1
relocations=1
headersize=2
minmemory=8
maxmemory=65535
initSS=3
initSP=128
checksum=0
initIP=0
initCS=0
reloctable=28
overlay=0
```
* `signature`: "MZ"로 시작한다.
* `pages`, `extrabytes`: 파일이 77바이트이므로 `pages=1`로 페이지는 하나뿐이고, 그 페이지의 사용 바이트는 `extrabytes=77`이다.
* `relocations`, `reloctable`: 재배치 항목은 `relocations=1`개가 있고, 재배치 표는 `reloctable=28`에서 시작한다. 기본 머리의 크기가 28이므로 재배치 표가 머리 다음에 바로 이어지고 있다는 것을 알 수 있다. FASM은 별도의 추가 정보를 덧붙이지 않고 기본 머리만을 만들어내고 있다. 하나 있는 재배치 항목은 0000:0001이다.
* `headersize`: `headersize=2`이므로 실제 코드의 시작은 2 * 16으로 32(0x20)부터다.
* `minmemory`: `minmemory=8`은 `stack 80h` 때문이다. `stack`은 초기화 대상이 아니기 때문에 그냥 메모리만 확보하면 된다. `stack 80h`를 설정해도 파일에 `80h`만큼의 공간이 확보되지는 않지만, `minmemory=8` 설정을 통해 메모리가 추가로 `80h`만큼은 반드시 더 확보되도록 한다.
* `maxmemory`: `maxmemory=65535`는 남은 메모리를 모두 할당하도록 한다.
* `initSS`, `initSP`: 코드 부분인 `_text`의 크기가 0x20, `_data`의 크기는 0x0e이지만 16바이트의 문단 단위로 메모리를 관리하기 때문에 `stack`은 그 이후에 잡힌다. 그래서 SS:IP가 0003:0080으로 된다.
* `initCS`, `initIP`: CS:IP는 0번지에서 바로 시작하므로 0000:0000이다.