SIer だけど技術やりたいブログ

SYSCALL_DEFINE マクロは何をしてるのか

linux

はじめに

SYSCALL_DEFINE はシステムコールを定義するためのマクロ。引数の数によって SYSCALL_DEFINE0 から SYSCALL_DEFINE6 まであり、例えば次のように利用される。変数の型と名前が引数に別々に取られて独特な形で面食らうが、関数定義とは異なるので仕方ない。

fs/open.c

SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
        return ksys_chroot(filename);
}

カーネルのコードリーディングをする上で地味に重要(システムコール名の定義が文字列だから cscope などのシンボル検索にひっかからない。)。自分はシステムコールの中身を見ていくときは grep "SYSCALL_DEFINE" | grep <調べたいシステムコール名> で検索することが多い。

今回は SYSCALL_DEFINE が何しているのかを実装を読んで理解する。前提として以下を読んでいると理解しやすい。

Linux システムコール 徹底入門 - SIerだけど技術やりたいブログwww.kimullaa.com

まとめ

マクロは次のことを実現している。

  • システムコール実行時に共通的に追加したい機能(ftrace や error_injection など)を設定する。
  • システムコールの引数がレジスタ経由で渡されるため、これを関数が利用できるようにする。

検証環境

CentOS 8 を利用する。また CPU は x86_64 とする。

]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
]# uname -a
Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64 #1 SMP Fri Jan 3 23:55:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

実装を読む

SYSCALL_DEFINE1 から SYSCALL_DEFINE6include/linux/syscalls.h で定義されている。

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
...
#define SYSCALL_DEFINEx(x, sname, ...)                          \
        SYSCALL_METADATA(sname, x, __VA_ARGS__)                 \
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

SYSCALL_METADATA マクロ

CONFIG_FTRACE_SYSCALLS=y の場合、 tracepoint が利用できるようにメタデータを設定する。

  • __MAP(nb,__SC_STR_TDECL,__VA_ARGS__) マクロによって、SYSCALL__DEFINE(...) の型情報だけ抜き出す。
  • __MAP(nb,__SC_STR_ADECL,__VA_ARGS__) マクロによって、SYSCALL__DEFINE(...) の変数名だけ抜き出す。
  • SYSCALL_TRACE_ENTER_EVENT マクロによって、システムコール実行前のフックポイントを設定する。
  • SYSCALL_TRACE_EXIT_EVENT マクロによって、システムコール実行後のフックポイントを設定する。
#define SYSCALL_METADATA(sname, nb, ...)                        \
        static const char *types_##sname[] = {                  \
                __MAP(nb,__SC_STR_TDECL,__VA_ARGS__)            \
        };                                                      \
        static const char *args_##sname[] = {                   \
                __MAP(nb,__SC_STR_ADECL,__VA_ARGS__)            \
        };                                                      \
        SYSCALL_TRACE_ENTER_EVENT(sname);                       \
        SYSCALL_TRACE_EXIT_EVENT(sname);                        \
        static struct syscall_metadata __used                   \
          __syscall_meta_##sname = {                            \
                .name           = "sys"#sname,                  \
                .syscall_nr     = -1,   /* Filled in at boot */ \
                .nb_args        = nb,                           \
                .types          = nb ? types_##sname : NULL,    \
                .args           = nb ? args_##sname : NULL,     \
                .enter_event    = &event_enter_##sname,         \
                .exit_event     = &event_exit_##sname,          \
                .enter_fields   = LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
        };                                                      \
        static struct syscall_metadata __used                   \
          __attribute__((section("__syscalls_metadata")))       \
         *__p_syscall_meta_##sname = &__syscall_meta_##sname;
static inline int is_syscall_trace_event(struct trace_event_call *tp_event)
{
        return tp_event->class == &event_class_syscall_enter ||
               tp_event->class == &event_class_syscall_exit;
}

システムコール実行の流れ

