44
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-08

はじめに

この記事はRust Advent Calendar 2020 9日目の記事です。

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

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

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

main.rs
#![no_std]

fn main() {                                                                                       
} 

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

まずは#[panic_handler]がないと言われます。Rustではパニック時の動作が定義されている必要があるようです。これはRustonomiconに参考実装があるので、一番簡単なものをほぼそのまま使いましょう。

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

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

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

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

#![feature(start)]

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

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

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システムではそれは大抵gccld)でしょう。つまり、Rustとしてのコンパイルは完了しているが、実行形式を出力するためのリンクが失敗していると考えてよさそうです。

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

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

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

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のシグネチャはこのようになっています。

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つは空の関数としてシンボルだけ用意することにします。

#[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の考慮も後回しにしましょう。

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

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

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の記事で、まさに今やろうとしているアセンブリが参考実装として記載されています。ほぼそのまま持ってきてしまいましょう。

#![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にはシステムコール番号と呼ばれる、システムコールを一意に識別するための番号が入ります。rdirsirdxには引数が入ります。rcxr11はカーネル内で破壊される可能性があるため使われないようにしておきます。最後に戻り値はraxに格納されています。

ここでの引数はwrite(2)の仕様に従います。システムコール仕様の確認はLinuxのMan pageなどを確認するのがいいでしょう。また、LinuxシステムコールにおけるABI(Application Binary Interface)仕様はglibcにまとまったコメントがあったので、こちらを参照するのも分かりやすいと思います。

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カーネルがユーザーランドに提供しているヘッダファイルでしょう。

/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)に渡すことにしてみます。

#[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を指定しておきましょう。完成形はこのようになりました。

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言語レベルのスタートアップ処理に少し詳しくなれたのでよしとしましょう。

44
33
0

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
44
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?