📘

Redoxにおけるシステムコールの実装を読む 〜 x86_64編

2022/05/08に公開

この資料はRedoxを読む会 #2のための資料を兼ねています。

https://osdev-jp.connpass.com/event/246485/

RedoxというRustで書かれたOSのシステムコールの実装を読み解きます。Redoxは現在x86_64とaarch64アーキテクチャに対応していますが、
今回はx86_64アーキテクチャの実装を中心に読んでいきます。

Redoxのレポジトリはこちら。読んだときのコミットハッシュは0a63f024e9c824384344ede006d40805b53909db、tagの0.7.0の直後のコミットになっています。

https://gitlab.redox-os.org/redox-os/redox

主に見るのはkernelディレクトリ以下でこれはGitのサブモジュールとして管理されています。

https://gitlab.redox-os.org/redox-os/kernel

以降、ソースコードの引用時、コメント部分は適宜取り除いたり、筆者によるコメントに置き換えている場合があります。

syscall呼び出し側

ユーザープログラムからの呼び出しの実装はsyscallディレクトリ以下にあります。これはkernel以下のGitのサブモジュールとして管理されていて別のクレートになっています。

https://gitlab.redox-os.org/redox-os/syscall

kernel/syscall/src/call.rsを見るとどんなシステムコールがあるかを見ることができます。
基本的には引数の数に合わせてsyscall1syscall3と言った関数があり、それを呼び出すためのラッパになっています。
例としてopen関数を見てみましょう。

pub fn open<T: AsRef<str>>(path: T, flags: usize) -> Result<usize> {
    unsafe { syscall3(SYS_OPEN, path.as_ref().as_ptr() as usize, path.as_ref().len(), flags) }
}

syscall3は第一引数にシステムコールの種類を示すIDを受け取り、残りはusizeとして3つのそのシステムコールのための引数を受け取る関数になっています。
syscall3そのものはアーキテクチャ依存の実装になっています。x86_64のものはkernel/syscall/src/arch/x86_64.rsに実装されていてこのようになっています。

macro_rules! syscall {
    ($($name:ident($a:ident, $($b:ident, $($c:ident, $($d:ident, $($e:ident, $($f:ident, )?)?)?)?)?);)+) => {
        $(
            pub unsafe fn $name(mut $a: usize, $($b: usize, $($c: usize, $($d: usize, $($e: usize, $($f: usize)?)?)?)?)?) -> Result<usize> {
                asm!(
                    "syscall",
                    inout("rax") $a,
                    $(
                        in("rdi") $b,
                        $(
                            in("rsi") $c,
                            $(
                                in("rdx") $d,
                                $(
                                    in("r10") $e,
                                    $(
                                        in("r8") $f,
                                    )?
                                )?
                            )?
                        )?
                    )?
                    out("rcx") _,
                    out("r11") _,
                    options(nostack),
                );

                Error::demux($a)
            }
        )+
    };
}

syscall! {
    syscall0(a,);
    syscall1(a, b,);
    syscall2(a, b, c,);
    syscall3(a, b, c, d,);
    syscall4(a, b, c, d, e,);
    syscall5(a, b, c, d, e, f,);
}

マクロになっていて読みにくいですね。例としてsyscall3がこのマクロによってどのような定義になるか展開します。

pub unsafe fn sycall3(mut a: usize, b: usize, c: usize, d: usize) -> Result<usize> {
    asm!(
        "syscall",
        inout("rax") a,
        in("rdi") b,
        in("rsi") c,
        in("rdx") d,
        out("rcx") _,
        out("r11") _,
        options(nostack),
    );

    Error::demux(a)
}

インラインアセンブラによってsyscallという命令を呼んでいます。これはユーザーランドからカーネルの機能を呼び出すためのFast System Callsという仕組みのための命令です。
これを呼び出すことによってユーザーランドから特権レベルの特定のルーチンに飛ぶことができます。

