特别说明:该文章前两天发布过,但一直在审核中。看头条网友说字数太多可能一直处于审核中状态,我把该文章拆分成几个章节发布,如影响阅读体验还请见谅。
五、系统调用编号
在示例程序中,我们使用了write和exit系统调用,并通过%rax传递了系统调用号。在Linux中,32位系统和64位系统有不同的系统调用编号。32位系统调用号定义在
arch/x86/syscalls/syscall_32.tbl文件;64位系统调用号定义在
arch/x86/syscalls/syscall_64.tbl文件。
下面列出了64位系统的部分系统调用及编号,可以看到,write()的系统调用编号为 1 ,exit()系统调用编号为 60。
0 common read sys_read
1 common write sys_write # write 系统调用
2 common open sys_open
3 common close sys_close
......
59 64 execve sys_execve
60 common exit sys_exit # exit 系统调用
61 common wait4 sys_wait4
62 common kill sys_kill
......
六、系统调用表及其初始化
linux内核中包含一个被称为系统调用表的数据结构。64位系统调用表定义在
arch/x86/kernel/syscall_64.c文件中:
// file: arch/x86/kernel/syscall_64.c
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_syscall_max+1个元素的数组。__NR_syscall_max是一个宏,在64位模式下其值为542,该宏定义于
include/generated/asm-offsets.h文件,这个文件是Kbuild编译后生成的。
// file: include/generated/asm-offsets.h
#define __NR_syscall_max 542 /* sizeof(syscalls_64) - 1 # */
系统调用表的元素类型为sys_call_ptr_t,这是通过typedef定义的函数指针。
// file: arch/x86/kernel/syscall_64.c
typedef void (*sys_call_ptr_t)(void);
sys_ni_syscall表示一个未实现的系统调用,其定义如下:
// file: kernel/sys_ni.c
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
sys_ni_syscall直接返回一个错误码-ENOSYS。ENOSYS值为38,表示调用了一个未实现的函数。
// file: include/uapi/asm-generic/errno.h
#define ENOSYS 38 /* Function not implemented */
符号 ...是GCC编译器的的一个扩展--Designated Initializers,该扩展允许我们以任意顺序初始化成员元素。正如我们看到的,sys_call_table先用sys_ni_syscall进行初始化,然后再用<asm/syscalls_64.h>头文件中的内容对数组进行填充。该头文件是使用
arch/x86/syscalls/syscalltbl.sh脚本读取syscall_64.tbl后生成的,它包含以下宏:
// file: arch/x86/include/generated/asm/syscalls_64.h
__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)
__SYSCALL_COMMON(2, sys_open, sys_open)
......
__SYSCALL_X32(540, compat_sys_process_vm_writev, compat_sys_process_vm_writev)
__SYSCALL_X32(541, compat_sys_setsockopt, compat_sys_setsockopt)
__SYSCALL_X32(542, compat_sys_getsockopt, compat_sys_getsockopt)
__SYSCALL_COMMON宏定义如下,该宏被扩展成__SYSCALL_64宏,最终被扩展成函数定义。
// file: arch/x86/kernel/syscall_64.c
#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#define __SYSCALL_64(nr, sym, compat) [nr] = sym,
最终,sys_call_table被扩展成了下面的格式,各系统调用号关联的函数指针被填充到该数组中;其它所有未实现的系统调用号都指向了sys_ni_syscall函数,该函数只是简单返回一个错误码-ENOSYS。
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,
[0] = sys_read,
[1] = sys_write,
[2] = sys_open,
......
};
七、系统调用的定义
下面我们以示例程序中使用的write系统调用为例,来看看系统调用是如何定义的。
write系统调用函数原型如下,可以通过 man 2 write命令查看。
ssize_t write(int fd, const void *buf, size_t count);
在linux内核中,write系统调用定义在fs/read_write.c文件中。由于write有3个参数,所以是用SYSCALL_DEFINE3宏定义的。
// file: fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput(f);
}
return ret;
}
SYSCALL_DEFINE3宏定义在 include/linux/syscalls.h中。可以看到,linux 内核一共定义了7个宏,每个宏后面都有一个数字,表示入参数量。
// file: include/linux/syscalls.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void)
#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_DEFINE3被扩展成了SYSCALL_DEFINEx宏,该宏又扩展成了SYSCALL_METADATA和__SYSCALL_DEFINEx。
以write为例,看下扩展过程:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
扩展成:
SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)
注意,扩展后,函数名前面多个了下划线”_“。”##“是连接操作符,在宏扩展时,可以把2个符号合并成一个,具体使用见 gcc 文档 3.5 Concatenation。
继续扩展:
SYSCALL_METADATA(_write, 3, unsigned int, fd, const char *, buf, size_t, count) \
__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)
SYSCALL_METADATA宏的实现,由Kbuild时配置的选项CONFIG_FTRACE_SYSCALLS来决定,只有设置CONFIG_FTRACE_SYSCALLS选项时,该宏才有实际意义。从选项名称就能够看出来,它主要是用来对系统调用过程进行追踪的。 关于调试和追踪方面的细节,本文暂不涉及,我们主要来看下__SYSCALL_DEFINEx宏的实现。
7.1 __SYSCALL_DEFINEx
__SYSCALL_DEFINEx宏定义于 include/linux/syscalls.h文件:
// file: include/linux/syscalls.h
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
SYSCALL_ALIAS(sys##name, SyS##name); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
7.1.1 __MAP
__MAP宏会根据参数数量和映射函数做适当的扩展。其中n表示参数数量,m代表映射函数,其它参数都是成对出现的,t表示参数类型,a表示参数值。从注释中也可以看到,__MAP(n, m, t1, a1, t2, a2, ..., tn, an)会被扩展成m(t1, a1), m(t2, a2), ..., m(tn, an)。
// file: include/linux/syscalls.h
/*
* __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/COMPAT_SYSCALL_DEFINE
*/
#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__)
7.1.2 __SC_DECL、__SC_LONG、__SC_CAST、__SC_TEST、__SC_ARGS
这些宏是作为__MAP宏的映射函数存在的,这些宏中的t表示参数类型(type),a表示参数值(argument)。其中__SC_DECL、__SC_CAST和__SC_ARGS这三个宏比较简单,就不做说明了,重点说说其它宏。
// file: include/linux/syscalls.h
#define __SC_DECL(t, a) t a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
#define __TYPE_IS_LL(t) (__same_type((t)0, 0LL) || __same_type((t)0, 0ULL))
7.1.2.1 __SC_LONG
7.1.2.1.1 __TYPE_IS_LL
__SC_LONG宏中引用了__TYPE_IS_LL宏,而__TYPE_IS_LL宏又引用了__same_type函数。__same_type函数定义如下:
// file: include/linux/compiler.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函数通过gcc 内建函数
__builtin_types_compatible_p来判断2个入参的类型是否一致,如果一致,返回1,否则返回0。
__builtin_types_compatible_p函数说明如下:
You can use the built-in function __builtin_types_compatible_p to determine whether two types are the same.
This built-in function returns 1 if the unqualified versions of the types type1 and type2 (which are types, not expressions) are compatible, 0 otherwise. The result of this built-in function can be used in integer constant expressions.
综上所述,__TYPE_IS_LL(t)的作用是判断给定的类型t是否是Logg Long或Unsigned Long Long类型,如果是其值为1,否则为0。
7.1.2.1. __builtin_choose_expr
__builtin_choose_expr也是一个gcc 内建函数,该函数有3个参数,第一个参数是一个常量表达式(const_exp)。其作用类似于三元操作符”?:“,如果第一参数非0,则返回第2个参数,否则返回第3个参数。
Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2)
You can use the built-in function __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.
7.1.2.1.3 结论
经过以上分析,宏__SC_LONG(t, a)的作用就是把”LL“或”ULL“类型的参数,转换为”LL“类型;其它类型的参数,转换成”L“类型。
7.1.2.2 __SC_TEST
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
从名称也可以看到,宏__SC_TEST(t, a)主要用于测试目的。该宏又引用了BUILD_BUG_ON_ZERO,其定义如下。
// file: include/linux/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). */
/* sizeof(struct { int:-!!(e); } 用法参考: https://stackoverflow.com/questions/9229601/what-is-in-c-code */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
这是一种使用技巧,它主要用来进行编译时检查。
sizeof(struct { int: -!!(e); }))
执行流程如下,详见What is ":-!!" in C code?:
(e): Compute expression e.
!!(e): Logically negate twice: 0 if e == 0; otherwise 1.
-!!(e): Numerically negate the expression from step 2: 0 if it was 0; otherwise -1.
struct{int: -!!(0);} --> struct{int: 0;}: If it was zero, then we declare a struct with an anonymous integer bitfield that has width zero. Everything is fine and we proceed as normal.
struct{int: -!!(1);} --> struct{int: -1;}: On the other hand, if it isn't zero, then it will be some negative number. Declaring any bitfield with negative width is a compilation error.
综上,__SC_TEST(t, a)的作用就是当参数类型t不是LL类型,但其类型大小却超过L类型时,强制编译器报错。说白了就是进行类型检测。
7.1.3 SYSCALL_ALIAS
SYSCALL_ALIAS宏定义如下:
// file: include/linux/linkage.h
#ifndef SYSCALL_ALIAS
#define SYSCALL_ALIAS(alias, name) asm( \
".globl " VMLINUX_SYMBOL_STR(alias) "\n\t" \
".set " VMLINUX_SYMBOL_STR(alias) "," \
VMLINUX_SYMBOL_STR(name))
#endif
宏VMLINUX_SYMBOL_STR定义如下:
// file: include/linux/export.h
/*
* Export symbols from the kernel to modules. Forked from module.h
* to reduce the amount of pointless cruft we feed to gcc when only
* exporting a simple symbol or two.
*
* Try not to add #includes here. It slows compilation and makes kernel
* hackers place grumpy comments in header files.
*/
/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define VMLINUX_SYMBOL_STR(x) __VMLINUX_SYMBOL_STR(x)
#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x
实际效果是给name设置了个别名alias,本例中是给SyS_write设置了别名sys_write。
7.1.4 最终扩展
我们继续往下分析,刚才分析到了如下代码:
__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)
所以我们知道,在宏内部x值为3,__VA_ARGS__参数类型和值列表。
根据__MAP及__SC_DECL宏定义,__MAP(x,__SC_DECL,__VA_ARGS__)被扩展成为:
unsigned int fd, const char * buf, size_t count
根据__MAP及__SC_LONG宏定义,__MAP(x,__SC_LONG,__VA_ARGS__)被扩展成:
long fd, long buf, long count
__MAP(x,__SC_CAST,__VA_ARGS__)被扩展成:
(unsigned int) fd, (const char *) buf, (size_t) count
__MAP(x,__SC_ARGS,__VA_ARGS__) 被扩展成:
fd, buf, count
所以,__SYSCALL_DEFINEx(3, _write, unsigned int, fd, const char *, buf, size_t, count)最终扩展如下:
asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
asmlinkage long SyS_write(long fd, long buf, long count) \
{ \
long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count); \
__MAP(x,__SC_TEST,__VA_ARGS__); \ # 用于测试,不涉及
__PROTECT(x, ret, fd, buf, count); \
return ret; \
} \
SYSCALL_ALIAS(sys_write, SyS_write); \
static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)
再结合write函数具体实现,完整的write系统调用扩展如下:
asmlinkage long sys_write(unsigned int fd, const char * buf, size_t count); \
static inline long SYSC_write(unsigned int fd, const char * buf, size_t count); \
asmlinkage long SyS_write(long fd, long buf, long count) \
{ \
long ret = SYSC_write((unsigned int) fd, (const char *) buf, (size_t) count); \
__MAP(x,__SC_TEST,__VA_ARGS__); \ # 用于测试,不涉及
__PROTECT(x, ret, fd, buf, count); \
return ret; \
} \
SYSCALL_ALIAS(sys_write, SyS_write); \
static inline long SYSC_write(unsigned int fd, const char * buf, size_t count)
{
struct fd f = fdget(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
file_pos_write(f.file, pos);
fdput(f);
}
return ret;
}
这段代码先声明了2个入参相同的函数sys_write和SYSC_write;然后定义了函数SyS_write,该函数内部调用了SYSC_write;给SyS_write设置了一个别名sys_write;SYSC_write是write系统调用的具体实现。
7.1.5 总结
总结一下实现流程:
- 内部实现函数为SYSC_write;
- SyS_write函数对SYSC_write进行了封装,增加了编译时类型检查及参数保护;
- 给SyS_write设置了别名sys_write。