no_stdのRust on LinuxでHello, world!する

# はじめに

この記事は[Rust Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rust) 9日目の記事です。

Linux上のRustをno_stdで動かしてみます。no_stdとは標準ライブラリを使わないということで、一般的なOSの上ではない環境などでRustを動作させたいときに使われます。今回はLinux上で動かすので別にまあ標準ライブラリを使えばいいんですが、こういったムダなことを通してRustの低レイヤ部分を学べたらいいなと思ってやってみました。

# とりあえずno_stdを付けてみる

no_stdでRustを使うには、ソースコードに`#![no_std]`で属性を付けてやります。`cargo new`のデフォルトで書かれている`println!`は標準ライブラリで定義されているため今回は削除しておきます。

```rust:main.rs
#![no_std]

fn main() {                                                                                       
} 
```

こんな感じで空っぽの`main`関数だけ定義して`cargo build`しますが、いろいろと怒られます。

まずは`#[panic_handler]`がないと言われます。Rustではパニック時の動作が定義されている必要があるようです。これは[Rustonomicon](https://doc.rust-lang.org/nomicon/panic-handler.html)に参考実装があるので、一番簡単なものをほぼそのまま使いましょう。

```rust
#[panic_handler]    
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
    loop {}
}
```

また、`eh_personality`がないとも言われます。これはパニック時にスタックを巻き戻すときに使われる関数のようですが、パニック時の動作をabortに変更することで必要なくなります。

```toml
[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"
```

これらを直すと次は`start`がないと言われます。これは[Unstable Book](https://doc.rust-lang.org/unstable-book/language-features/lang-items.html)に参考実装があり、`#[start]`を付けた関数を用意することで解決できます。ただし、これはまだ安定化されていないやり方なので、ツールチェインをnightlyとし、`#![feature(start)]`を付けておく必要があります。

```rust
#![feature(start)]

#[start]
fn rust_main(_argc: isize, _argv: *const *const u8) -> isize {
    0
}
```

ここまでの修正を合わせて、main.rsはこのようになっています。

```rust:main.rs
#![feature(start)]
#![no_std]

#[panic_handler]
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
    loop {}
}

#[start]
fn rust_main(_argc: isize, _argv: *const *const u8) -> isize {
    0
}
```

# 標準Cライブラリと戦う

さて、上記のmain.rsを`cargo build`してみましょう。すると、このようなエラーが出力されました。省略部分には`cc`に続くオプションが大量に並んでいますが、絶対パスなども含まれているためここでは載せていません。

```
error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" ...省略...
  = note: /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
          (.text+0x12): undefined reference to `__libc_csu_fini'
          /usr/bin/ld: (.text+0x19): undefined reference to `__libc_csu_init'
          /usr/bin/ld: (.text+0x26): undefined reference to `__libc_start_main'
          collect2: error: ld returned 1 exit status

```

今回の環境（普通のLinuxシステム）では、`cc`の実体は`gcc`です。その実行中にリンカ（`ld`）がエラーを出しているようです。Rustコンパイラはリンクの工程を外部のツールに頼っており、Linuxシステムではそれは大抵`gcc`（`ld`）でしょう。つまり、Rustとしてのコンパイルは完了しているが、実行形式を出力するためのリンクが失敗していると考えてよさそうです。

見つからないシンボルは、`__libc_csu_fini`、`__libc_csu_init`、`__libc_start_main`です。これらは名前からも察せられるように、標準Cライブラリに含まれる関数です。ここで、上記では省略した`cc`のオプションを確認すると`-nodefaultlibs`が付いていることが分かります。これは、通常デフォルトでリンクされる標準Cライブラリをリンクしないようにする[GCCのオプション](https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html)です。`#![no_std]`が付いているときにはRustの標準ライブラリを使わないだけでなく、Cの標準ライブラリも使わないように設定されるというのは自然な流れでしょう。

ちなみに、見つからないシンボルを使おうとしている`_start`はどこから来ているのでしょうか。実はこれも標準Cライブラリの一部ではあるのですが、スタートアップの処理（Scrt1.o）はメインのライブラリ（libc.so）とは分離されており、`-nodefaultlibs`ではlibc.soしか除去されません。Scrt1.oも取り除くには`-nostartfiles`などのオプションを付ける必要があります。ですが、今回はこのまま進めていくことにしましょう。

標準Cライブラリの関数が見つからないためにリンクが失敗していることは分かりました。これはもちろん、libc.soをリンクすることで修正することができます。それは例えば、build.rsにこのような処理を追加することで可能です。

```rust:build.rs
fn main() {
    println!("cargo:rustc-link-lib=c");
}
```

ですが、せっかくno_stdでやっているのにCの標準ライブラリを使いたくないですね。シンボルがないなら自分で作ればいいわけです。作る準備として、それぞれの関数について少し確認してみましょう。

```
0000000000001040 <_start>:
    1040:       31 ed                   xor    ebp,ebp
    1042:       49 89 d1                mov    r9,rdx
    1045:       5e                      pop    rsi
    1046:       48 89 e2                mov    rdx,rsp
    1049:       48 83 e4 f0             and    rsp,0xfffffffffffffff0
    104d:       50                      push   rax
    104e:       54                      push   rsp
    104f:       4c 8d 05 8a 02 00 00    lea    r8,[rip+0x28a]        # 12e0 <__libc_csu_fini>
    1056:       48 8d 0d 23 02 00 00    lea    rcx,[rip+0x223]        # 1280 <__libc_csu_init>
    105d:       48 8d 3d 7c 01 00 00    lea    rdi,[rip+0x17c]        # 11e0 <main>
    1064:       ff 15 76 2f 00 00       call   QWORD PTR [rip+0x2f76]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    106a:       f4                      hlt    
    106b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

```

まずこれは、libc.soをリンクした場合のバイナリを`objdump -d`で出力したときの`_start`の処理です。`call`命令で実行しているのは`__libc_start_main`だけであることが分かります。また、glibc（2.28）の実装における`__libc_start_main`のシグネチャはこのようになっています。

```c:glibc/csu/libc-start.c
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
         int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
         ElfW(auxv_t) *auxvec,
#endif
         __typeof (main) init,
         void (*fini) (void),
         void (*rtld_fini) (void), void *stack_end) 
```

つまり、`__libc_csu_fini`と`__libc_csu_init`は`__libc_start_main`に引数として与えられ、`__libc_start_main`の中で実行されています。`__libc_csu_init`は初期化（initial）処理、`__libc_csu_fini`は終了（finish）処理をおそらく意味しており、どうもGCC拡張でmain関数前後のコンストラクタとデストラクタを定義できたりするみたいです。今回は使わなくてもよさそうなので、この2つは空の関数としてシンボルだけ用意することにします。

```rust
#[no_mangle]
fn __libc_csu_fini() {}

#[no_mangle]
fn __libc_csu_init() {}
```

続いて`__libc_start_main`ですが、まずは第一引数として`main`が渡されていることに注目します。これが実質、私たちが実行したい`main`関数になります。実質と言っているのは、ここでの`main`は`#[start]`を付けた`rust_main`ではなく、`rust_main`を呼びだすだけの（おそらくコンパイラにより定義された）`main`関数となっているためです。

コマンドライン引数はとりあえず使わないので一旦考えないようにしましょう。用意されている引数を使わないことによる悪影響はないはずです。また、単純な形のシグネチャなのでABIの考慮も後回しにしましょう。

```rust
#[no_mangle]    
fn __libc_start_main(main: fn() -> isize) {
    main();
}
```

結果、main.rsはこのようになりました。とりあえず`cargo build`は成功し、デバッガなどで確認すれば`rust_main`が呼ばれていることも確認できます。

```rust:main.rs
#![feature(start)]
#![no_std]

#[no_mangle]
fn __libc_csu_fini() {}

#[no_mangle]
fn __libc_csu_init() {}

#[no_mangle]
fn __libc_start_main(main: fn() -> isize) {
    main();
}

#[panic_handler]
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
    loop {}
}

#[start]
fn rust_main(_argc: isize, _argv: *const *const u8) -> isize {
    0
}
```

# システムコールを使う

現状のmain.rsは正しく終了できていないため、最終的には`_start`にある`hlt`命令でSegmentation faultになってしまいます。ですが、とりあえずそれは放置して`rust_main`の処理を作ってしまいましょう。

「Hello, world!」という文字列を出力したいわけですが、入出力にはOSの協力が必要なのでLinuxのシステムコールを叩く必要があります。しかし今の環境にはRustの標準ライブラリはおろか、Cレベルのシステムコールラッパーを提供している標準Cライブラリもありません。つまり、アセンブリレベルでシステムコールを叩く必要があります。

Rustでは最近新しいインラインアセンブリのマクロ`asm!`がnightlyで提供されるようになりました。それを紹介する[Inside Rust Blogの記事](https://blog.rust-lang.org/inside-rust/2020/06/08/new-inline-asm.html)で、まさに今やろうとしているアセンブリが参考実装として記載されています。ほぼそのまま持ってきてしまいましょう。

```rust
#![feature(start, asm)]

#[start]
fn rust_main(_argc: isize, _argv: *const *const u8) -> isize {
    let buf = "Hello, world!\n";
    let ret: isize;
    unsafe {
        asm!(
            "syscall",
            in("rax") 1, // write
            in("rdi") 1, // stdout
            in("rsi") buf.as_ptr(),
            in("rdx") buf.len(),
            out("rcx") _, // destroyed in kernel
            out("r11") _, // destroyed in kernel
            lateout("rax") ret,
        );
    }
    ret
}
```

`asm!`もまだnightlyでしか使えないため、`#![feature(asm)]`が必要です。また、アセンブリの中身はRustで面倒を見てくれないため、当然`unsafe`になります。

ではアセンブリの中身を見ていきましょう。`syscall`はシステムコールを発行するための命令です。（システムコール専用のソフトウェア割り込みみたいな感じだと思います。）そして`rax`にはシステムコール番号と呼ばれる、システムコールを一意に識別するための番号が入ります。`rdi`、`rsi`、`rdx`には引数が入ります。`rcx`と`r11`はカーネル内で破壊される可能性があるため使われないようにしておきます。最後に戻り値は`rax`に格納されています。

ここでの引数は`write(2)`の仕様に従います。システムコール仕様の確認はLinuxの[Man page](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/write.2.html)などを確認するのがいいでしょう。また、LinuxシステムコールにおけるABI（Application Binary Interface）仕様はglibcにまとまったコメントがあったので、こちらを参照するのも分かりやすいと思います。

```c:glibc/sysdeps/unix/sysv/linux/x86_64/sysdep.h
/* The Linux/x86-64 kernel expects the system call parameters in
   registers according to the following table:

    syscall number	rax
    arg 1		rdi
    arg 2		rsi
    arg 3		rdx
    arg 4		r10
    arg 5		r8
    arg 6		r9

    The Linux kernel uses and destroys internally these registers:
    return address from
    syscall		rcx
    eflags from syscall	r11
...
    Syscalls of more than 6 arguments are not supported.  */
```

要は、システムコール番号を`rax`に、引数をそれぞれ適切なレジスタに設定して`syscall`命令を発行すればいいようです。

ここで、システムコールの仕様はMan pageなどで確認できますが、システムコール番号までは記載されていません。また、Linuxのシステムコール番号はCPUアーキテクチャごとに異なっていますので、自分の環境における適切な番号を確認する必要があります。私の環境では/usr/include/x86_64-linux-gnu/asm/unistd_64.hに記載されているようでした。おそらくLinuxカーネルがユーザーランドに提供しているヘッダファイルでしょう。

```c:/usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
...
```

さて、これで「Hello, world!」はできていると思いますが、先ほど放置した終了処理についても考えていきましょう。glibcの`__libc_start_main`では`exit(3)`を最後に呼んでいるようでした。プロセスを終了させるので最終的に`_exit(2)`が呼ばれればよさそうです。先ほどの`write(2)`と同じ要領で、`__libc_start_main`の最後で`_exit(2)`を呼ぶようにしましょう。その際、`main`の戻り値を受け取ってその値を`_exit(2)`に渡すことにしてみます。

```rust
#[no_mangle]
fn __libc_start_main(main: fn() -> isize) {
    let ret = main();
    unsafe {
        asm!(
            "syscall",
            in("rax") 60, // _exit
            in("rdi") ret,
            out("rcx") _, // destroyed in kernel
            out("r11") _, // destroyed in kernel
        );
    }
}
```

これで`main`の実行後に正しくプロセスを終了できるようになりました。

最後におまけで、`__libc_start_main`でコマンドライン引数をちゃんと扱うようにして、念のため`extern`でC言語のABIを指定しておきましょう。完成形はこのようになりました。

```rust:main.rs
#![feature(start, asm)]
#![no_std]

#[no_mangle]
fn __libc_csu_fini() {}

#[no_mangle]
fn __libc_csu_init() {}

#[no_mangle]
extern "C" fn __libc_start_main(
    main: extern "C" fn(isize, *const *const u8) -> isize,
    argc: isize,
    argv: *const *const u8
) {
    let ret = main(argc, argv);
    unsafe {
        asm!(
            "syscall",
            in("rax") 60, // _exit
            in("rdi") ret,
            out("rcx") _, // destroyed in kernel
            out("r11") _, // destroyed in kernel
        );
    }
}

#[panic_handler]
fn panic(_panic: &core::panic::PanicInfo<'_>) -> ! {
    loop {}
}

#[start]
fn rust_main(_argc: isize, _argv: *const *const u8) -> isize {
    let buf = "Hello, world!\n";
    let ret: isize;
    unsafe {
        asm!(
            "syscall",
            in("rax") 1, // write
            in("rdi") 1, // stdout
            in("rsi") buf.as_ptr(),
            in("rdx") buf.len(),
            out("rcx") _, // destroyed in kernel
            out("r11") _, // destroyed in kernel
            lateout("rax") ret,
        );
    }
    ret
}
```

実行すると「Hello, world!」を出力し、戻り値もシェルに渡せていることが分かります。

```
$ cargo run -q
Hello, world!
$ echo $? 
14
```

# おわりに

スタック周りの設定とかいろいろやらなければいけないかと思っていましたが、Linuxはカーネルがきれいに設定してくれているおかげか、動かすだけならそこまで複雑にならずに実装できました。main関数の起動に関しては、`#![no_main]`を付けて`_start`に全部書いてしまったほうが楽だったかもしれないですね。まあC言語レベルのスタートアップ処理に少し詳しくなれたのでよしとしましょう。