マクロの定義を見る前に、システムコールの実行の流れをおさらいする。

  1. syscall_init() で sysenter 命令が実行されたときに entry_SYSCALL_64 が実行されるように設定する。
  2. ユーザ空間でレジスタを設定し、 sysenter 命令を実行する。
  3. arch/x86/entry/entry_64.S#entry_SYSCALL_64 が実行される。
  4. スタックの切り替え、レジスタに設定された引数から pt_regs を組み立てる。
  5. arch/x86/entry/common.c#do_syscall_64 を実行する。
  6. sys_call_table[nr](regs); を実行し、システムコールの本体を実行する。

sys_call_table

sys_call_table はシステムコール番号と関数の対応表。arch/x86/entry/syscall_64.c で定義されている。

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
        /*
         * Smells like a compiler bug -- it doesn't work
         * when the & below is removed.
         */
        [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

配列の定義部分で #include <asm/syscalls_64.h> している。このヘッダーファイルはビルド時に arch/x86/entry/syscalls/Makefilearch/x86/entry/syscalls/syscalltbl.sh が生成する。元の情報は ./arch/x86/entry/syscalls/syscall_64.tbl であり、ここにシステムコール番号と関数の対応が定義されている。

#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0       common  read                    __x64_sys_read
1       common  write                   __x64_sys_write
2       common  open                    __x64_sys_open
3       common  close                   __x64_sys_close
4       common  stat                    __x64_sys_newstat

手動でもヘッダーファイルが生成できるので、どんなファイルが生成されるかを確認しておく。

]# bash ./arch/x86/entry/syscalls/syscalltbl.sh arch/x86/entry/syscalls/syscall_64.tbl syscalls_64.h
]# cat syscalls_64.h | head
#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif
#ifdef CONFIG_X86
__SYSCALL_64(1, __x64_sys_write, )
#else /* CONFIG_UML */
__SYSCALL_64(1, sys_write, )
#endif

__SYSCALL_DEFINEx マクロ

これがシステムコールの本体。3つの関数(__x64_sys##name__se_sys##name__do_sys##name)が定義される。

  • __x64_sys##name

    • アセンブリから呼ばれるため、レジスタ経由で引数が渡せるように asmlinkage をつける。
    • pt_regs からシステムコール関数で利用する値を取り出して __se_sys##name を呼び出す。
  • __se_sys##name

    • __do_sys##name を呼び出す。
    • 呼び出しが適切に行われるように、tail call の最適化を防止する。
    • 型情報のチェックをする。
  • __do_sys##name

    • SYSCALL_DEFINEで定義した処理本体。
/*
 * Instead of the generic __SYSCALL_DEFINEx() definition, this macro takes
 * struct pt_regs *regs as the only argument of the syscall stub named
 * __x64_sys_*(). It decodes just the registers it needs and passes them on to
 * the __se_sys_*() wrapper performing sign extension and then to the
 * __do_sys_*() function doing the actual job. These wrappers and functions
 * are inlined (at least in very most cases), meaning that the assembly looks
 * as follows (slightly re-ordered for better readability):
 *
 * <__x64_sys_recv>:            <-- syscall with 4 parameters
 *      callq   <__fentry__>
 *
 *      mov     0x70(%rdi),%rdi <-- decode regs->di
 *      mov     0x68(%rdi),%rsi <-- decode regs->si
 *      mov     0x60(%rdi),%rdx <-- decode regs->dx
 *      mov     0x38(%rdi),%rcx <-- decode regs->r10
 *
 *      xor     %r9d,%r9d       <-- clear %r9
 *      xor     %r8d,%r8d       <-- clear %r8
 *
 *      callq   __sys_recvfrom  <-- do the actual work in __sys_recvfrom()
 *                                  which takes 6 arguments
 *
 *      cltq                    <-- extend return value to 64-bit
 *      retq                    <-- return
 *
 * This approach avoids leaking random user-provided register content down
 * the call chain.
 *
 * If IA32_EMULATION is enabled, this macro generates an additional wrapper
 * named __ia32_sys_*() which decodes the struct pt_regs *regs according
 * to the i386 calling convention (bx, cx, dx, si, di, bp).
 */
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long __x64_sys##name(const struct pt_regs *regs);    \
        ALLOW_ERROR_INJECTION(__x64_sys##name, ERRNO);                  \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));     \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
        asmlinkage long __x64_sys##name(const struct pt_regs *regs)     \
        {                                                               \
                return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
        }                                                               \
        __IA32_SYS_STUBx(x, name, __VA_ARGS__)                          \
        static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))      \
        {                                                               \
                long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
                __MAP(x,__SC_TEST,__VA_ARGS__);                         \
                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       \
                return ret;                                             \
        }                                                               \
        static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

