Z80 (CP/M) と x86 (MS-DOS) 用のプログラムを1個のCOMファイルに格納する

**COMファイル**は、メモリの `0x100` 番地以降の内容をそのままファイルに格納した、単純な形式の実行可能ファイルである。
この形式のファイルは、CP/M や MS-DOS などの複数のOSで採用されているが、通常はその中身は実行する CPU や OS によって異なる。
そこで、今回は、Z80 (CP/M) と x86 (MS-DOS) の2種類の環境で共通してそのまま実行できるCOMファイルを作ってみた。

# ポイント

おまじない

```text
EB 03 C3 xx xx
```

を実行すると、x86 はこのままこの後のプログラムを実行し、Z80 は絶対アドレス `xx xx` で指定した場所に実行を移す。

# 原理

`EB 03` は、x86では [JMP 命令](https://c9x.me/x86/html/file_module_x86_id_147.html)で3バイト飛ばして実行することを表す。
すなわち、今回は `C3 xx xx` の部分を飛ばして実行することになる。

一方、Z80 では、以下の命令として実行する。

|データ|命令|
|---|---|
|`EB`|`EX DE, HL`|
|`03`|`INC BC`|
|`C3 xx xx`|`JP xxxx`|

そのため、絶対アドレス `xx xx` にジャンプすることになる。
これらの命令は未定義以外のフラグを変更しないので、もし `EX` および `INC` 命令で変化するレジスタの値が重要であれば、逆の操作

```text
DEC BC
EX DE, HL
```

により戻せばよい。

# プログラム例

今回は、Z80 (CP/M) で実行すると「hello from Z80」と、x86 (MS-DOS) で実行すると「hello from x86」と出力するプログラムを作ってみた。

## Z80 (CP/M) 用のプログラム

[CP/M information archive : BDOS system calls](https://www.seasip.info/Cpm/bdos.html)

CP/M の機能 (BDOS) は、以下の手順で呼び出すことができる。

1. `C` レジスタに機能番号を、`DE` レジスタにパラメータを格納する
2. `CALL 5` を実行する (5番地を呼び出す)

今回は、以下の機能を用いる。

* 文字列の出力 機能番号：9
  * `DE` レジスタで指定したアドレスから、`$` が格納されている位置の手前までのASCII文字列を表示する。
* アプリケーションの終了 機能番号：0
  * アプリケーションを終了し、コマンドプロンプトに戻る。

[MikeAssembler](https://mikecat.github.io/mike_assembler/) を用い、以下のプログラムのアセンブルを行った。

```text
target z80
org 0x140

	ld c, 9
	ld de, message
	call 5
	ld c, 0
	call 5

message:
	datab "hello from Z80", 0x0d, 0x0a, "$"
```

おまじない、および x86 用のプログラムを置くための領域をあけるため、`0x100` 番地ではなく `0x140` 番地から配置するよう設定している。

## x86 (MS-DOS) 用のプログラム

[アセンブラ ～MS-DOSの世界～](https://programmer.main.jp/assembler2/assembler12.html)

MS-DOS の機能は、以下の手順で呼び出すことができる。

1. 各レジスタにパラメータを格納する
2. `INT 0x21` 命令を実行する

今回は、以下の機能を用いる。

* [文字列出力](https://programmer.main.jp/assembler2/3_4.html)
  * `AH` に機能番号の `9` を、`DS:DX` に出力するデータの開始アドレスを格納して呼び出す。
  * 指定したアドレスから `$` の手前までのデータを出力する。
* [プログラム終了](https://programmer.main.jp/assembler2/7_3.html)
  * `AH` に機能番号の `0x4C`、`AL` に[リターンコード](https://programmer.main.jp/assembler2/7_4.html) (0：正常終了) を格納して呼び出す。
  * プログラムを終了してメモリを開放する。

今回は、以下の [NASM](https://www.nasm.us/) 用のプログラムを用意した。
CPU の種類によって分岐するおまじない、および前章の Z80 (CP/M) 用のプログラムも埋め込んでいる。

```text
bits 16
cpu 8086
org 0x100

	db 0xeb, 0x03, 0xc3, 0x40, 0x01

	mov ah, 9
	mov dx, message
	int 0x21
	mov ax, 0x4c00
	int 0x21

message:
	db "hello from x86", 0x0d, 0x0a, "$"

align 0x40
	db 0x0e, 0x09, 0x11, 0x4d, 0x01, 0xcd, 0x05, 0x00
	db 0x0e, 0x00, 0xcd, 0x05, 0x00, 0x68, 0x65, 0x6c
	db 0x6c, 0x6f, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20
	db 0x5a, 0x38, 0x30, 0x0d, 0x0a, 0x24
```

## 完成品

以下が、今回作成したファイル (のhexdump) である。

```text:hello.com
00000000  eb 03 c3 40 01 b4 09 ba 11 01 cd 21 b8 00 4c cd  |...@.......!..L.|
00000010  21 68 65 6c 6c 6f 20 66 72 6f 6d 20 78 38 36 0d  |!hello from x86.|
00000020  0a 24 90 90 90 90 90 90 90 90 90 90 90 90 90 90  |.$..............|
00000030  90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90  |................|
00000040  0e 09 11 4d 01 cd 05 00 0e 00 cd 05 00 68 65 6c  |...M.........hel|
00000050  6c 6f 20 66 72 6f 6d 20 5a 38 30 0d 0a 24        |lo from Z80..$|
```

CyberChef でデコードし、結果を保存することで、バイナリを入手できる。

[From Hexdump - CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Hexdump%28%29&input=MDAwMDAwMDAgIGViIDAzIGMzIDQwIDAxIGI0IDA5IGJhIDExIDAxIGNkIDIxIGI4IDAwIDRjIGNkICB8Li4uQC4uLi4uLi4hLi5MLnwKMDAwMDAwMTAgIDIxIDY4IDY1IDZjIDZjIDZmIDIwIDY2IDcyIDZmIDZkIDIwIDc4IDM4IDM2IDBkICB8IWhlbGxvIGZyb20geDg2LnwKMDAwMDAwMjAgIDBhIDI0IDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwICB8LiQuLi4uLi4uLi4uLi4uLnwKMDAwMDAwMzAgIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwIDkwICB8Li4uLi4uLi4uLi4uLi4uLnwKMDAwMDAwNDAgIDBlIDA5IDExIDRkIDAxIGNkIDA1IDAwIDBlIDAwIGNkIDA1IDAwIDY4IDY1IDZjICB8Li4uTS4uLi4uLi4uLmhlbHwKMDAwMDAwNTAgIDZjIDZmIDIwIDY2IDcyIDZmIDZkIDIwIDVhIDM4IDMwIDBkIDBhIDI0ICAgICAgICB8bG8gZnJvbSBaODAuLiR8&oeol=CRLF)

## 実行

[AltairZ80](https://schorn.ch/altair.html) を用いて、Z80 での実行をシミュレーションした。
簡単な使い方は [CP/Mシミュレータ](http://okgnz.web.fc2.com/zbrd/zbrd21.htm) に載っている。

![Z80 として実行した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/b7beccba-695a-4104-9ced-a160b417c279.png)

ファイルをインポートする際になぜか余計なデータ (`1A` の連続) がついているが、実行には関係ない部分であり、狙い通り「hello from Z80」が出力された。

[DOSBox](https://www.dosbox.com/) を用いて、x86 での実行をシミュレーションした。

![x86 として実行した例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/e6bdafc6-cf98-4fb8-9098-5747f56fe843.png)

狙い通り、「hello from x86」が出力された。

同じファイルを Z80 (CP/M) と x86 (MS-DOS) それぞれのエミュレータで実行し、どちらでも意図通りの出力がされたことを確認できた。

# 結論

プログラムの先頭に CPU の種類によってジャンプしたりしなかったりする命令を置くことで、CPU の種類によって分岐し、1個のCOMファイルに2種類の環境用のプログラムをそのまま実行できる形で格納することができた。