はじめに
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_DEFINE6
は include/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;
}
システムコール実行の流れ
マクロの定義を見る前に、システムコールの実行の流れをおさらいする。
syscall_init()
で sysenter 命令が実行されたときに entry_SYSCALL_64 が実行されるように設定する。- ユーザ空間でレジスタを設定し、 sysenter 命令を実行する。
arch/x86/entry/entry_64.S#entry_SYSCALL_64
が実行される。- スタックの切り替え、レジスタに設定された引数から pt_regs を組み立てる。
arch/x86/entry/common.c#do_syscall_64
を実行する。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/Makefile
で arch/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__))
__PROTECT
は include/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_INJECTION
や FAULT_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);
}