本文基于linux x86-64平台下内核源码分析,源码版本4.18.0-80,以应用层openat调用为例展开探索。
目录
本文实验环境:
[root@yglocalhost ~]# uname -r
4.18.0-80.el8.x86_64
[root@yglocalhost ~]# cat /etc/redhat-release
CentOS Linux release 8.0.1905 (Core)
一、内核层系统调用名称
我们知道系统调用到内核层一般命名都加前缀"sys_",openat到内核层应该就是sys_openat,那么在centos8.0上还是不是呢?
在内核源码目录中找到含sys_openat系统调用所在源文件:
[root@yglocalhost ~]# grep -nrw sys_openat /usr/src/kernel/4.18.0-80.el8.x86_64
./arch/x86/entry/syscalls/syscall_32.tbl:309:295 i386 openat sys_openat __ia32_compat_sys_openat
./arch/x86/include/generated/asm/syscalls_32.h:1367:__SYSCALL_I386(295, sys_openat, )
./arch/x86/include/generated/asm/syscalls_64.h:1214:__SYSCALL_64(257, sys_openat, )
./include/linux/syscalls.h:422:asmlinkage long sys_openat(int dfd, const char __user *filename, int flags,
./include/uapi/asm-generic/unistd.h:182:__SC_COMP(__NR_openat, sys_openat, compat_sys_openat)
x64平台上即为:__SYSCALL_64(257, sys_openat, )
在syscalls_64.h文件可以找到:
arch\x86\include\generated\asm\syscalls_64.h
可以看出x86-64平台上,系统调用都加了前缀"_x64_sys",openat在内核中系统调用号为257,对应的系统调用接口为__x64_sys_openat。
arch\x86\entry\syscall_64.c
// SPDX-License-Identifier: GPL-2.0
/* System call table for x86-64. */
#include <linux/linkage.h>
#include <linux/sys.h>
#include <linux/cache.h>
#include <asm/asm-offsets.h>
#include <asm/syscall.h>
/* this is a lie, but it does not hurt as
* sys_ni_syscall just returns -EINVAL
*/
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,
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>
};
这里先看两个概念:
系统调用表: sys_call_table,所有提供给应用层使用的系统调用地址都在该数组中。
系统调用号:__NR_xxxx,其中xxx为系统调用在应用层的名称,如openat的系统调用号为__NR_openat,最大为__NR_syscall_max,在当前环境中该值为334。
所以__SYSCALL_64(257, __x64_sys_openat)实际上就是:
extern asmlinkage long __x64_sys_openat(const struct pt_regs *)
sys_call_table[257]=&__x64_sys_openat
可以通过命令在/proc/kallsyms中查看该系统调用:
[root@yglocalhost ~]# grep sys_openat /proc/kallsyms
ffffffff968a06d0 T __x64_sys_openat
ffffffff968a06f0 T __ia32_sys_openat
ffffffff968a0730 T __ia32_compat_sys_openat
ffffffff97f78fe0 t _eil_addr___ia32_compat_sys_openat
ffffffff97f79000 t _eil_addr___ia32_sys_openat
ffffffff97f79010 t _eil_addr___x64_sys_openat
可以看到,在x86-64平台上应该就是__x64_sys_openat,那么应用层openat,到底调的是不是对应内核层的__x64_sys_openat呢?
通过编写测试程序验证,我们打印出内核系统调用表__NR_openat下标所对应的地址验证:
运行结果对比:
可以看出,openat系统调用对应内核系统调用表sys_call_table[__NR_openat]对应的地址就是指向__x64_sys_openat,其实在4.17及以上版本的内核中,x86-64的系统调用前缀都是"_x64_sys"。
二、系统调用定义探秘
内核中系统调用的定义都是以SYSCALL_DEFINEx开始的,x表示系统调用的参数个数,我们来看下openat,应用层函数声明为:
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
有4个参数,所以它在内核层定义则形似:
SYSCALL_DEFINE4(openat,
内核源码中可以搜索到真正的定义处,在fs\open.c文件中:
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(dfd, filename, flags, mode);
}
下面进入正题,来一步步解析系统调用宏SYSCALL_DEFINEx的真面目。
首先我们来看看***SYSCALL_DEFINE4***的定义:
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__)
可以看出,SYSCALL_DEFINE4实际是执行SYSCALL_DEFINEx宏(其中x为4,x表示系统调用参数个数),然后SYSCALL_DEFINEx又到__SYSCALL_DEFINEx。
注意:VA_ARGS 就是上面的 … ,都是可变参数列表,这里即是:
int, dfd, const char __user *, filename, int, flags, umode_t, mode
SYSCALL_DEFINEx
宏:
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
现在我们将
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
宏展开:
SYSCALL_DEFINEx(4, _openat, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
------>
SYSCALL_METADATA(_openat, 4, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
__SYSCALL_DEFINEx(4, _openat, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
1、SYSCALL_METADATA宏
其中SYSCALL_METADATA宏是追踪系统调用以捕获syscall进入和退出事件,宏定义如下:
#define SYSCALL_TRACE_ENTER_EVENT(sname) \
static struct syscall_metadata __syscall_meta_##sname; \
static struct trace_event_call __used \
event_enter_##sname = { \
.class = &event_class_syscall_enter, \
{ \
.name = "sys_enter"#sname, \
}, \
.event.funcs = &enter_syscall_print_funcs, \
.data = (void *)&__syscall_meta_##sname,\
.flags = TRACE_EVENT_FL_CAP_ANY, \
}; \
static struct trace_event_call __used \
__attribute__((section("_ftrace_events"))) \
*__event_enter_##sname = &event_enter_##sname;
#define SYSCALL_TRACE_EXIT_EVENT(sname) \
static struct syscall_metadata __syscall_meta_##sname; \
static struct trace_event_call __used \
event_exit_##sname = { \
.class = &event_class_syscall_exit, \
{ \
.name = "sys_exit"#sname, \
}, \
.event.funcs = &exit_syscall_print_funcs, \
.data = (void *)&__syscall_meta_##sname,\
.flags = TRACE_EVENT_FL_CAP_ANY, \
}; \
static struct trace_event_call __used \
__attribute__((section("_ftrace_events"))) \
*__event_exit_##sname = &event_exit_##sname;
#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;
展开为:
2、SYSCALL_DEFINEx宏
下面我们重点看下__SYSCALL_DEFINEx宏。
这个宏定义在该源文件(include\linux\syscalls.h)中也能找到,但是我们也许要注意下该文件开头部分:
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
/*
* It may be useful for an architecture to override
* the definitions of the SYSCALL_DEFINE0() and
* __SYSCALL_DEFINEx() macros, in particular to use
* a different calling convention for syscalls.
* To allow for that, the prototypes for the
* sys_*() functions below will *not* be included
* if CONFIG_ARCH_HAS_SYSCALL_WRAPPER is enabled.
*/
#include <asm/syscall_wrapper.h>
#endif /* CONFIG_ARCH_HAS_SYSCALL_WRAPPER */
在centos8.0系统上可以查到内核配置:
[root@yglocal home]# grep CONFIG_ARCH_HAS_SYSCALL_WRAPPER /boot/config-4.18.0-147.el8.x86_64
CONFIG_ARCH_HAS_SYSCALL_WRAPPER=y
其实在x86-64平台,v4.17以上的内核中都启用了该配置,所以我们需要在asm/syscall_wrapper.h源文件中找到__SYSCALL_DEFINEx定义:
arch\x86\include\asm\syscall_wrapper.h
/*
* 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)
*/
#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__))
这段代码前面注释解释的很清楚了,此宏用来替代原文件中通用的__SYSCALL_DEFINEx()宏,将struct pt_regs * regs作为syscall系统调用存根__x64_sys_()的唯一参数,然后通过调用__se_sys_()函数来完成符号拓展,最后参数传递给__do_sys_*()函数来完成实际工作。
我们将
__SYSCALL_DEFINEx(4, _openat, int, dfd, const char __user *, filename,
int, flags, umode_t, mode)
宏初步展开后为:
asmlinkage long __x64_sys_openat(const struct pt_regs *regs);
ALLOW_ERROR_INJECTION(__x64_sys_openat, ERRNO); //不管它
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__));
static inline long __do_sys_openat(__MAP(4,__SC_DECL,__VA_ARGS__));
asmlinkage long __x64_sys_openat(const struct pt_regs *regs)
{
return __se_sys_openat(SC_X86_64_REGS_TO_ARGS(4,__VA_ARGS__));
}
//__IA32_SYS_STUBx(x, name, __VA_ARGS__)
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__))
{
long ret = __do_sys_openat(__MAP(4,__SC_CAST,__VA_ARGS__));
__MAP(x,__SC_TEST,__VA_ARGS__);
__PROTECT(x, ret,__MAP(4,__SC_ARGS,__VA_ARGS__));
return ret;
}
static inline long __do_sys_openat(__MAP(4,__SC_DECL,__VA_ARGS__))
下面我们来仔细分析下上面展开后的代码内容。
asmlinkage long __x64_sys_openat(const struct pt_regs *regs);
ALLOW_ERROR_INJECTION(__x64_sys_openat, ERRNO); //不管它
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__));
static inline long __do_sys_openat(__MAP(4,__SC_DECL,__VA_ARGS__));
这几行有三个函数的声明,我们一个一个看。
(1)__x64_sys_openat函数:
asmlinkage long __x64_sys_openat(const struct pt_regs *regs)
它是应用层调openat后,陷入内核实际执行的系统调用。仅有一个参数,struct pt_regs *,应用层调用openat实际上是经过glibc库封装的接口,glibc内部会将参数填充到相应的寄存器中。
glibc-2.28\sysdeps\unix\sysv\linux\x86_64\sysdep.h
# define PSEUDO(name, syscall_name, args) \
.text; \
ENTRY (name) \
DO_CALL (syscall_name, args); \
cmpq $-4095, %rax; \
jae SYSCALL_ERROR_LABE
(2)_se_sys_openat函数:
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__))
将__VA_ARGS__可变参列表部分代入后
static long __se_sys_openat(__MAP(4,__SC_LONG,
int, dfd, const char __user *, filename, int, flags,
umode_t, mode))
看下__MAP宏定义:
/*
* __MAP - apply a macro to syscall arguments
* __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
* m(t1, a1), m(t2, a2), ..., m(tn, an)
* The first argument must be equal to the amount of type/name
* pairs given. Note that this list of pairs (i.e. the arguments
* of __MAP starting at the third one) is in the same format as
* for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
*/
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
再次展开后:
static long __se_sys_openat(__SC_LONG(int, dfd),
__SC_LONG(const char __user *, filename),__SC_LONG(int, flags),
__SC_LONG(umode_t, mode))
这里需要分析下__SC_LONG宏,看下面定义:
#define __TYPE_AS(t, v) __same_type((__force t)0, v)
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
将__se_sys_openat函数的参数里__SC_LONG宏全部展开后为:
static long __se_sys_openat(
__typeof(__builtin_choose_expr((__same_type((__force int)0, 0LL) || __same_type((__force int)0, 0ULL)), 0LL, 0L)) dfd,
__typeof(__builtin_choose_expr((__same_type((__force const char __user *)0, 0LL) || __same_type((__force const char __user *)0, 0ULL)), 0LL, 0L)) filename,
__typeof(__builtin_choose_expr((__same_type((__force int)0, 0LL) || __same_type((__force int)0, 0ULL)), 0LL, 0L)) flags,
__typeof(__builtin_choose_expr((__same_type((__force umode_t)0, 0LL) || __same_type((__force umode_t)0, 0ULL)), 0LL, 0L)) mode);
a、使用__force修饰的变量可以进行强制类型转换。
b、__typeof类似于typedef,用于引用表达式的类型。
c、__builtin_choose_expr
是类似于c中的三元运算符 “?:”,它是gcc编译器内置的函数,函数原型为:__builtin_choose_expr(const_exp, exp1, exp2)
,它的值取决于第一个常量表达式的结果,若 const_exp
结果非0,则返回 exp1
,否则返回 exp2
。
d、看下__same_type宏,找到定义如下:
include\linux\compiler_types.h
/* Are two types/vars the same type (ignoring qualifiers)? */
#ifndef __same_type
# define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))
#endif
__same_type宏是用来判断类型或者变量a、b的类型是否一致,一致则返回1,否则返回0。
看下第一个参数:
__typeof(__builtin_choose_expr((__same_type((__force int)0, 0LL)
|| __same_type((__force int)0, 0ULL)), 0LL, 0L)) dfd
在x86的64位系统上,int类型是占4个字节,0LL是长整型,占8个字节,
所以__same_type((__force int)0, 0LL)为假;
同理__same_type((__force int)0, 0ULL)也为假,
所以__builtin_choose_expr取exp2,即0L,
也即第一个参数变为:
__typeof(0L) dfd
同理第二个参数:
__typeof(__builtin_choose_expr((__same_type((__force const char __user *)0, 0LL)
|| __same_type((__force const char __user *)0, 0ULL)), 0LL, 0L)) filename
在x86的64位系统上,char*为指针类型,占8个字节,0LL是长整型,占8个字节,
所以__same_type((__force int)0, 0LL)为真,
所以__builtin_choose_expr取exp1,即0LL,
也即第一个参数变为:
__typeof(0LL) filename
所以最终__se_sys_openat函数的声明形式为:
static long __se_sys_openat(__typeof(0L) dfd,
__typeof(0LL) filename, __typeof(0L) flags, __typeof(0L) mode);
------>
static long __se_sys_openat(long dfd, long long filename, long flags, long mode);
(3)__do_sys_openat函数
static inline long __do_sys_openat(__MAP(4,__SC_DECL,__VA_ARGS__))
#define __SC_DECL(t, a) t a
所以展开后为:
static long __do_sys_openat(__MAP(4,__SC_DECL,
int, dfd, const char __user *, filename, int, flags,
umode_t, mode))
------>
static long __do_sys_openat(__SC_DECL(int, dfd),
__SC_DECL(const char __user *, filename),__SC_DECL(int, flags),
__SC_DECL(umode_t, mode))
------>
static long __do_sys_openat(int dfd, const char __user *filename,
int flags,umode_t mode)
(4)__se_sys_openat函数被调用的地方
asmlinkage long __x64_sys_openat(const struct pt_regs *regs)
{
return __se_sys_openat(SC_X86_64_REGS_TO_ARGS(4,__VA_ARGS__));
}
x64_sys()就一个参数struct pt_regs,它会调用__se_sys*()函数,传递的参数是SC_X86_64_REGS_TO_ARGS(x,VA_ARGS)(其中x为参数个数),SC_X86_64_REGS_TO_ARGS宏定义如下:
/* Mapping of registers to parameters for syscalls on x86-64 and x32 */
#define SC_X86_64_REGS_TO_ARGS(x, ...) \
__MAP(x,__SC_ARGS \
,,regs->di,,regs->si,,regs->dx \
,,regs->r10,,regs->r8,,regs->r9) \
将宏 __se_sys_openat(SC_X86_64_REGS_TO_ARGS(4,__VA_ARGS__))
展开即为:
__se_sys_openat(SC_X86_64_REGS_TO_ARGS(4,regs))
再次展开:
__se_sys_openat(__MAP(4,__SC_ARGS
,,regs->di,,regs->si,,regs->dx
,,regs->r10,,regs->r8,,regs->r9))
再次展开:
__se_sys_openat(__SC_ARGS( ,regs->di),__SC_ARGS( ,regs->si),
__SC_ARGS( ,regs->dx),__SC_ARGS( ,regs->r10))
且
#define __SC_ARGS(t, a) a
全部展开后即为:
__se_sys_openat(regs->di,regs->si,regs->dx,regs->r10)
上述参数将传入 __se_sys_openat
函数中。
(5)再看下__se_sys_openat函数内部实现:
static long __se_sys_openat(__MAP(4,__SC_LONG,__VA_ARGS__))
{
long ret = __do_sys_openat(__MAP(4,__SC_CAST,__VA_ARGS__));
__MAP(x,__SC_TEST,__VA_ARGS__);
__PROTECT(x, ret,__MAP(4,__SC_ARGS,__VA_ARGS__));
return ret;
}
① __do_sys_openat(__MAP(4,__SC_CAST,__VA_ARGS__)) #define __SC_CAST(t, a) (__force t) a
所以展开后为:
__do_sys_openat((__force int) dfd, (__force const char __user *) filename,
(__force int) flags, (__force umode_t) mode)
② __MAP(x,__SC_TEST,__VA_ARGS__);
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
include\linux\build_bug.h
/*
* Force a compilation error if condition is true, but
* also produce a result (of value 0 and type size_t),
* so the expression can be used
* e.g. in a structure initializer (or where-ever else
* comma expressions aren't permitted).
*/
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:(-!!(e)); }))
这里看下 BUILD_BUG_ON_ZERO
宏,当表达式e为真的时候,它会强制编译出错,我们来重点看下sizeof(struct { int:(-!!(e)); })
!!(e):
表示对表达式e取两次反,e为假(0)时,两次取反后仍为0,否则为1
-!!(e):
是对上面的结果取负数,若!!(e)为0,则取负仍为0,若为1,则取负为-1.
int:(-!!(e)):
占用int型变量的位域,为0,表示空域,为1,表示占用1个位域,为负则编译会报错。
一言以蔽之,__SC_TEST(t, a)
宏就是通过BUILD_BUG_ON_ZERO
来判断表达式:
!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long)
该表达式为假则编译通过,为真则编译出错。
__MAP(x,__SC_TEST,__VA_ARGS__)
展开宏后:
进一步可以看出这里是验证参数类型是否有误:是8字节类型就ok,不是8字节的话,如果该类型占用字节大于8字节long类型,则编译不通过(x86-64平台上)。
③__PROTECT(x, ret,__MAP(4,__SC_ARGS,__VA_ARGS__)); #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
/*
* 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
防止gcc的尾部调用优化,保持参数保存在栈上,在整个生命期内不让编译器碰他,参数的栈可能是调用者的也可能是被调用者的,但是gcc编译器不一定总能区别到。
到这里linux内核(x86-64)系统调用宏定义就全部展开了,完整如下图:
三、写在最后
为什么内核系统调用要用这么复杂的宏定义来实现呢,其实在2.6.28及之前内核源码中,都是直接定义系统调用,没有使用宏的,这里就不得不提著名的内核的CVE-2009-0029漏洞:
The ABI in the Linux kernel 2.6.28 and earlier on s390,
powerpc, sparc64, and mips 64-bit platforms requires that
a 32-bit argument in a 64-bit register was properly sign extended
when sent from a user-mode application, but cannot verify this,
which allows local users to cause a denial of service (crash)
or possibly gain privileges via a crafted system call.
内核没有确保某些32位参数执行了正确的符号扩展,便接受了用户空间的64位寄存器所传送的32位参数,如果向有漏洞的系统调用传送了特制参数便可以导致系统崩溃或获得权限提升。某些架构的ABI定义函数的调用程序必须将每个参数符号扩展为完整的寄存器宽度,这在Linux系统调用处理中可能导致问题。
正是由于CVE-2009-0029漏洞的存在,内核大佬们就用宏优雅的解决了这个问题,而且将这些类型转换及参数检查操作封装在宏里,使得内核开发者在定义系统调用时更简单明了。
本文微信链接点这里,欢迎关注我的公众号大胖聊编程,一起交流学习。
参考链接:
CVE-2009-0029漏洞相关:
https://www.cvedetails.com/cve/CVE-2009-0029/
https://www.securityfocus.com/bid/33275
https://bugzilla.redhat.com/show_bug.cgi?id=479969
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html