はじめに
「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 で頑張ってみましょう。
まず必要なのはターゲットの定義です。
{
"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 です。
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 修正)
[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追記)
#![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 の定義だけで大丈夫です。
[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 なのに、違うことがやりたかったなあとちょっぴり思っています。