Zenn
Open1

MS-DOSのアセンブラでEXE形式の実行ファイルを作成する

カゴRPカゴRP

MS-DOSのEXEフォーマット形式のサンプルコード

Hello, World!を表示するプログラム

hello.asm
	ASSUME	CS:CODE,DS:DATA,SS:STACK

STACK   SEGMENT STACK
	db	123h dup(?)
STACK   ENDS

DATA    SEGMENT
MSG	db	"Hello, World!",0dh,0ah,'$'
DATA    ENDS

CODE    SEGMENT
START:
	mov	ax, DATA
	mov	ds, ax
	;文字出力
	mov	ah, 09h
	mov	dx, offset MSG
	int	21h
	;終了
	mov	ax, 4c00h
	int	21h
CODE    ENDS

	END	START

EXE形式ファイルのアセンブル&リンク

JWASMR -mz -Fo hello.exe hello.asm

COMファイルは28バイト程度でしたが、このEXEファイルは385バイトになりました。
セグメントをSTACK、DATA、CODEの順に書きましたが、それが原因で、ファイルの先頭の方にSTACKの領域の分がファイルに埋め込まれました。

セグメントの順番を変えてみる

hello.asm
	ASSUME	CS:CODE,DS:DATA,SS:STACK

CODE    SEGMENT
START:
	mov	ax, DATA
	mov	ds, ax
	;文字出力
	mov	ah, 09h
	mov	dx, offset MSG
	int	21h
	;終了
	mov	ax, 4c00h
	int	21h
CODE    ENDS

DATA    SEGMENT
MSG	db	"Hello, World!",0dh,0ah,'$'
DATA    ENDS

STACK   SEGMENT STACK
	db	123h dup(?)
STACK   ENDS

	END	START

スタックセグメントの定義を最後にしてみました。

アドレス
00000000 4D 5A 60 00 01 00 01 00 03 00 13 00 FF FF 03 00
00000010 23 01 00 00 00 00 00 00 1E 00 00 00 00 00 01 00
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000030 B8 02 00 8E D8 B4 09 BA 00 00 CD 21 B8 00 4C CD
00000040 21 00 FF FF FF FF 24 00 25 00 26 00 27 00 28 00
00000050 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0D 0A 24
スタックセグメントの領域が消えて96バイトになりました。
スタックセグメントの領域が無いのにどうやって確保するのかと言うと、EXE形式のファイルはヘッダー情報を持っています。ファイルをバイナリエディタで見ると4D 5Aから始まります。この先頭部分をDOS Header等と言います。アスキーコードのMZで始まっている事でMS-DOSはEXE形式のファイルだと判断しています。ヘッダー情報の内容はほぼ各セグメントレジスタやポインタレジスタの初期値の情報です。
ヘッダーサイズは08hの値×16バイトになり、それ以降にロードモジュールが格納されています。今回、08hには3がセットされているので、2Fhまでがヘッダ情報の領域で、30hからがロードモジュールになります。

MZ形式EXEファイルのヘッダー情報

|Offset|名称|Size|説明|
|----|----|----|----|----|
|0:0x00|Signature|word|0x5A4D(「M」および「Z」のASCII)ファイルが有効なEXE形式のファイルで有る事を示す為に付けられた署名|
|2:0x02|Extra bytes|word|最後のページに入っているバイト数。|
|4:0x04|Pages|word|全体/部分ページの数。512バイト単位のヘッダも含むファイルの大きさ|
|6:0x06|Relocation items|word|リロケーションテーブルのエントリ数。|
|8:0x08|Header size|word|ヘッダーのサイズ(16バイト単位)。|
|10:0x0A|Minimum allocation|word|PSPとプログラムイメージを除く、プログラムに必要な段落数。十分な大きさの空きブロックがない場合、ロードは停止します。|
|12:0x0C|Maximum allocation|word|プログラムによって要求された段落の数。十分な大きさの空きブロックがない場合は、可能な最大のブロックが割り当てられます。|
|14:0x0E|Initial SS|word|SSにセットする再配置可能なセグメントアドレス。|
|16:0x10|Initial SP|word|SPの初期値。|
|18:0x12|Checksum|word|ファイル内の他のすべての単語の合計に追加すると、結果はゼロになります。|
|20:0x14|Initial IP|word|IPの初期値。|
|22:0x16|Initial CS|word|CSにセットする再配置可能セグメントアドレス。|
|24:0x18|Relocation table|word|再配置テーブルへの(絶対)オフセット。|
|26:0x1A|Overlay|word|オーバーレイ管理に使用される値。ゼロの場合、これがメインの実行可能ファイルです。|
|28:0x1C|Overlay information|N/A|ファイルには、メインのプログラムオーバーレイ管理に関する追加情報が含まれている場合があります。|
このヘッダー情報はMS-DOSで使用されている領域です。WindowsのEXEファイルにも存在しますが、この情報が使われる事はありません。正確には、Windowsでは先頭のSignatureと以下で表すPE形式EXEファイルの追加情報の最後の項目のみを使用しているようです。

