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

# はじめに

この記事は [Rustその2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rust2) 15日目 の記事です。小ネタになります。

以前、[macOS 上の Rust でしっかりとスタティックリンク](/moriai/items/b1fa7d1b43d985d408cc)した時、どれだけバイナリがスリム化できるかにトライしてみましたが、固定文字列とシステムコール２発だけなのに、4,096バイトを消費してしまい、いまいち不満が残りました。だって、"Hello, World!\n\0" で15バイト、システムコール２発で２ワード、計50バイトぐらい、リロケーション情報が色々ついても、３桁バイトにまでいかずとも。もちろん、PC や Mac ではメインメモリが GB が当たり前ですから、そんなことを気にしてもしょうがないので、ターゲットを変えてみることにしました。けれども、組込系はやっている人が多そうだし...

良いターゲットはないかなぁと github 上をぷらぷらしていたら、Serentty さんが Rust で初代x86系CPU 8086 上の DOS、つまり、MS-DOS / PC-DOS  をターゲットにしているのを見つけ、「これだっ」ということで、ご紹介したいと思います。

ということで、DOS といっても Denial of Service 攻撃ではなく、Disk Operating System を攻めてみるぜ、っていうお話です。期待して来てくださった方、ごめんなさい。

なお、以下の記事が古くなってきたので、[アップデート](https://qiita.com/moriai/items/8ab9d0f11a50b0de5ae0)しましたので、ぜひとも[そちら](https://qiita.com/moriai/items/8ab9d0f11a50b0de5ae0)をご覧ください。（2020/5/5追記）


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

ホスト環境はこんなところです。

+ macOS 10.14.6 (18G2022)
+ rustc 1.41.0-nightly (3eeb8d4f2 2019-12-12)
+ cargo 1.41.0-nightly (626f0f40e 2019-12-03)

そして、rust-src と cargo-xbuild をインストールします。

```
$ rustup component add rust-src
$ cargo install cargo-xbuild
```

次に、x86 用の as, gcc, ld も用意します。年の瀬ですし、さくっと 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 を使った C++ で書かれたコンパクトなエミュレータ](https://github.com/mist64/hvdos.git)を部分的に Rust に移植してみましたので、それを使っていきます。

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

# Rust で書いた DOS アプリ

Rust でどこまで対応できるのかはよくわかりませんが、まずは "Hello, World!" を書いてみましょう。ここでは [Serentty さん](https://github.com/Serentty)の [Rusty DOS](https://github.com/Serentty/rusty-dos) を参考にしながら書いてみます。

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

```json:dos.json
{
    "arch": "x86",
    "cpu": "i386",
    "data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128",
    "dynamic-linking": false,
    "executables": true,
    "exe-suffix": ".com",
    "linker-flavor": "gcc",
    "linker": "x86_64-elf-gcc",
    "linker-is-gnu": true,
    "llvm-target": "i386-unknown-none-code16",
    "max-atomic-width": 64,
    "position-independent-executables": false,
    "disable-redzone": true,
    "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"
      ]
    },
    "pre-link-objects-exe-crt": [
      "startup.o"
    ],
    "relro-level": "full",
    "target-c-int-width": "32",
    "target-endian": "little",
    "target-pointer-width": "32",
    "os": "none",
    "vendor": "unknown"
  }
```

基本は Serentty さんのものですが、linker として x86_64-elf-gcc を指定するところがポイントです。linker script は com.ld を指定し、その中身は Serentty さんのものをそのまま使います。ターゲットファイル名を dos.json としましたので、.cargo/config に以下のように書いておきます。

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

[target.dos]
runner = "hvdos"
```

次に、スタートアッププログラム startup.c のコンパイルとリンクを build.rs に記述します。

```Rust:build.rs
use std::env;
use std::process::Command;

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();

    Command::new("x86_64-elf-gcc")
        .args(&["src/startup.c", "-c", "-march=i386", "-m16", "-ffreestanding", "-fno-pie", "-o"])
        .arg(&format!("{}/startup.o", out_dir))
        .status().unwrap();

    println!("cargo:rustc-link-search=native={}", out_dir);
}
```

cc ではなく x86_64-elf-gcc で、i386 の 16bit モードでコンパイルし、startup.o が書き出される場所を cargo:rustc-link-search= を使って cargo に教えてあげるところがポイント[^1]です。

あとは頑張って、main.rs を書きます。Rust というよりも、ほとんどアセンブラですね。 😅

```Rust:src/main.rs
#![feature(proc_macro_hygiene, asm)]
#![no_main]
#![no_std]

use rusty_asm::rusty_asm;
use core::panic::PanicInfo;

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

fn write(fd: usize, buf: *const u8, len: usize) {
    unsafe {
        rusty_asm! {
            let buf: in("{dx}") = buf;
            let len: in("{cx}") = len;
            let fd: in("{bx}") = fd;
            clobber("ah");
            clobber("al");
            asm("volatile", "intel") {
                "mov ah, 40h
                 int 21h"
            }
        }
    }
}

fn exit() -> ! {
    unsafe {
        rusty_asm! {
            asm("volatile", "intel") {
                "mov ah, 4ch
                 int 21h"
            }
        }
    }
    loop {}
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    let msg = "Hello, world!\r\n";
    let buf = msg.as_ptr();
    let len = msg.len();

    write(1, buf, len);
    exit();
}
```

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

Cargo.toml などを修正して、ビルドします。

```
$ RUSTFLAGS="-C opt-level=z -C relocation-model=static" cargo xbuild --release
```

では、run!

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

無事に動きました。 🎉🍻

# 結果

[前回の結果](/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 |
| <b>Rust (-C opt-level=3, no_std)</b> | <b>4,216 bytes</b> | <b>4,096 bytes</b> |
| C (-Oz)  | 8,432 bytes | 8,440 bytes |
| <b>Rust for MS-DOS (-C opt-level=s)</b> |   | <b>56 bytes</b>  |

56バイトといっても、ほとんどアセンブラですから当然でしょう[^2]。

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

# おわりに

些細なことをちょっと深く掘ってしまいましたが、こういうことをさらっと試せるのって素晴らしいですね。

# 追記（2019/12/28）

[GitHub](https://github.com/moriai/hello/tree/v0.1.0/rust-msdos) においてあるスタートアップルーチンには DOS の exit システムコールを呼び出すコードが含まれていましたので、それを取り除き、単純に OS にリターンするようにしました。また、Rust の exit() で終了コードを返すように変更してみました。`fn exit` 以下は次のようになります。

```Rust:rust/main.rs
...

fn exit(status: usize) -> ! {
    unsafe {
        rusty_asm! {
            let status: in("al") = status as u8;
            asm("volatile", "intel") {
                "mov ah, 4ch
                 int 21h"
            }
        }
    }
    loop {}
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    let msg = "Hello, world!\r\n";
    let buf = msg.as_ptr();
    let len = msg.len();

    write(1, buf, len);
    exit(0);
}
```

ちなみに、バイナリサイズは56バイトとなりました。
@fujitanozomu さん、ご指摘ありがとうございました。

# 追記（2020/5/4）

2020年1月頃、Homebrew の i386-elf-gcc と x86_64-elf-gcc の環境が統合されてしまったので、関連する箇所を修正しておきました。

また、Rust nightly で asm! が deprecated になっています（[RFC2843: Add llvm_asm! and deprecate asm!](https://github.com/rust-lang/rfcs/pull/2843)）ので、rusty_asm の代わりに直接 llvm_asm を呼ぶことにしました。修正版については[こちらをご覧ください](https://qiita.com/moriai/items/8ab9d0f11a50b0de5ae0)。

[^1]: Crate cc を使う場合にはここらへんはよきに計らってもらえるので、気を使う必要はありません。
[^2]: ごく簡単なプログラムは動くのですが、少しでも Rust らしいことをやろうと思うと、rustc が落ちたり、リンクエラーが発生してしまっています。なので、正直なところ、実用的にはちょっと辛いと思いますので、あくまでも小ネタということでお許し下さい。