__PROTECTinclude/linux/linkage.h に定義されている。これはコンパイラの最適化(tail call)を防ぐために必要らしい。pt_regs はレジスタ経由で渡されるので asmlinkage を付与するが、これが無視されないようにする。
参考 [PATCH 1/2] asmlinkage_protect replaces prevent_tail_call

/*
 * This is used by architectures to keep arguments on the stack
 * untouched by the compiler by keeping them live until the end.
 * The argument stack may be owned by the assembly-language
 * caller, not the callee, and gcc doesn't always understand
 * that.
 *
 * We have the return value, and a maximum of six arguments.
 *
 * This should always be followed by a "return ret" for the
 * protection to work (ie no more work that the compiler might
 * end up needing stack temporaries for).
 */
/* Assembly files may be compiled with -traditional .. */
#ifndef __ASSEMBLY__
#ifndef asmlinkage_protect
# define asmlinkage_protect(n, ret, args...)    do { } while (0)
#endif
#endif

ALLOW_ERROR_INJECTION は、フォールトインジェクション機能(システムコール単位でエラーを返せるようにしてカーネルデバッグに役立てる機能)を設定する。ただし CentOS8 だと FAULT_INJECTIONFAULT_INJECTION_DEBUG_FS が設定されていないため、利用できない。
参考 Fault injection capabilities infrastructure

マクロを展開してみる

このマクロを <https://www.kimullaa.com/entry/2020/05/17/111833 に従って展開してみる。

# gcc -Wp,-MD,fs/.open.o.d  -nostdinc -isystem /usr/lib/gcc/x86_64-redhat-linux/8/include -I./arch/x86/include -I./arch/x86/include/generated   -I./include/drm-backport -I./include -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -Werror-implicit-function-declaration -Wno-format-security -std=gnu89 -fno-PIE -DCC_HAVE_ASM_GOTO -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx -m64 -falign-jumps=1 -falign-loops=1 -mno-80387 -mno-fp-ret-in-387 -mpreferred-stack-boundary=3 -mskip-rax-setup -mtune=generic -mno-red-zone -mcmodel=kernel -funit-at-a-time -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -DCONFIG_AS_CFI_SECTIONS=1 -DCONFIG_AS_FXSAVEQ=1 -DCONFIG_AS_SSSE3=1 -DCONFIG_AS_CRC32=1 -DCONFIG_AS_AVX=1 -DCONFIG_AS_AVX2=1 -DCONFIG_AS_AVX512=1 -DCONFIG_AS_SHA1_NI=1 -DCONFIG_AS_SHA256_NI=1 -pipe -Wno-sign-compare -fno-asynchronous-unwind-tables -mindirect-branch=thunk-extern -mindirect-branch-register -fno-jump-tables -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-int-in-bool-context -O2 -Werror --param=allow-store-data-races=0 -Wframe-larger-than=2048 -fstack-protector-strong -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-var-tracking-assignments -g -gdwarf-4 -pg -mrecord-mcount -mfentry -DCC_USING_FENTRY -fno-inline-functions-called-once -Wdeclaration-after-statement -Wno-pointer-sign -Wno-stringop-truncation -fno-strict-overflow -fno-merge-all-constants -fmerge-constants -fno-stack-check -fconserve-stack -Werror=implicit-int -Werror=strict-prototypes -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -fmacro-prefix-map=./= -Wno-packed-not-aligned    -DKBUILD_BASENAME='"open"' -DKBUILD_MODNAME='"open"' -c -o fs/.tmp_open.i -E  fs/open.c