レジスタがinoutなどで指定されています。RAXは関数の戻り値を格納するためのレジスタと呼び出し規約ではなっていますが、このシステムコールでは同時にシステムコールのIDを渡す役割も担っているようです。
RDI、RSI、RDXは関数の引数を渡すためのレジスタなのでそのままの使い方ですね。outとしてRCXとR11が指定されています。
実はsycall命令はこれらのレジスタを上書きするのでこれらの値が破壊されることをコンパイラに伝えるための書き方です。
options(nostack)はこのインラインアセンブラ内でスタックに値を追加しないことを教えるためのオプションです。

呼び出し側の実装はこのようになっていて、次にカーネル側でどのように処理されるかを見ていきましょう。

syscallのための初期設定

syscall時にどのような動きをするかはIntel SDM Vol.3 Chapter 5.8.8を見るとわかります。
システムコールは一見すると例外や割り込みの一種に見えるかもしれませんが、x86_64アーキテクチャでは別物であることに注意してください。

kernel/src/arch/x86_64/interrupt/syscall.rsより

pub unsafe fn init() {
    // IA32_STAR[31:0] are reserved.

    // syscallでprivileged level 0になるときのコードセグメントセレクタ
    let syscall_cs_ss_base = (gdt::GDT_KERNEL_CODE as u16) << 3;
    // sysretでprivileged level 3になるときのコードセグメントセレクタ
    let sysret_cs_ss_base = ((gdt::GDT_USER_CODE32_UNUSED as u16) << 3) | 3;
    let star_high = u32::from(syscall_cs_ss_base) | (u32::from(sysret_cs_ss_base) << 16);

    msr::wrmsr(msr::IA32_STAR, u64::from(star_high) << 32);
    msr::wrmsr(msr::IA32_LSTAR, syscall_instruction as u64);
    msr::wrmsr(msr::IA32_FMASK, 0x0300); // Clear trap flag and interrupt enable

    let efer = msr::rdmsr(msr::IA32_EFER);
    msr::wrmsr(msr::IA32_EFER, efer | 1);
}

wrmsr命令でMSR(Model Specific Register)というレジスタに値を書き込みます。

syscall_cs_ss_baseはsyscallが呼ばれたときに使われるコードセグメントとスタックセグメントを指定するためのセグメントセレクタです。
コードセグメントはIA32_STAR[47:32]の設定値で、でスタックセグメントはIA32_STAR[47:32]の設定値+8の値となります。そのため、GDTではこの2つのセグメントは連続しています。
逆にsysret_cs_ss_baseはsysretが呼ばれたときに使われるコードセグメントとスタックセグメントを指定するためのセグメントセレクタです。
実際のコードセグメントはIA32_STAR[63:48]の設定値の+16でスタックセグメントはIA32_STAR[63:48]の設定値+8の値となり、その1つ手前のインデックスを使っています。

セグメントセレクタの構造として[15:3]がインデックスとして使われ、[1:0]が特権レベルを指します。
そのためsyscall_cs_ss_baseではレベル0に突入するため0になっていますが、
sysret_cs_ss_baseはレベル3に戻るため3を付け加えています。

ちなみにGDTの定義及びその初期化はkernel/src/arch/x86_64/gdt.rsで行われています。
GDTの詳しい解説はSDMの他に英語ですがこちらの資料も参考になります。

https://wiki.osdev.org/Global_Descriptor_Table

FMASKはRFLAGSの現在値をマスクするための値です。第8ビットと第9ビットはそれぞれトラップと割り込みの有効化のビットなのでシステムコール中はこれらを無効にするということになります。
各フィールドについてはIntel SDM Vol.3 Chapter 2.3とFigure 2-5を参照しましょう。

syscallハンドラ

syscallが発行されたときに呼び出されるコード関数がsyscall_instructionになります。
長いのでいくつかに分けて順番に見ていきましょう。
まず最初の"magic"と言われている箇所を見ていきます。"you don't need to understand"と書いてありますが、理解します。

