Explore "Full-Stack" in depth!

情報系の専門学校で、今は機械学習に的を絞って学習中。プログラミングを趣味でやりつつ、IT系のあらゆる知識と技術を身に付けるべく奮闘中。

glibcラッパーからLinuxのシステムコールハンドラまでを読む,まとめる

概要

IPFactory Advent Calendar 2019 一日目.
急遽開いた弊サークルのカレンダー,既に一日目が終わろうとしている.

ここでは日頃勉強の内容をMarkdownに書き溜めているリポジトリから,
Linuxにおけるシステムコールの流れのメモを取り出して紹介しよう.

誰も投稿しないよりよっぽどマシだし,
おそらく誰かの何かになれると思う.


システムコールの流れ

アプリケーションプログラムがシステムコールを発行した時,
内部ではどのようなフローをたどるのかについて解説する.
これを一度理解しておくことで,
ユーザランドカーネルランドのインタフェースについて理解を深められる.

glibcでのシステムコールラッパーの処理

まず,ユーザアプリケーションでシステムコールを呼び出す時,
往々にして glibc等で定義されたシステムコールラッパー を利用する.
後々実際に見ていくが,
このラッパーは内部で syscall 命令 を実行している.

例えば brk(2) は以下のようなラッパーが定義されている.

/* This must be initialized data because commons can't have aliases.  */
void *__curbrk = 0;
int
__brk (void *addr)
{
  void *newbrk;
  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);
  if (newbrk < addr)
    {
      __set_errno (ENOMEM);
      return -1;
    }
  return 0;
}
weak_alias (__brk, brk)

このコードで重要なのは,
INLINE_SYSCALL (brk, 1, addr); マクロの実行である.

# define INLINE_SYSCALL(name, nr, args...) \
  ({                                                                              \
    unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);              \
    if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, )))              \
      {                                                                              \
        __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, ));                      \
        resultvar = (unsigned long int) -1;                                      \
      }                                                                              \
    (long int) resultvar; })

少し見づらいが,簡単にまとめる.

  • INTERNAL_SYSCALL マクロでシステムコールを実行する
    • このマクロについては後述
    • INLINE_SYSCALL の第一引数を直接受け取る( 上記例なら brk )
    • INTERNAL_SYSCALL の第三引数に引数の個数が渡る( 仮引数名 -> nr )
  • INTERNAL_SYSCALL_ERROR_P はエラーチェック

INTERNAL_SYSCALL 内部について見てみる.

#define INTERNAL_SYSCALL(name, err, nr, args...)                        \
        internal_syscall##nr (SYS_ify (name), err, args)

引数で渡された nrinternal_syscall が結合される.
つまり, 1 が渡されれば internal_syscall1() という関数マクロの呼び出しになる.

#define SYS_ify(syscall_name)        __NR_##syscall_name

と定義されているので,brk(2)における INTERNAL_SYSCALL の呼び出しは次のようになる.
(__NR_brk は 12 とマクロ定数で定義されている).

internal_syscall1(12, , addr)
#undef internal_syscall1
#define internal_syscall1(number, err, arg1)                                \
({                                                                        \
    unsigned long int resultvar;                                        \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);                                 \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;                        \
    asm volatile (                                                        \
    "syscall\n\t"                                                        \
    : "=a" (resultvar)                                                        \
    : "0" (number), "r" (_a1)                                                \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);                        \
    (long int) resultvar;                                                \
})

syscall 命令の実行が確認できる.

syscall 命令の実行

Intel x64 SDM を読むと,
syscall 命令時には IA32_LSTAR というレジスタの値を RIP に入れていることが分かる.
なんとなくこの IA32_LSTARLinuxカーネルシステムコールハンドラ(のアドレス)が入っていそうだなあ,という予感がする

linux/arch/x86/kernel/cpu/common.c を見ると,
syscall_init() 関数を発見できる.
この関数は linux/arch/x86/kernel/cpu/common.ccpu_init() で呼ばれている.

/* May not be marked __init: used by software suspend */
void syscall_init(void)
{
    wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
    wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

Linuxシステムコールハンドラ

MSR_LSTARentry_SYSCALL_64 というアドレスを格納している.
このシンボルが システムコールハンドラ だと推測できる.
linux/arch/x86/entry/entry_64.S を見てみる.

SYM_CODE_START(entry_SYSCALL_64)
    UNWIND_HINT_EMPTY
    /*
    * Interrupts are off on entry.
    * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
    * it is too small to ever cause noticeable irq latency.
    */

    swapgs

swapgs 命令によって, IA32_KERNEL_GS_BASE; に格納されたカーネルデータ構造へのポインタを
gs レジスタに格納できる.

/* Construct struct pt_regs on stack */
    pushq    $__USER_DS               /* pt_regs->ss */
    pushq    PER_CPU_VAR(cpu_tss_rw + TSS_sp2)  /* pt_regs->sp */
    pushq    %r11                 /* pt_regs->flags */
    pushq    $__USER_CS               /* pt_regs->cs */
    pushq    %rcx                 /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
    pushq    %rax                 /* pt_regs->orig_ax */

    PUSH_AND_CLEAR_REGS rax=$-ENOSYS

    TRACE_IRQS_OFF

    /* IRQs are off. */
    movq %rax, %rdi
    movq %rsp, %rsi
    call do_syscall_64        /* returns with IRQs disabled */

LinuxにおけるC言語の呼び出し規約として,
第一引数は rdi , 第二引数は rsi レジスタを用いる.
つまり rax (先程のインラインアセンブリによるシステムコール番号)が第一引数,
rsp ( pt_regs 構造体がスタックにつまれていて,そのアドレス) が第二引数ということになる. そして呼ばれる do_syscall_64.

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    struct thread_info *ti;

    enter_from_user_mode();
    local_irq_enable();
    ti = current_thread_info();
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
        nr = syscall_trace_enter(regs);

    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
    } else if (likely((nr & __X32_SYSCALL_BIT) &&
              (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
        nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                    X32_NR_syscalls);
        regs->ax = x32_sys_call_table[nr](regs);
#endif
    }

    syscall_return_slowpath(regs);
}
#endif

sys_call_table から該当するシステムコールの番号で検索し,
対応する __x64_sys_name() の関数ポインタを取得, rax に入れる.


取り敢えずここまでで,

までの流れが確認できた.
ユーザランドカーネルランドの切り替わり部分が理解出来たので,良しとする.

後で更に深くまで書き足すかもしれないが,
~急にやることになった記事としては~ 悪くない.