🌌

Auxiliary Vector によるプロセス起動時の値渡しと Redox OS でのユースケース

に公開

はじめに

こんにちは、isan です。
私は、現在ネームスペースマネージャ in ユーザスペースの開発を行っております。
まだ開発は完了していないため、開発中に出会った興味深い技術について記事にしたいと思います。
今回は、Auxiliary Vector について記事にしてみました。

この記事の目的

この記事では、プロセス起動時値渡し方法の一つである Auxiliary Vector(auxv) について解説するとともに、Redox OS でのユースケースについて紹介します。
この記事では、次の流れで説明します。

  • プロセス起動時値渡し方法について
  • Auxiliary Vector とは
  • Redox OS でのユースケース
  • ネームスペースマネージャ in ユーザスペースでの活用

プロセス起動時値渡しについて

みなさんプロセスを生成する際に値を渡したいことがあると思います。
目的は色々ありますが、渡す方法はあまり多くありません。例えば、次のようなものが考えられると思います。

  • コマンドライン引数: ./program arg1 arg2 のように、ユーザが実行時に直接文字列として渡す最も一般的な方法です。
  • 環境変数: 親プロセスの環境変数を引き継ぐか、新しい変数を設定して渡します。設定パスなど、環境に関する情報を渡すのに使われます。

これらはプログラムそれぞれが固有に要求するものです。
実行されるプログラムがどのような環境や用途で使用されることを目的としているかによって、渡し方が変わるでしょう。
では、OS レベルの情報等すべてのプロセスに共通して必要なデータを渡すには、どんな方法があるでしょうか?
この用途に使用されるものが、Auxiliary Vector です。

Auxiliary Vector とは

Auxiliary Vector(auxv) とは、OS がプロセス起動時に引数や環境変数とともに OS レベルの情報を渡すためのもので、キーと値のペアによって構成されます。
実際に見てみましょう。LD_SHOW_AUXV=1 をつけてバイナリを実行すると実際に設定されている値を見ることができます。

$ LD_SHOW_AUXV=1 /bin/echo
AT_SYSINFO_EHDR:      0x7fff95ab7000
AT_MINSIGSTKSZ:       1440
AT_HWCAP:             178bfbfd
AT_PAGESZ:            4096
AT_CLKTCK:            100
AT_PHDR:              0x5ce1f5d3d040
AT_PHENT:             56
AT_PHNUM:             13
AT_BASE:              0x7564ef508000
AT_FLAGS:             0x0
AT_ENTRY:             0x5ce1f5d40010
AT_UID:               1000
AT_EUID:              1000
AT_GID:               1000
AT_EGID:              1000
AT_SECURE:            0
AT_RANDOM:            0x7fff95a5ae39
AT_HWCAP2:            0x0
AT_EXECFN:            /bin/echo
AT_PLATFORM:          x86_64
AT_??? (0x1b): 0x1c
AT_??? (0x1c): 0x20

プログラムのエントリポイント (AT_ENTRY) やプログラムのファイル名 (AT_EXECFN)、CPU アーキテクチャ (AT_PLATFORM) が含まれているようです。
これら、すべてのプロセスに共通するような OS レベルの情報が引数や環境変数で渡されていたらとてもごちゃごちゃしますし、auxv で渡されるのはとても理にかなっていますね。
これらの値は、getauxval という関数を介して取得することができます。

Linux で定義されているキーは linux/include/uapi/linux/auxvec.h で確認することができます。他のどのようなものがあるか気になる方はご確認ください。

AT_UID は uid を渡すのに使用されているようですが、getuid の実装を見てもこの値は使用されていないようです。AT_UID を含めそれらの ID を渡すキーは具体的に何に使用されているのでしょうか?

glibc はいくつかの値の存在を前提としているらしく、存在しない場合はクラッシュします。
実際に、すずき氏による glibcとAuxiliary Vector という記事で、auxv の書き換えによって glibc がクラッシュしているのを見ることができます。

auxv の用途と利点が理解できたと思います。
それでは、Redox OS で特にどのように利用されているかをみていきましょう。

Redox OS でのユースケース

Redox OS で auxv は他の OS と同様のデータとともに Redox OS 固有のデータを渡すために使用されています。
特に注目すべきは、デフォルトスキームとプロセス / スレッド FD の値渡しです。
Redox OS の標準 C ライブラリである relibc における fexec_imple() の実装と、init() の実装を見ながら確認していきましょう。

デフォルトスキームの値渡し

まず、デフォルトスキームについて説明します。
Redox OS にはスキーム[1]という概念が存在します。
ファイルやデバイスなどの各リソースは、それを司るスキームによって管理され、"Scheme-rooted Path" でアクセスされます。
例えば、ファイルは、/scheme/file/path/to/file、ネットワークソケットは /scheme/udp/192.0.2.0/24といった感じです。
この時、/scheme/{scheme-name} のプレフィックスをつけずにパスを指定すると、システム側がプレフィックスをデフォルト値で補完します。この際付加されるスキームをデフォルトスキームと呼びます。ほとんどのプロセスにおいて、デフォルトスキームは RedoxFS が提供する "file" スキームです。
Redox OS では、auxv を用いてこのデータを渡しています。
fexec_impl() を確認すると、デフォルトスキームは文字列であるため、ポインタと長さをそれぞれ auxv に追加しています。

relibc/redox-rt/src/proc.rs
pub fn fexec_impl<A, E>(
~
        if let Some(default_scheme) = extrainfo.default_scheme {
            push(append(default_scheme)?)?;
            push(AT_REDOX_INITIAL_DEFAULT_SCHEME_PTR)?;
            push(default_scheme.len())?;
            push(AT_REDOX_INITIAL_DEFAULT_SCHEME_LEN)?;
        }
~
}