#[naked]
pub unsafe extern "C" fn syscall_instruction() {
    core::arch::asm!(concat!(
    // Yes, this is magic. No, you don't need to understand
    "
        swapgs                    // Set gs segment to TSS
        mov gs:[{sp}], rsp        // Save userspace stack pointer
        mov rsp, gs:[{ksp}]       // Load kernel stack pointer
        push QWORD PTR {ss_sel}   // Push fake userspace SS (resembling iret frame)
        push QWORD PTR gs:[{sp}]  // Push userspace rsp
        push r11                  // Push rflags
        push QWORD PTR {cs_sel}   // Push fake CS (resembling iret stack frame)
        push rcx                  // Push userspace return pointer
    ",

#[naked]はRust特有の呼び出し規約によるレジスタ退避のロジックを余計に埋め込まれるのを防ぐためのアトリビュートです。
このインラインアセンブラはnoreturnがつけられていて、このインラインアセンブラ内で呼び出し元に復帰します。そのため、Rustが勝手にレジスタ退避のロジックを埋め込んでしまうと、それらを取り出す処理が入らなくなりスタックに余計なものが積まれた状態で呼び出し元に復帰してしまいます。

swapgsはGSというセグメンテーションレジスタとIA32_GS_BASEというMSRのフィールドをスワップする命令です。セグメンテーションレジスタは他にもいくつかありますが、このような命令が備わっているのはGSのみです。
SDMの解説でもこれはsyscallのような場面で使うことを想定されているようです。
カーネルがsyscallを処理する時、カーネルのスタックポインタを取得するすべがないのでこのような命令が用意されています。
ちなみにGSの初期値やIA32_GS_BASEの設定はkernel/src/arch/x86_64/gdt.rsinit_pagingで行われています。

{sp}といった書き方はRustにおけるformatマクロなどと同じルールで、後ろの方で渡される値が代入されます。
値が渡されているところだけ先に見ましょう。

    sp = const(offset_of!(gdt::ProcessorControlRegion, user_rsp_tmp)),
    ksp = const(offset_of!(gdt::ProcessorControlRegion, tss) + offset_of!(TaskStateSegment, rsp)),
    ss_sel = const(SegmentSelector::new(gdt::GDT_USER_DATA as u16, x86::Ring::Ring3).bits()),
    cs_sel = const(SegmentSelector::new(gdt::GDT_USER_CODE as u16, x86::Ring::Ring3).bits()),

offset_ofマクロは第一引数に何らかの構造体を受け取り、第二引数はその構造体のメンバーを受け取り、その構造体におけるそのメンバーまでのオフセットを計算してくれます。
ProcessorControlRegionの定義はこのようになっています。

#[repr(C, align(16))]
pub struct ProcessorControlRegion {
    pub tcb_end: usize,
    pub user_rsp_tmp: usize,
    pub tss: TssWrapper,
}

TssWrapperTaskStateSegmentのラッパーになっています。

#[repr(C, align(16))]
pub struct TssWrapper(pub TaskStateSegment);
#[derive(Clone, Copy, Debug, Default)]
#[repr(C, packed)]
pub struct TaskStateSegment {
    pub reserved: u32,
    /// The full 64-bit canonical forms of the stack pointers (RSP) for privilege levels 0-2.
    pub rsp: [u64; 3],
    pub reserved2: u64,
    /// The full 64-bit canonical forms of the interrupt stack table (IST) pointers.
    pub ist: [u64; 7],
    pub reserved3: u64,
    pub reserved4: u16,
    /// The 16-bit offset to the I/O permission bit map from the 64-bit TSS base.
    pub iomap_base: u16,
}

これはTask State SegmentをRustで扱うための構造体です。Task State Segmentについての解説はこちらを参照してください。

https://wiki.osdev.org/Task_State_Segment

#[repr(C, align(16))]というのは構造体のABIをC言語と同じにして、更にそのアドレスを16バイトでアラインメントさせるためのアトリビュートです。
#[repr(packed)]は構想体のフィールドに余計なパディングなどを挟ませないようにするものです。
reprについての詳しい説明は以下のドキュメントが詳しいです。

https://doc.rust-lang.org/nomicon/other-reprs.html

つまり、GSはProcessorControlRegionへのポインタになっているので、
mov gs:[{sp}], rspはGSにおけるuser_rsp_tmpにRSPの値を入れる、つまりユーザースタックポインタの値を保存する操作にあたります。
mov rsp, gs:[{ksp}]はGSにおけるtss.rsp[0]の値をRSPに代入する、つまりカーネルのスタックポインタを持ってくる操作です。

その後いろいろなものをスタックに積んでいます。これはsyscallの呼び出し元への復帰がsysretqではなくiretqで行われる場合を考慮しているためです。
これはintel固有のsysretqの脆弱性に対処するためのものです。

https://xenproject.org/2012/06/13/the-intel-sysret-privilege-escalation/

簡単に説明すると、sysretqの実行時、RCXレジスタに不正なアドレスが入っている場合、General Protection例外が発生するのですが、intel製のチップの場合、この不正アドレスのチェックのタイミングの違いにより本来ユーザー権限で実行されるべきなのに特権レベルで実行されてしまうという脆弱性があるためです。

次のパートがシステムコールを処理する部分です。

    // Push context registers
    "push rax\n",
    push_scratch!(),
    push_preserved!(),

    // Call inner funtion
    "mov rdi, rsp\n",
    "call __inner_syscall_instruction\n",

    // Pop context registers
    pop_preserved!(),
    pop_scratch!(),

push_scratchはスクラッチレジスタ、つまりcaller-saved(呼び出し元が保存するべき)レジスタを保存しpush_preservedはcallee-saved(呼び出し先が保存するべき)なレジスタを保存しています。pop_preservedpop_scratchはその逆です。
callee-savedなレジスタは本来は保存する必要ないはずですが、__inner_syscall_instruction内で使う場面
push raxとRAXだけ別に保存してありますが、これは単にpush_scratchで保存するものの中にraxが含まれていないので追加しているので深い意味はないです。

__inner_syscall_instructionを呼び出していて、この先を追うとシステムコールがどのように処理されるかがわかります。
直前のmovは現在のスタック値を関数の引数として渡すためのものです。RDIが第一引数として使われます。
呼び出し先の処理を詳しく見る前に、先に残りのユーザースペースに戻る部分のコードを見ていきます。

    "
        // Set ZF iff forbidden bits 63:47 (i.e. the bits that must be sign extended) of the pushed
        // RCX are set.
        test DWORD PTR [rsp + 4], 0xFFFF8000

        // If ZF was set, i.e. the address was invalid higher-half, so jump to the slower iretq and
        // handle the error without being able to execute attacker-controlled code!
        jnz 1f

        // Otherwise, continue with the fast sysretq.

        pop rcx                 // Pop userspace return pointer
        add rsp, 8              // Pop fake userspace CS
        pop r11                 // Pop rflags
        pop QWORD PTR gs:[{sp}] // Pop userspace stack pointer
        mov rsp, gs:[{sp}]      // Restore userspace stack pointer
        swapgs                  // Restore gs from TSS to user data
        sysretq                 // Return into userspace; RCX=>RIP,R11=>RFLAGS

1:

        // Slow iretq
        xor rcx, rcx
        xor r11, r11
        swapgs
        iretq
    "),

    sp = const(offset_of!(gdt::ProcessorControlRegion, user_rsp_tmp)),
    ksp = const(offset_of!(gdt::ProcessorControlRegion, tss) + offset_of!(TaskStateSegment, rsp)),
    ss_sel = const(SegmentSelector::new(gdt::GDT_USER_DATA as u16, x86::Ring::Ring3).bits()),
    cs_sel = const(SegmentSelector::new(gdt::GDT_USER_CODE as u16, x86::Ring::Ring3).bits()),

    options(noreturn),
    );
}

これでsyscall_instruction全部です。
最初のtestjnzの分岐の部分が冒頭述べた脆弱性対策のRCXに対する値のチェックです。これが不正な値だった場合は1:にジャンプしてiretqによってリターンします。
そうでない場合は、余計にpushしていた値も含めレジスタの値をもとに戻しつつswapgsでGSの値を再び交換してsysretqで帰ります。
このときRCXの値は自動的にRIP、すなわちプログラムカウンタに、R11の値がRFLAGSに書き戻されます。

さて、__inner_syscall_instructionを見ていきましょう。

#[no_mangle]
pub unsafe extern "C" fn __inner_syscall_instruction(stack: *mut InterruptStack) {
    let _guard = ptrace::set_process_regs(stack);
    with_interrupt_stack!(|stack| {
        // Set a restore point for clone
        let rbp;
        core::arch::asm!("mov {}, rbp", out(reg) rbp);

        let scratch = &stack.scratch;
        syscall::syscall(scratch.rax, scratch.rdi, scratch.rsi, scratch.rdx, scratch.r10, scratch.r8, rbp, stack)
    });
}

引数として渡されていたのは呼び出し直前のカーネルスタックのアドレスでした。InterruptStackの構造はこのようになっています。

#[derive(Default)]
#[repr(packed)]
pub struct InterruptStack {
    pub preserved: PreservedRegisters,
    pub scratch: ScratchRegisters,
    pub iret: IretRegisters,
}

#[derive(Default)]
#[repr(packed)]
pub struct PreservedRegisters {
    pub r15: usize,
    pub r14: usize,
    pub r13: usize,
    pub r12: usize,
    pub rbp: usize,
    pub rbx: usize,
}

#[derive(Default)]
#[repr(packed)]
pub struct ScratchRegisters {
    pub r11: usize,
    pub r10: usize,
    pub r9: usize,
    pub r8: usize,
    pub rsi: usize,
    pub rdi: usize,
    pub rdx: usize,
    pub rcx: usize,
    pub rax: usize,
}

#[derive(Default)]
#[repr(packed)]
pub struct IretRegisters {
    pub rip: usize,
    pub cs: usize,
    pub rflags: usize,
    pub rsp: usize,
    pub ss: usize
}

ptraceに関するパートは深く突っ込まないことにします。これを解説するのはきっと他の人がやってくれるはずです。
大雑把に説明するとproc:スキーム用のクレートでブレイクポイント用の処理をいろいろやっています。

with_interrupt_stackというマクロが使われているので、中身を見てみましょう。

macro_rules! with_interrupt_stack {
    (|$stack:ident| $code:block) => {{
        let allowed = ptrace::breakpoint_callback(PTRACE_STOP_PRE_SYSCALL, None)
            .and_then(|_| ptrace::next_breakpoint().map(|f| !f.contains(PTRACE_FLAG_IGNORE)));

        if allowed.unwrap_or(true) {
            let $stack = &mut *$stack;
            (*$stack).scratch.rax = $code;
        }

        ptrace::breakpoint_callback(PTRACE_STOP_POST_SYSCALL, None);
    }}
}

codeの実行結果をstackポインタを利用してユーザー側のRAXが退避されているフィールドに代入しています。RAXは戻り値を代入するためのレジスタなので、システムコールの値を代入しているというわけです。
他の部分はptrace関連なのでとりあえず今回は深く突っ込まずにいます。

では$code:blockとして渡されている部分を改めて見ていきましょう。
RBPをrbpという変数に退避させています。このレジスタはフレームポインタといって関数の呼び出し元のスタックポインタが入っています。
通常であれば関数の呼び出し時・リターン時に自動的に更新されるものです。コメントにはcloneシステムコールのために保存していると書いてあります。
その後、syscall::syscallを呼び出しています。ここから先はアーキテクチャ依存のないRustの世界です。
引数として渡しているのはスタック上に積まれたユーザー側のスクラッチレジスタの値です。
呼び出し元でRAXにはシステムコールのIDが入っていましたね。RDIからR8は普通の引数で、RBPの値が入ったrbpとユーザー側の状態にアクセスするためのスタックポインタが引数として渡されています。

アキーテクチャー非依存の処理を眺める

syscall::syscallの中身を見ていくのですが、基本的にはsyscallのIDでの条件分岐で、数が多くややこしいので適宜省略して見ていきます。

pub fn syscall(a: usize, b: usize, c: usize, d: usize, e: usize, f: usize, bp: usize, stack: &mut InterruptStack) -> usize {
    #[inline(always)]
    fn inner(a: usize, b: usize, c: usize, d: usize, e: usize, f: usize, bp: usize, stack: &mut InterruptStack) -> Result<usize> {
        //SYS_* is declared in kernel/syscall/src/number.rs
        match a & SYS_CLASS {
            SYS_CLASS_FILE => {
                let fd = FileHandle::from(b);
                match a & SYS_ARG {
                    SYS_ARG_SLICE => match a {
                        SYS_FMAP if b == !0 => {
                            MemoryScheme::fmap_anonymous(unsafe { validate_ref(c as *const Map, d)? })
                        },
                        _ => file_op_slice(a, fd, validate_slice(c as *const u8, d)?),
                    }
                    SYS_ARG_MSLICE => match a {
                        SYS_FSTAT => fstat(fd, unsafe { validate_ref_mut(c as *mut Stat, d)? }),
                        _ => file_op_mut_slice(a, fd, validate_slice_mut(c as *mut u8, d)?),
                    },
                    _ => match a {
                        SYS_CLOSE => close(fd),
                        // 省略
                    }
                }
            },
            SYS_CLASS_PATH => match a {
                SYS_OPEN => open(validate_str(b as *const u8, c)?, d).map(FileHandle::into),
                SYS_CHMOD => chmod(validate_str(b as *const u8, c)?, d as u16),
                SYS_RMDIR => rmdir(validate_str(b as *const u8, c)?),
                SYS_UNLINK => unlink(validate_str(b as *const u8, c)?),
                _ => Err(Error::new(ENOSYS))
            },
            _ => match a {
                SYS_YIELD => sched_yield(),
                // 省略
                SYS_CLONE => {
                    let b = CloneFlags::from_bits_truncate(b);

                    #[cfg(not(target_arch = "x86_64"))]
                    {
                        //TODO: CLONE_STACK
                        let ret = clone(b, bp).map(ContextId::into);
                        ret
                    }

                    #[cfg(target_arch = "x86_64")]
                    {
                        let old_rsp = stack.iret.rsp;
                        if b.contains(flag::CLONE_STACK) {
                            stack.iret.rsp = c;
                        }
                        let ret = clone(b, bp).map(ContextId::into);
                        stack.iret.rsp = old_rsp;
                        ret
                    }
                },
                // 省略
                _ => Err(Error::new(ENOSYS))
            }
        }
    }


    // The next lines set the current syscall in the context struct, then once the inner() function
    // completes, we set the current syscall to none.
    //
    // When the code below falls out of scope it will release the lock
    // see the spin crate for details
    {
        let contexts = crate::context::contexts();
        if let Some(context_lock) = contexts.current() {
            let mut context = context_lock.write();
            context.syscall = Some((a, b, c, d, e, f));
        }
    }

    let result = inner(a, b, c, d, e, f, bp, stack);

    {
        let contexts = crate::context::contexts();
        if let Some(context_lock) = contexts.current() {
            let mut context = context_lock.write();
            context.syscall = None;
        }
    }

    // errormux turns Result<usize> into -errno
    Error::mux(result)
}

まず関数内でinnerという関数を定義しています。この関数はResult型を返しますが、最終的にError::muxによりエラーコードを示すusize型に変換されています。
IDにマスクをかけた結果によっての条件分岐がまず入ります。
これはファイルディスクリプタを扱うものか、ファイルのパスを扱うものか、それ以外かで分岐しているのがわかります。
今回は代表としてcloseopenを軽く見ていきましょう。突っ込みすぎると本質から外れていくのと自分の資料つくる時間が足りないので…

pub fn close(fd: FileHandle) -> Result<usize> {
    let file = {
        let contexts = context::contexts();
        let context_lock = contexts.current().ok_or(Error::new(ESRCH))?;
        let context = context_lock.read();
        context.remove_file(fd).ok_or(Error::new(EBADF))?
    };

    file.close()
}

contextというのはコンテキストスイッチのコンテキストでカーネルの様々な状態が格納されているものです。コンテキストの中には現在開いているファイルがVecで管理されています。
remove_fileの定義はこうなっています。

    pub fn remove_file(&self, i: FileHandle) -> Option<FileDescriptor> {
        let mut files = self.files.write();
        if i.into() < files.len() {
            files[i.into()].take()
        } else {
            None
        }
    }

FileHandleusizeに変換されて、そのインデックスのファイルがtakeによって取り出されています。

続いてopenを見ましょう。

pub fn open(path: &str, flags: usize) -> Result<FileHandle> {
    let (mut path_canon, uid, gid, scheme_ns, umask) = {
        let contexts = context::contexts();
        let context_lock = contexts.current().ok_or(Error::new(ESRCH))?;
        let context = context_lock.read();
        (context.canonicalize(path), context.euid, context.egid, context.ens, context.umask)
    };

    let flags = (flags & (!0o777)) | ((flags & 0o777) & (!(umask & 0o777)));


    for _level in 0..32 { // XXX What should the limit be?

        let mut parts = path_canon.splitn(2, ':');
        let scheme_name_opt = parts.next();
        let reference_opt = parts.next();

        let (scheme_id, file_id) = {
            let scheme_name = scheme_name_opt.ok_or(Error::new(ENODEV))?;
            let (scheme_id, scheme) = {
                let schemes = scheme::schemes();
                let (scheme_id, scheme) = schemes.get_name(scheme_ns, scheme_name).ok_or(Error::new(ENODEV))?;
                (scheme_id, Arc::clone(&scheme))
            };
            let reference = reference_opt.unwrap_or("");
            let file_id = match scheme.open(reference, flags, uid, gid) {
                Ok(ok) => ok,
                Err(err) => if err.errno == EXDEV {
                    let resolve_flags = O_CLOEXEC | O_SYMLINK | O_RDONLY;
                    let resolve_id = scheme.open(reference, resolve_flags, uid, gid)?;

                    let mut buf = [0; 4096];
                    let res = scheme.read(resolve_id, &mut buf);

                    let _ = scheme.close(resolve_id);

                    let count = res?;

                    let buf_str = str::from_utf8(&buf[..count]).map_err(|_| Error::new(EINVAL))?;

                    let contexts = context::contexts();
                    let context_lock = contexts.current().ok_or(Error::new(ESRCH))?;
                    let context = context_lock.read();
                    path_canon = context.canonicalize(buf_str);

                    continue;
                } else {
                    return Err(err);
                }
            };
            (scheme_id, file_id)
        };

        let contexts = context::contexts();
        let context_lock = contexts.current().ok_or(Error::new(ESRCH))?;
        let context = context_lock.read();
        return context.add_file(FileDescriptor {
            description: Arc::new(RwLock::new(FileDescription {
                namespace: scheme_ns,
                scheme: scheme_id,
                number: file_id,
                flags: flags & !O_CLOEXEC,
            })),
            cloexec: flags & O_CLOEXEC == O_CLOEXEC,
        }).ok_or(Error::new(EMFILE));
    }
    Err(Error::new(ELOOP))
}

context.canonicalize(path)pathの相対パスを絶対パスに変換するものです。このときスキーマも解決されます。
この絶対パスを元にスキーマとファイルの実体をとってこようとしています。
スキーマごとにopenが実装されていたのは、前回の発表で見ましたね。Schemeトレイトのメソッドの1つです。
EXDEVでのエラーが発生した場合、continueして再度openを試みています。
これはCross-device linkという別のデバイスのファイルにハードリンクされている場合発生するエラーのようです。
このへんの処理はちょっとよくわからなかったので、次以降の人の解説に期待です。

最終的にはcontextに新しいファイルディスクリプタを追加しておしまいです。

aarch64向けの実装は?

かなり軽くしか見ていないのですが、aarch64アーキテクチャではシステムコールは例外の一種であるので、普通の例外ハンドリングと大差ない感じで実装されていました。
そのため、x86_64よりはいろいろと楽そうに見えました。

参考になる資料

次の人向けのおもしろそうなネタ

  • ptrace。デバッグ周りの実装がどうなっているかを調べるのは楽しそう
  • コンテキストスイッチやスケジューリング周り
  • funmapシステムコール。メモリ管理周りのシステムコールは少し覗いたがややこしそう
  • その他特定のシステムコールを掘り下げるだけでも1回分になりそう

Discussion