PE形式EXEファイル追加情報

|Offset|名称|Size|説明|
|----|----|----|----|----|
|28:0x1C|Reserved|qword||
|36:0x24|OEM identifier|word|名前で定義されていますが、他の情報は提供されていません。通常はゼロ|
|38:0x26|OEM info|word|名前で定義されていますが、他の情報は提供されていません。通常はゼロ|
|40:0x28|Reserved|20 bytes|
|50:0x3C|PE header start|dword| PEヘッダーの開始アドレス|

Windowsで作成したEXEファイルの先頭部分

アドレス
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000030 00 00 00 00 00 00 00 00 00 00 00 00 F0 00 00 00
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00
00000080 E1 2D 8D AC A5 4C E3 FF A5 4C E3 FF A5 4C E3 FF
00000090 B6 2A E2 FE A6 4C E3 FF B6 2A E6 FE BD 4C E3 FF
000000A0 B6 2A E7 FE A9 4C E3 FF CA 28 E2 FE A1 4C E3 FF
000000B0 A5 4C E2 FF E7 4C E3 FF E4 2B E7 FE A4 4C E3 FF
000000C0 E4 2B 1C FF A4 4C E3 FF E4 2B E1 FE A4 4C E3 FF
000000D0 52 69 63 68 A5 4C E3 FF 00 00 00 00 00 00 00 00
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000000F0 50 45 00 00 4C 01 08 00 22 DD 65 5B 00 00 00 00
00000100 00 00 00 00 E0 00 02 01 0B 01 0E 0E 00 52 00 00
00000110 00 42 00 00 00 00 00 00 55 10 01 00 00 10 00 00
00000120 00 10 00 00 00 00 40 00 00 10 00 00 00 02 00 00
WindowsのEXEファイルの先頭にはMS-DOSで実行した際にThis program cannot be run in DOS mode.と表示する命令が書き込まれています。

PE header start(3Ch)の情報を見るとF0hがセットされている事が分かります。F0hアドレスを見るとアスキーコードでPEがセットされています。WindowsではF0h以前の部分は無視されますが、MS-DOSではMS-DOSヘッダー情報とMS-DOSリアルモードスタブプログラムと言われる部分を実行して終了します。
PE形式EXEをMS-DOSで実行したときの動作は以下のようになります。

	push	cs
	pop	ds
	mov	dx, 000eh
	mov	ah, 09h
	int	21h
	mov	ax, 4c01h
	int	21h

000Ehはバイナリエディタで見たときの0000004Ehを指しています。This program cannot be run in DOS mode.がメモリに展開されたときの先頭文字のオフセットアドレスです。
COM形式のプログラムのインストラクションポインタはORG 100Hを最初に宣言してるので100から始まりますが、EXE形式のプログラムのインストラクションポインタは0から始まります。ややこしい話しですが、COM形式の場合はバイナリエディタで見たアドレスと比較してメモリ上のオフセットアドレスはプラス100すると一致しますが、EXE形式の場合はDOSヘッダー情報の分だけマイナスすると一致します。

簡略化セグメントで記述

hello.asm
	.model	small
	.stack
	.data

MSG	db	"Hello, World!",0dh,0ah,'$'

	.code
	.startup
	;文字列出力
	mov	ah, 09h
	mov	dx, offset MSG
	int	21h
	;終了
	.exit	00h

END

簡略化セグメントのメモリモデル

メモリモデル コード データ 説明
TINY near near COM形式限定。コードとデータ合わせて64キロバイトまで
SMALL near near コードとバイトそれぞれ64キロバイトまで
MEDIUM far near データは64キロバイトまで
COMPACT near far コードは64キロバイトまで
LARGE far far コードとデータが複数のセグメントを持つ
huge far far コードとデータが複数のセグメントを持つ
FLAT near near 32bit or 64bit OSで使用。コードとデータ合わせて4GB or 2TBまで

オプション

分類 指定形式 説明
言語型 C 関数の呼び出し規約
言語型 STDCALL 関数の呼び出し規約
スタック NEARSTACK デフォルト・グループDGROUPに割り当てる
スタック FARSTACK グループDGROUPとは別のセグメントに割り当てる

簡略化セグメントの種類

定義 セグメント名 セグメントの種類
.code [name] _TEXT or [name] コードセグメント
.const CONST 定数nearデータセグメント
.data _DATA 初期化nearデータセグメント
.data? _BSS 未初期化nearデータセグメント
.stack [size] STACK size(デフォルト1024)バイトのスタックセグメント
.fardata [name] FAR_DATA or [name] 初期化farデータセグメント
.fardata? [name] FAR_BSS or [name] 未初期化farデータセグメント
ログインするとコメントできます