1. COM
1970년대 중반, MS-DOS의 전신격인 CP/M 때부터 있었던 완전 초창기 실행 파일 포맷이다. 고안자는 개리 킬달.
엄밀히 말해, 얘는 파일 포맷이라 할 것도 없는 쌩 메모리 이미지 덤프였다. 그 어떤 고유한 헤더나 메타데이터도 없이 그냥 곧장 기계어 코드와 데이터가 쭉 이어질 뿐이었다. 코드와 데이터는 모두 64KB 단일 세그먼트에 묶여 있었고, 메모리 주소의 첫 256바이트는 시스템 용도로 예약되어 있어서 프로그램이 사용할 수 없었다.
확장자가 com인 실행 파일은 그냥 명령 프롬프트에서 돌아가는 간단한 유틸밖에 없을 것 같지만, 그래도 겨우 몇만 바이트 남짓한 com 형태로 그래픽 모드에서 실행되는 게임도 많이 나왔었다. 그래픽이라고 해 봤자 320*200 4색 CGA 수준이긴 했지만.. Alley Cat처럼 말이다.
1980년대에 들어서 컴퓨터의 대세가 8비트에서 16비트로 넘어가고 성능과 메모리 용량이 향상되자, 이 형식은 큰 프로그램을 만들기에는 너무 비좁아졌다. 그래서 확장할 필요가 생겼다.
2. MZ EXE
1983년, MS-DOS 2.0에서 최초로 도입됐다. 그 전의 1.0은 파일 시스템에 서브디렉터리라는 게 지원되지 않았으며 실행 파일도 아직 COM밖에 없었다.
EXE는 단일이 아닌 다중 세그먼트(특히 코드 영역과 데이터 영역의 분리)를 도입하여 64KB 공간 한계를 얼추 극복했다. 메모리 모델이니, far near 포인터니 뭐니 하면서 일이 굉장히 복잡해지긴 했지만 말이다.
또한, 멀티태스킹 환경에 대비해서 재배치 정보도 도입했다. 이제 좀 운영체제에서 파일을 있는 그대로 메모리에 올리는 게 아니라 최소한의 가공과 상대 주소 보정을 하는 loader가 필요해졌다.
오늘날 모든 EXE들은 앞부분에 MZ라는 문자로 시작하는 간단한 헤더를 갖추고 있다. MZ는 EXE 파일 포맷의 설계자인 당시 마소의 프로그래머 Mark Zbikowski의 이니셜이다! zip 압축 파일의 식별자인 PK (개발자 필립 카츠)만큼이나 세계에서 제일 유명한 파일 포맷 식별자 이니셜일 것이다. MZ 저분은 미국 토박이라고 하지만, 이름으로 보아하니 러시아 계열 이민자의 후손인 듯하다.
비록 도스는 역사 속으로 사라졌지만, Windows용 실행 파일들은 지금까지도 과거 호환을 위해서 앞부분에 최소한의 MZ EXE 헤더 껍데기는 넣어 놓는 게 관행이 돼 있다.
한편, 32비트 이후부터는 프로그램들이 옛날처럼 다시 단일 세그먼트 기반 flat 모델로 돌아갔다. 단지, 그 세그먼트의 이론적 최대 크기가 꼴랑 64KB이던 것이 4GB로 왕창 커졌을 뿐이다.
3. NE (new)
1985년에 발표된 Windows 1.0과 함께 등장한 포맷이다. 도스와는 다른 방식의 API 호출, exe와 dll의 구분, 표준화된 리소스와 버전 정보 데이터, 함수의 import와 export 내역처럼 도스용 exe에는 없던 추가적인 정보가 많이 필요해진 관계로 새로운 실행 파일 포맷을 또 제정한 것으로 보인다. 단, 맨 앞부분은 그냥 도스 EXE처럼 시작하고, 새로운 방식은 다른 오프셋에서부터 시작된다. 이는 NE 다음의 PE도 마찬가지이다.
사실, NE는 Windows뿐만 아니라 마소에서 1986년경에 잠시 만들다 말았던 일명 '멀티태스킹 MS-DOS 4.0'(일반적인 그 MS-DOS 4.0 말고)용 실행 파일 포맷으로도 쓰였다고 알려져 있다. 하지만 도스는 텍스트 기반 환경이지만 Windows는 GUI 환경이고, 16비트 Windows에는 딱히 콘솔(명령 프롬프트)이라는 서브시스템이 존재하지 않았다. 그러니 바이너리 수준에서의 파일 포맷만 일치할 뿐, 양 플랫폼의 실행 파일을 딴 데서 원활하게 실행할 수는 없었을 것이다.
Windows 1.x부터 3.x까지 16비트 시절에 실행 파일 포맷은 크게 바뀌지 않았다. 단, 2에서 3으로 넘어가던 시절에는 Windows에 386 확장 모드라는 게 도입되었기 때문에, 이 프로그램은 종전의 리얼(real) 모드뿐만 아니라 보호(protected) 모드에서도 잘 실행된다는 보증 플래그가 추가되었다. 평범한 Windows API만 쓴 프로그램이 여기에 큰 영향을 받지는 않았겠지만, 그래도 혹시나 해서 말이다.
1980년대 왕창 옛날에 만들어졌던 일부 Windows용 프로그램들은 95뿐만 아니라 3.x에서 실행해도 "구버전용임. 여기서도 일단 실행은 되지만 케바케이기 때문에 온전하고 정상적인 동작을 기대할 수 없음. 최신 버전을 구해서 쓰셈.." 이런 주의 메시지가 뜨는 게 있었는데, 바로 이 플래그가 없이 옛날 방식대로 빌드된 프로그램이어서 그렇다.
특히 대화상자가 캡션(제목 표시줄)이 없이 표시되는 프로그램을 보면 옛날 냄새가 풀풀 난다. 캡션은 popup 윈도우가 아닌 overlapped 윈도우의 전유물이던 시절이 있었기 때문이다.
NE 방식의 실행 파일에는 각종 코드와 데이터, 리소스들이 resident, non-resident, discardable 이런 식으로 속성 구분이 있었다. 컴퓨터에 메모리는 왕창 부족한데, CPU와 운영체제 차원에서의 가상 메모리 지원이 없고, 그 열악한 환경에서 멀티태스킹을 구현하려다 보니, 돌아가는 방식이 가난함과 처절함 그 자체였다.
읽어들인 데이터는 언제든지 주소가 재배치되거나(단편화를 막고 연속된 많은 영역의 메모리를 확보할 수 있게), 삭제되어서 디스크로 되돌아갈 수 있었다. 반드시 메모리에 언제나 불러들여 놓는 데이터는 성능 차원에서 정말 중요한 것에만 한해서 지정해야 했다.
4. PE (portable)
1993년에 등장한 Windows NT 3.1에서 첫 도입된 또 다른 새로운 실행 파일이다. AT&T에서 오래 전에 개발한 COFF라는 오브젝트/라이브러리 파일 포맷의 연장선상에 있다. 아마 Windows NT 개발진의 출신 배경이 그쪽 계열 연구소여서 이런 포맷에 친숙하지 않았나 싶다. 덕분에 마소에서 개발하는 개발툴들은 16비트 시절에는 obj/lib의 포맷으로 인텔에서 개발한 OMF 방식을 썼지만 32비트부터 COFF로 갈아탔다. 그리고 exe/dll을 로딩하는 방식도 쿨하게 memory mapped file 방식으로 바꿨다.
PE는 현대적인 32비트 가상 메모리 환경에 맞춰졌기 때문에, 16비트 NE처럼 수동 메모리 관리를 염두에 둔 지저분한 속성이 존재하지 않는다. 세그먼트 대신에 코드, 데이터, 리소스 등의 용도별 섹션이 있고, 이들 섹션은 간단한 문자열 태그로 구분하기 때문에 섹션의 추후 확장이 가능하다. 그리고 헤더에 CPU 식별자도 넣어서 굳이 x86뿐만 아니라 다른 CPU 아키텍처의 실행 파일도 이 방식으로 기술 가능하게 했다.
훗날 64비트 CPU가 등장하면서, 아니 정확히는 1990년대 말에 IA64의 출시를 염두에 두고 PE의 기본 틀은 동일한데 메모리 주소나 몇몇 size 필드만 4바이트에서 8바이트로 확장된 일명 PE+ 규격이 나왔다. 그래도 기존 32비트에서도 얘를 알아보고 최소한 "에러 메시지+실행 거부" 정도의 대처는 할 수 있다.
리소스는 64비트도 32비트와 바이너리 차원에서 포맷이 완전히 동일하다. 이게 무슨 기계어 코드도 아닌데 필드 크기가 굳이 64비트 크기로 확장됐다거나 한 건 없다. 문자열이 유니코드 기반으로 바뀌었으니 16비트 방식과는 호환되지 않지만 32와 64비트끼리는 호환된다.
오늘날은 재래식 네이티브 코드뿐만 아니라 닷넷 기반(가상 머신), 그리고 UWP용(일명 metro) 앱 같은 것도 나왔지만, 이들도 실행 파일들의 기본 골격은 PE로 동일하다. 그 안에서 읽어들이는 시스템 DLL과 구동 방식이 서로 차이가 날 뿐이다.
Posted by 사무엘