MS-DOSのアセンブラでEXE形式の実行ファイルを作成する
MS-DOSのEXEフォーマット形式のサンプルコード
Hello, World!を表示するプログラム
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の領域の分がファイルに埋め込まれました。
セグメントの順番を変えてみる
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
スタックセグメントの定義を最後にしてみました。
| アドレス | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 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ファイルの先頭部分
| アドレス | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 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ヘッダー情報の分だけマイナスすると一致します。
簡略化セグメントで記述
.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データセグメント |