init() で auxv から文字列を復元し、set_default_scheme_manual を呼び出し、設定しています。

relibc/src/platform/mod.rs
pub unsafe fn init(auxvs: Box<[[usize; 2]]>) {
~
    if let (Some(scheme_ptr), Some(scheme_len)) = (
        get_auxv(&auxvs, AT_REDOX_INITIAL_DEFAULT_SCHEME_PTR),
        get_auxv(&auxvs, AT_REDOX_INITIAL_DEFAULT_SCHEME_LEN),
    ) {
        let scheme_bytes: &'static [u8] =
            unsafe { core::slice::from_raw_parts(scheme_ptr as *const u8, scheme_len) };
        if let Ok(scheme) = core::str::from_utf8(scheme_bytes) {
            self::sys::path::set_default_scheme_manual(scheme.into());
        }
    }
~
}

プロセス・スレッド FD の値渡し

プロセス・スレッド FD は、名前の通り、それぞれスレッド / プロセスに紐づいたファイルディスクリプタです。
これらは waitpidexit といったプロセスやスレッドに関連した関数を実行するために用いられます。FD はプロセスのファイルテーブル[2]に存在し、使用するには index を知っている必要があります。relibc はこれらの FD index を内部で保存・使用して関数を提供しています。
例: waitpid の実装

relibc/redox-rt/src/sys.rs
pub fn this_proc_call(payload: &mut [u8], flags: CallFlags, metadata: &[u64]) -> Result<usize> {
    proc_call(**crate::current_proc_fd(), payload, flags, metadata)
}
pub fn sys_waitpid(target: WaitpidTarget, status: &mut usize, flags: WaitFlags) -> Result<usize> {
    let (call, pid) = match target {
        WaitpidTarget::AnyChild => (ProcCall::Waitpid, 0),
        WaitpidTarget::SingleProc { pid } => (ProcCall::Waitpid, pid),
        WaitpidTarget::AnyGroupMember => (ProcCall::Waitpgid, 0),
        WaitpidTarget::ProcGroup { pgid } => (ProcCall::Waitpgid, pgid),
    };
    wrapper(true, false, || {
        this_proc_call(
            unsafe { plain::as_mut_bytes(status) },
            CallFlags::empty(),
            &[call as u64, pid as u64, flags.bits() as u64],
        )
    })
}

fexec_impl() では、auxv に index をそのまま指定する形で渡しています。

relibc/redox-rt/src/proc.rs
pub fn fexec_impl<A, E>(
~
        push(extrainfo.thr_fd as usize)?;
        push(AT_REDOX_THR_FD)?;
        push(extrainfo.proc_fd as usize)?;
        push(AT_REDOX_PROC_FD)?;
~
}

init() では、index を読み取った後、プロセス FD を使用してプログラム実行前のセットアップを行っています。スレッド FD も libc の start で使用されていることが確認できます。

relibc/src/platform/mod.rs
pub unsafe fn init(auxvs: Box<[[usize; 2]]>) {
~
    let Some(proc_fd) = get_auxv(&auxvs, AT_REDOX_PROC_FD) else {
        panic!("Missing proc and thread fd!");
    };
    redox_rt::initialize(FdGuard::new(proc_fd));
}

ネームスペースマネージャ in ユーザスペースでの活用

最後にネームスペースマネージャ in ユーザスペースでどのように活用しているか紹介します。
ネームスペースマネージャでは、ネームスペース FD の値渡しに使用する予定です。
ネームスペース FD はプロセスが属しているネームスペースに紐づけられた FD です。
この FD はプロセスがリソースを開く際に使用し、ネームスペースマネージャがリクエストを仲介します。ネームスペースマネージャが実装された後は、プロセスはネームスペース FD を介さずにリソースを開くことは許可されません。
プロセス・スレッド FD を参考に、auxv で次のように定義すれば、値渡しの準備は完了です。

relibc/src/platform/auxv_defs.rs
#[cfg(target_os = "redox")]
pub const AT_REDOX_PROC_FD: usize = 41;
#[cfg(target_os = "redox")]
pub const AT_REDOX_THR_FD: usize = 42;
#[cfg(target_os = "redox")]
pub const AT_REDOX_NS_FD: usize = 43;

おわりに

いかがでしたか? この記事では、値渡し方法の一つである Auxiliary Vector と Redox OS でのユースケースについて紹介しました。
やはり、ただプログラムを開発しているだけではこういったものは気にしないのですが、少し目を向けてみると便利な仕組みに驚かされます。
また、libc は思考停止で使用してしまっているので、libc の初期化などに目を向けられてよかったです。

今回、ネームスペースマネージャ in ユーザスペースプロジェクト自体はあまり取り上げていませんが、興味深い話がたくさんあるのでまた記事にできればと思っています。
中でも、「 SYS_OPEN の削除」といったものも議論されています。
それでは、また次の記事でお会いしましょう!

脚注
  1. スキームに関して詳しく知りたい方は、公式ドキュメントか以前に執筆した記事Rust 製 OS、 Redox OS に UNIX ドメインソケットを実装したをご確認ください。 ↩︎

  2. Redox OS では、ユーザが明示的に使用する FD とプロセス FD やスレッド FD のような "Redox internal-use file descriptors" と呼ばれる、Redox OS が内部的に使用する FD を分けるためにファイルテーブル分割を行っています。詳しくは、以前に執筆した記事Rust 製 OS、Redox OS での一括 FD 送信実装とファイルテーブル分割をご確認ください。またコレに関連した issue もたっているので、興味があればご確認ください。Infinite loop from redox-rt in OpenSSH ↩︎

Discussion