3
3

More than 1 year has passed since last update.

Rust で DOS プログラミングしてみる? Updated!

Last updated at Posted at 2020-05-05

はじめに

「Rust で DOS プログラミングしてみる?」を昨年末に書いて以来、久々にビルドし直そうとしてみたら、ビルドできなかったり、ワーニングが色々と出たり、他にも色々と不満が出てきたので、@fujitanozomu さんの記事も参考にしつつ、アップデートしてみました。

コードは GitHub にて公開しています。こちらのレポジトリは適宜アップデートしていますので、合わせてご参照ください。

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

ホスト環境は下記の通りです。個人的には 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 で公開されていますが、それからブートできるシステムを構築するのは大変そうです。VirtualBox の上に FreeDOS をインストールするのも良いのですが、ここは macOS の Hypervisor framework を使ったコンパクトなエミュレータ(Rust移植版)を使います。cargo installでサクッとインストールできるので楽チンです。

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

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

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

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

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コンパイラビルトインの __atomic_load_N(), __atomic_store_N() などの N が決まるようです。N の単位はバイトなので、この設定では __atomic_load_4(), __atomic_store_4() が使われるようになります。(2020.8.1 追記)

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

linker script は com.ld です。

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 修正)

.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追記)

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);
}

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

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

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 でのバイナリサイズ一覧に今回の結果を追記してみましょう。

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
Rust for MS-DOS (-C opt-level=s) 53 bytes

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

今回のソースコードも GitHub に追加しておきました。

おわりに

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

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3