はじめに
この記事は 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!
無事に動きました。 🎉🍻
結果
前回の結果に今回の結果を追記してみましょう。
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
以下は次のようになります。
...
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 を呼ぶことにしました。修正版についてはこちらをご覧ください。