Rust で DOS プログラミングしてみる？ Updated！

# はじめに

[「Rust で DOS プログラミングしてみる？」](https://qiita.com/moriai/items/bae7fe3d1c2eaefe5cd0)を昨年末に書いて以来、久々にビルドし直そうとしてみたら、ビルドできなかったり、ワーニングが色々と出たり、他にも色々と不満が出てきたので、@fujitanozomu さんの[記事](https://qiita.com/fujitanozomu/items/6480c5f2dc526265b72c)も参考にしつつ、アップデートしてみました。

コードは [GitHub](https://github.com/moriai/hello/tree/master/rust-msdos) にて公開しています。こちらのレポジトリは適宜アップデートしていますので、合わせてご参照ください。


# クロスコンパイル環境の準備

ホスト環境は下記の通りです。個人的には Catalina が一番使い易いと思っています。（2022.6.25 修正）

+ macOS 10.15.7 (19H2014)
+ rustc 1.63.0-nightly (5750a6aa2 2022-06-20)
+ cargo 1.63.0-nightly (8d42b0e87 2022-06-17)

そして、rust-src をインストールします。以前は cargo-xbuild が必要でしたが、不要となりました。（2021.8.13 修正）

```
$ rustup component add rust-src
```

次に、x86 用の gcc（ld のフロントエンドとして使う） や binutils を用意します。さくっと Homebrew が楽チンです。

```
$ brew install x86_64-elf-gcc
```

これで、一揃いの環境が出来上がります。


# DOS 環境の準備

[MS-DOS はオープンソースとして GitHub で公開されています](https://github.com/microsoft/MS-DOS)が、それからブートできるシステムを構築するのは大変そうです。[VirtualBox の上に FreeDOS をインストールする](http://wiki.freedos.org/wiki/index.php/VirtualBox)のも良いのですが、ここは [macOS の Hypervisor framework を使ったコンパクトなエミュレータ（Rust移植版）](https://github.com/moriai/hvdos.rs)を使います。`cargo install`でサクッとインストールできるので楽チンです。

```
$ cargo install --git https://github.com/moriai/hvdos.rs.git
```


# クロスコンパイル環境の準備

[Serentty さん](https://github.com/Serentty)の [Rusty DOS](https://github.com/Serentty/rusty-dos) を参考にしながら Rust のクロスコンパイル環境を準備します。ここでは、C やアセンブラのスタートアップは使わずに、Rust で頑張ってみましょう。

まず必要なのはターゲットの定義です。

```json:dos.json
{
  "arch": "x86",
  "cpu": "i386",
  "data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128",
  "disable-redzone": true,
  "dynamic-linking": false,
  "exe-suffix": ".com",
  "executables": true,
  "features": "-mmx,-sse,+soft-float",
  "linker-flavor": "gcc",
  "linker-is-gnu": true,
  "llvm-target": "i386-unknown-none-code16",
  "max-atomic-width": 32,
  "os": "none",
  "panic-strategy": "abort",
  "position-independent-executables": false,
  "pre-link-args": {
    "gcc": [
      "-Wl,--as-needed",
      "-Wl,-z,noexecstack",
      "-Wl,--gc-sections",
      "-Wl,-melf_i386",
      "-m16",
      "-nostdlib",
      "-march=i386",
      "-ffreestanding",
      "-fno-pie",
      "-Tcom.ld"
    ]
  },
  "relocation-model": "static",
  "relro-level": "off",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "32",
  "vendor": "unknown"
}
```

基本は Serentty さんのものですが、linker として x86_64-elf-gcc を指定し、startup ルーチンを使わないところがポイントです。relocation-model や panic-strategy などもこのファイルで定義しておきます。cpu は i8086 を指定したいところですが、Rust や LLVM ではサポートされていないようなので i386 で我慢します。features に +16bit-mode を追加すると、より16ビット環境に馴染むコードが生成されますが、2バイト即値オペランドのマシンコードの生成がうまくいきません。data-layout や target-pointer-width を 16 にすると、libcore を生成できなかったりします。x86 の 16ビット環境が LLVM ではサポートされていないためか、色々と変なことが起きている感じです。

max-atomic-width は 32 としました（が、これで良いのかはどうか）。この設定によって、[LLVMコンパイラビルトイン](https://llvm.org/docs/Atomics.html#libcalls-atomic)の \_\_atomic\_load\__N_(), \_\_atomic\_store\__N_() などの _N_ が決まるようです。_N_ の単位はバイトなので、この設定では  \_\_atomic\_load\_4(), \_\_atomic\_store\_4() が使われるようになります。（2020.8.1 追記）

ということで、生成されるコードは i386 以上のリアルモードや仮想8086モードで動作するコードとなり、オリジナルの i8086 では（たぶん）動作しません。

linker script は com.ld です。

```text:com.ld
OUTPUT_FORMAT(binary)
ENTRY(_startup)
SECTIONS
{
    . = 0x0100;
    .text :
    {
        *(.startup);
        *(.text);
    }
    .data :
    {
        *(.data);
        *(.bss);
        *(.rodata);
    }
    _heap = ALIGN(4);
}
```

startup セクションがメモリ上の先頭（0x0100）に配置されるようにします。エントリポイントは _startup で、startup セクションの先頭に _startup がくるようにプログラムします。

ターゲットファイル名を dos.json としましたので、.cargo/config に以下のように書いておきます。また、ターゲット側の標準ライブラリとして core が必要になるので、それをビルドするように指定します。（2021.8.13 修正）

```text:.cargo/config
[build]
target = "dos.json"

[unstable]
build-std = ["core"]

[target.dos]
linker = "x86_64-elf-gcc"
runner = "hvdos"
```

# Rust で DOS アプリを書く

あとは頑張って、main.rs を書きます。Rust というよりも、ほとんどアセンブラですね。 😅
`asm!` が安定化されたので、そちらを使うように修正しました。（2022.6.25追記）

```Rust:src/main.rs
#![no_main]
#![no_std]

use core::arch::asm;
use core::panic::PanicInfo;
use core::result::Result;

#[panic_handler]
pub fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

fn write(fd: u16, buf: *const u8, len: u16) -> Result<u16, u16> {
    unsafe {
        let status: u16;
        let flags: u16;

        asm!(
            "mov ah, 0x40",
            "int 0x21",
            "pushf",
            "pop dx",
            in("dx") buf,
            in("cx") len,
            in("bx") fd,
            out("ax") status,
            lateout("dx") flags
        );
        if flags & 0x01 != 0 {
            Err(status)
        } else {
            Ok(status)
        }
    }
}

pub fn exit(status: u8) -> ! {
    unsafe {
        asm!(
            "mov ah, 0x4c",
            "int 0x21",
            in("al") status
        );
    }
    loop {}
}

#[link_section=".startup"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
    let msg = "Hello, world!\r\n";
    let status = match write(1, msg.as_ptr(), msg.len() as u16) {
        Ok(_) => 0,
        Err(err) => err as u8
    };
    exit(status);
}
```

[前回のプログラム](/moriai/items/b1fa7d1b43d985d408cc)と基本的には変わりません。macOS のシステムコール呼び出しを MS-DOS のシステムコール呼び出しに変えただけですね。 😄

Cargo.toml を修正します。opt-level の定義だけで大丈夫です。

```toml:Cargo.toml
[package]
name = "hello"
version = ...
...
edition = "2018"

[dependencies]

[profile.dev]
opt-level = 1

[profile.release]
opt-level = "s" #or "z"
```

ビルドします。

```
$ cargo build --release
```

では、run!

```
$ hvdos target/dos/release/hello.com
Hello, world!
```

無事に動きました。 🎉🍻


# 生成されたコードを観察

objdump を使って、生成されたコードを（Intel形式で）逆アセンブルしてみます。

```
$ x86_64-elf-objdump -b binary -m i8086 -M intel -D --adjust-vma=0x100 target/dos/release/hello.com
 ...
00000100 <.data>:
 100:	66 53                	push   ebx
 102:	66 31 db             	xor    ebx,ebx
 105:	66 43                	inc    ebx
 107:	66 ba 22 01 00 00    	mov    edx,0x122
 10d:	66 b9 0f 00 00 00    	mov    ecx,0xf
 113:	b4 40                	mov    ah,0x40
 115:	cd 21                	int    0x21
 117:	66 31 c0             	xor    eax,eax
 11a:	b4 4c                	mov    ah,0x4c
 11c:	cd 21                	int    0x21
 ...
```

コードを観察すると、各命令に operand override prefix = 66h が付加されていて、16bit のレジスタ ax, bx, cx, dx ではなく、32bit のレジスタ eax, ebx, ecx, edx が使われてしまっています。このため、リアルな i8086 ではこのコードは動かないでしょう。

Rust 版 hvdos を使うと、命令の実行をトレースすることができます。

```
$ hvdos -t target/dos/release/hello.com 
Step CS:IP=0x0:0x102 SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x105 SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x107 SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x10d SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x113 SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x115 SS:SP=0x0:0xfff4
Hello, world!
Step CS:IP=0x0:0x11a SS:SP=0x0:0xfff4
Step CS:IP=0x0:0x11c SS:SP=0x0:0xfff4
```

hvdos は x86_64 の仮想CPU上のリアルモードを提供しているので、出力されたコードが普通に動きます。

以前は、dos.json の features に +16bit-mode にすると、次のようなコードが生成されました。

```
$ x86_64-elf-objdump -b binary -m i8086 -M intel -D --adjust-vma=0x100 target/dos/release/hello.com 
 ...
00000100 <.data>:
 100:	53                   	push   bx
 101:	31 db                	xor    bx,bx
 103:	43                   	inc    bx
 104:	ba 1e 01             	mov    dx,0x11e
 107:	00 00                	add    BYTE PTR [bx+si],al
 109:	b9 0f 00             	mov    cx,0xf
 10c:	00 00                	add    BYTE PTR [bx+si],al
 10e:	b4 40                	mov    ah,0x40
 110:	cd 21                	int    0x21
 112:	31 c0                	xor    ax,ax
 114:	b4 4c                	mov    ah,0x4c
 116:	cd 21                	int    0x21
 ...
```

一見すると、16bit コードとして良さげな感じがするのですが、00 00 という命令が余計で、副作用が怖いですね。


# バイナリサイズの比較

[前回の macOS でのバイナリサイズ一覧](/moriai/items/b1fa7d1b43d985d408cc#結果)に今回の結果を追記してみましょう。

|   | unstripped | stripped |
|---|-----------:|---------:|
| Rust (-C opt-level=3) |271,832 bytes | 175,328 bytes |
| Rust (-C opt-level=3 -C prefer-dynamic) | 9,048 bytes | 8,656 bytes |
| Rust (-C opt-level=3, no_std) | 4,216 bytes | 4,096 bytes |
| C (-Oz)  | 8,432 bytes | 8,440 bytes |
| <b>Rust for MS-DOS (-C opt-level=s)</b> |   | <b>53 bytes</b>  |

53バイト。素晴らしい。でも、ほとんどアセンブラで、生成されたコードに余計な情報は入っていませんから、当然の結果でしょう。

今回のソースコードも [GitHub](https://github.com/moriai/hello/tree/v0.3.0/rust-msdos) に追加しておきました。

# おわりに

些細なことをちょっと深く掘ってしまいましたが、色々と勉強になりました。せっかくの stay home なのに、違うことがやりたかったなあとちょっぴり思っています。