はじめに

この記事は 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 を参考にしながら書いてみます。

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

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

[build]
target = "dos.json"

[target.dos]
runner = "hvdos"

次に、スタートアッププログラム startup.c のコンパイルとリンクを 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 というよりも、ほとんどアセンブラですね。 ?

#![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!

無事に動きました。 ??

結果

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

unstrippedstrippedRust (-C opt-level=3)271,832 bytes175,328 bytesRust (-C opt-level=3 -C prefer-dynamic)9,048 bytes8,656 bytesRust (-C opt-level=3, no_std)4,216 bytes4,096 bytesC (-Oz)8,432 bytes8,440 bytesRust for MS-DOS (-C opt-level=s)
56 bytes

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

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

おわりに

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

追記(2019/12/28)

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

...

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 を呼ぶことにしました。修正版についてはこちらをご覧ください。

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

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

广告
将在 10 秒后关闭
bannerAds