これを表示すると、次のようになる。

static const char *types__chroot[] = { "const char *" }; static const char *args__chroot[] = { "filename" }; static struct syscall_metadata __syscall_meta__chroot; static struct trace_event_call __attribute__((__used__)) event_enter__chroot = { .class = &event_class_syscall_enter, { .name = "sys_enter""_chroot", }, .event.funcs = &enter_syscall_print_funcs, .data = (void *)&__syscall_meta__chroot, .flags = TRACE_EVENT_FL_CAP_ANY, }; static struct trace_event_call __attribute__((__used__)) __attribute__((section("_ftrace_events"))) *__event_enter__chroot = &event_enter__chroot;; static struct syscall_metadata __syscall_meta__chroot; static struct trace_event_call __attribute__((__used__)) event_exit__chroot = { .class = &event_class_syscall_exit, { .name = "sys_exit""_chroot", }, .event.funcs = &exit_syscall_print_funcs, .data = (void *)&__syscall_meta__chroot, .flags = TRACE_EVENT_FL_CAP_ANY, }; static struct trace_event_call __attribute__((__used__)) __attribute__((section("_ftrace_events"))) *__event_exit__chroot = &event_exit__chroot;; static struct syscall_metadata __attribute__((__used__)) __syscall_meta__chroot = { .name = "sys""_chroot", .syscall_nr = -1, .nb_args = 1, .types = 1 ? types__chroot : ((void *)0), .args = 1 ? args__chroot : ((void *)0), .enter_event = &event_enter__chroot, .exit_event = &event_exit__chroot, .enter_fields = { &(__syscall_meta__chroot.enter_fields), &(__syscall_meta__chroot.enter_fields) }, }; static struct syscall_metadata __attribute__((__used__)) __attribute__((section("__syscalls_metadata"))) *__p_syscall_meta__chroot = &__syscall_meta__chroot; long __x64_sys_chroot(const struct pt_regs *regs); static struct error_injection_entry __attribute__((__used__)) __attribute__((__section__("_error_injection_whitelist"))) _eil_addr___x64_sys_chroot = { .addr = (unsigned long)__x64_sys_chroot, .etype = EI_ETYPE_ERRNO, };; static long __se_sys_chroot(__typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))), 0LL, 0L)) filename); static inline __attribute__((unused)) __attribute__((no_instrument_function)) long __do_sys_chroot(const char * filename); long __x64_sys_chroot(const struct pt_regs *regs) { return __se_sys_chroot(regs->di); } long __ia32_sys_chroot(const struct pt_regs *regs); static struct error_injection_entry __attribute__((__used__)) __attribute__((__section__("_error_injection_whitelist"))) _eil_addr___ia32_sys_chroot = { .addr = (unsigned long)__ia32_sys_chroot, .etype = EI_ETYPE_ERRNO, };; long __ia32_sys_chroot(const struct pt_regs *regs) { return __se_sys_chroot((unsigned int)regs->bx); } static long __se_sys_chroot(__typeof(__builtin_choose_expr((__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))), 0LL, 0L)) filename) { long ret = __do_sys_chroot(( const char *) filename); (void)(sizeof(struct { int:(-!!(!(__builtin_types_compatible_p(typeof(( const char *)0), typeof(0LL)) || __builtin_types_compatible_p(typeof(( const char *)0), typeof(0ULL))) && sizeof(const char *) > sizeof(long))); })); do { } while (0); return ret; } static inline __attribute__((unused)) __attribute__((no_instrument_function)) long __do_sys_chroot(const char * filename)
{
 return ksys_chroot(filename);
}

改行が省略されてるとさすがに見づらすぎるので indent -bap -gnu で整形する。中身を見ると、だいたい上記に書いてあったことが書いてあるなという感じ。

static const char *types__chroot[] = { "const char *" };
static const char *args__chroot[] = { "filename" };

