LoginSignup
11
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-14

はじめに

この記事は Rustその2 Advent Calendar 2019 15日目 の記事です。小ネタになります。

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

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

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

なお、以下の記事が古くなってきたので、アップデートしましたので、ぜひともそちらをご覧ください。(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 で公開されていますが、それからブートできるシステムを構築するのは大変そうです。VirtualBox の上に FreeDOS をインストールするのも良いのですが、ここは macOS の Hypervisor framework を使った C++ で書かれたコンパクトなエミュレータを部分的に Rust に移植してみましたので、それを使っていきます。

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

Rust で書いた DOS アプリ

Rust でどこまで対応できるのかはよくわかりませんが、まずは "Hello, World!" を書いてみましょう。ここでは Serentty さんRusty DOS を参考にしながら書いてみます。

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

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 に以下のように書いておきます。

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

[target.dos]
runner = "hvdos"

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

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 というよりも、ほとんどアセンブラですね。 😅

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

前回のプログラムと基本的には変わりません。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!

無事に動きました。 🎉🍻

結果

前回の結果に今回の結果を追記してみましょう。

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) 56 bytes

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

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

おわりに

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

追記(2019/12/28)

GitHub においてあるスタートアップルーチンには DOS の exit システムコールを呼び出すコードが含まれていましたので、それを取り除き、単純に OS にリターンするようにしました。また、Rust の exit() で終了コードを返すように変更してみました。fn exit 以下は次のようになります。

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!)ので、rusty_asm の代わりに直接 llvm_asm を呼ぶことにしました。修正版についてはこちらをご覧ください


  1. Crate cc を使う場合にはここらへんはよきに計らってもらえるので、気を使う必要はありません。 

  2. ごく簡単なプログラムは動くのですが、少しでも Rust らしいことをやろうと思うと、rustc が落ちたり、リンクエラーが発生してしまっています。なので、正直なところ、実用的にはちょっと辛いと思いますので、あくまでも小ネタということでお許し下さい。 

11
6
7

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
11
6