static struct syscall_metadata __syscall_meta__chroot;

static struct trace_event_call
  __attribute__ ((__used__)) event_enter__chroot =
{
  .class = &event_class_syscall_enter,
  {
.name = "sys_enter" "_chroot",},.event.funcs =
    &enter_syscall_print_funcs,.data =
    (void *) &__syscall_meta__chroot,.flags = TRACE_EVENT_FL_CAP_ANY,};

static struct trace_event_call __attribute__ ((__used__))
  __attribute__ ((section ("_ftrace_events"))) * __event_enter__chroot =
  &event_enter__chroot;;
static struct syscall_metadata __syscall_meta__chroot;

static struct trace_event_call __attribute__ ((__used__)) event_exit__chroot =
{
  .class = &event_class_syscall_exit,
  {
.name = "sys_exit" "_chroot",},.event.funcs =
    &exit_syscall_print_funcs,.data =
    (void *) &__syscall_meta__chroot,.flags = TRACE_EVENT_FL_CAP_ANY,};

static struct trace_event_call __attribute__ ((__used__))
  __attribute__ ((section ("_ftrace_events"))) * __event_exit__chroot =
  &event_exit__chroot;;

static struct syscall_metadata
  __attribute__ ((__used__)) __syscall_meta__chroot =
{
  .name = "sys" "_chroot",.syscall_nr = -1,.nb_args = 1,.types =
    1 ? types__chroot : ((void *) 0),.args =
    1 ? args__chroot : ((void *) 0),.enter_event =
    &event_enter__chroot,.exit_event = &event_exit__chroot,.enter_fields =
  {
&(__syscall_meta__chroot.enter_fields),
      &(__syscall_meta__chroot.enter_fields)},};

static struct syscall_metadata __attribute__ ((__used__))
  __attribute__ ((section ("__syscalls_metadata"))) *
  __p_syscall_meta__chroot = &__syscall_meta__chroot;

static long
__se_sys_chroot (__typeof
                 (__builtin_choose_expr
                  ((__builtin_types_compatible_p
                    (typeof ((const char *) 0), typeof (0LL))
                    ||
                    __builtin_types_compatible_p (typeof ((const char *) 0),
                                                  typeof (0ULL))), 0LL,
                   0L)) filename);
static inline __attribute__ ((unused))
  __attribute__ ((no_instrument_function))
     long __do_sys_chroot (const char *filename);

long __x64_sys_chroot (const struct pt_regs *regs)
{
  return __se_sys_chroot (regs->di);
}

long __ia32_sys_chroot (const struct pt_regs *regs);
static struct error_injection_entry __attribute__ ((__used__))
  __attribute__ ((__section__ ("_error_injection_whitelist")))
  _eil_addr___ia32_sys_chroot =
{
.addr = (unsigned long) __ia32_sys_chroot,.etype = EI_ETYPE_ERRNO,};;

long
__ia32_sys_chroot (const struct pt_regs *regs)
{
  return __se_sys_chroot ((unsigned int) regs->bx);
} static long

__se_sys_chroot (__typeof
                 (__builtin_choose_expr
                  ((__builtin_types_compatible_p
                    (typeof ((const char *) 0), typeof (0LL))
                    ||
                    __builtin_types_compatible_p (typeof ((const char *) 0),
                                                  typeof (0ULL))), 0LL,
                   0L)) filename)
{
  long ret = __do_sys_chroot ((const char *) filename);
  (void) (sizeof (struct
                  {
  int:            (-! !
                   (!(__builtin_types_compatible_p
                      (typeof ((const char *) 0), typeof (0LL))
                      ||
                      __builtin_types_compatible_p (typeof ((const char *) 0),
                                                    typeof (0ULL)))
                    && sizeof (const char *) > sizeof (long)));
                  }));
  do
    {
    }
  while (0);
  return ret;
}

static inline __attribute__ ((unused))
  __attribute__ ((no_instrument_function))
     long __do_sys_chroot (const char *filename)
{
  return ksys_chroot (filename);
}

参考