Linux系统调用

Linux系统调用

1、系统调用相关概念

1、系统调用

  • 系统调用:系统调用是应用程序和内核之间的交互接口,它是由内核提供并且可供用户调用的函数集合。
  • 中断:系统处理异常事件的一种机制,在计算机执行期间系统发生了非预期的异常事件时,当前执行任务被中止并且相应的事件处理程序被执行,在处理完毕后又返回到原来被中断处继续执行。
  • 计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。
  • img

2、系统调用的实现

  • 操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。
  • 中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。
  • 一般地,系统调用都是通过中断实现的.

3、API、系统调用、POSIX标准和C库

  • 对于应用编程接口API和系统调用之间的关系,一般来说,应用程序调用用户空间实现的用户编程接口来编程,而不是直接执行系统调用。一个接口函数可以由一个系统调用实现,也可以由多个系统调用实现,甚至不完全使用系统调用。
  • UNIX系统在设计的早期就出现了操作系统的API层,UNIX的世界中,最通用的的系统调用层接口是POSIX(Portable Operating System Interface of UNIX)标准。需要注意的是,POSIX标准针对的是API而不是系统调用,判断一个系统是否是POSIX兼容时,要看它是否提供一组合适的应用编程接口,而不是看它的系统调用是如何定义和实现的。
  • Linux操作系统的API通常是以C标准库的方式提供的,比如Linux中的libc库。C标准库提供了POSIX的绝大部分API的实现,同时也为内核提供的每个系统调用封装了相应的函数,并且系统调用和C标准库封装的函数名称通常是相同的。例如,open系统调用在C标准库中对应的函数是open函数。另外,有几个接口函数可能调用封装了不同功能的同一个系统调用,例如libc库中实现的malloc()、calloc()和free()等函数,这几个函数用来分配和释放虚拟内存(堆上的虚拟内存),他们都是利用brk系统调用来实现的。

4、系统调用表

  • Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。以系统调用号_NR_name作为下标,可找出系统调用表sys_call_table中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。在x86上,系统调用号是通过eax寄存器传递给内核的。
  • unistd32.h是位于include目录下的Linux标准头文件,此代码文件主要包含:系统调用号、系统调用汇编函数和系统调用函数原型。用户在调用一个系统调用时,系统调用号作为参数传递给终端0X80,该标号实际上是系统调用表sys_call_table的下标,通过该标号执行相应的系统调用。system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 以下是内核实现的系统调用符号常数,用于作为系统调用函数表中的索引值。( arch/arm64/include/asm/unistd32.h )
#define __NR_restart_syscall 0
__SYSCALL(__NR_restart_syscall, sys_restart_sysycall)
#define __NR_exit 1 //终止进程
__SYSCALL(__NR_exit, sys_exit)
#define __NR_fork 2 //创建一个新进程
__SYSCALL(__NR_fork, sys_fork)
#define __NR_read 3 //读文件
__SYSCALL(__NR_read, sys_read)
#define __NR_write 4 //写文件
__SYSCALL(__NR_write, sys_write)
#define __NR_open 5 //打开文件
__SYSCALL(__NR_open, ccompat_sys_open)

  • 例如,open这个系统调用被赋予的系统调用号是5,因此在所有的ARM64系统中,open这个系统调用号是不能更改的,open系统调用最终实现在如下函数中:
1
2
3
4
5
6
7
8
<fs/open.c>
SYSCALL_DEFINE3(open,const char __user *, filename,int , flagsa,umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;

return do_sys_open(AT_FDCED, filename, flags, mode);
}
  • SYSCALL_DEFINEx是一类宏,实现在include/linux/syscall.h头文件中。
  • 其中SYSCALL_DEFINE1表示有1个参数,SYSCALL_DEFINE2表示有两个参数,以此类推。SYSCALL_DEFINEx宏的定义如下
1
2
3
#define SYSCALL_DEFINEx(x, name, ...)
SYSCALL_METADATA(sname, x, __VA_ARGS_)
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
  • 这个宏在扩展完之后会变成sys_open()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
asmlinkage long __arm64_sys_open(const struct pt_regs *regs);

asmlinkage long __arm64_sys_open(const struct pt_regs *regs)
{
return __se_sys_open(filename, flags, mode);
}

static long __se_sys_open(const char __user * filename, int flags, umode_t mode)
{
long ret = __do_sys_open(filename, flags, mode);
return ret;
}
static inline long __do_sys_open(const char __user *filename, int flags, umode_t mode)
  • 因此, SYSCALL_DEFINE3(open, …)语句展开后会多出两个函数,分别是__arm64_sys_open()和__se_sys_open()函数。其中__arm64_sys_open()函数的地址会存放在系统调用表sys_call_table()中,最后这两个函数变成了__do_sys_open()函数。
  • 在kernel/sys.c文件中,__SYSCALL宏用来设置某个系统调用的函数指针到sys_call_table[]数组中
1
#define __SYSCALL(nr, sym)  [nr] = (syscall_fn_t)__arm64_##sym,
  • 系统在初始化时会把__arm64_sys_xx()函数添加到 sys_call_table[]数组中 ,因此,sys_call_table的原型如下:
1
typedef long (*syscall_fn_t) (struct pt_regs *regs);
  • 参数为pt-regs *regs数据结构,系统调用表如图所示
  • img

5、程序访问系统调用

  • 通过C标准库函数来访问系统调用
1
2
3
4
5
#include<usistd.h>
#include<sys/syscall.h> /*系统调用的定义>

long syscall(long number, ...);

  • syscall()函数可以直接执行系统调用,第一个参数是系统调用号,比如open系统调用号是5,后面的“…”是可变参数,用来传递参数到内核,如open的调用:
1
2
#define NR_OPEN 5
syscall(NR_OPEN, filename, flags, mode);

2、系统调用过程

1、响应函数sys_xxx

响应函数名以“sys_”开头,后跟该系统调用的名字。

例如
系统调用fork()的响应函数是sys_fork()(见Kernel/fork.c),
exit()的响应函数是sys_exit()(见kernel/fork.)。

2、系统调用表与系统调用号-=>数组与下标

  • 上面已经提到,unisted.h为每个系统调用规定了唯一的编号。

  • 假设用name表示系统调用的名称,那么系统调用号与系统调用响应函数的关系是:以系统调用号_NR_name作为下标,可找出系统调用表sys_call_table(见arch/i386/kernel/entry.S)中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。

  • 系统调用表sys_call_table记录了各sys_name函数在表中的位置,共190项。有了这张表,就很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。系统调用表共256项,余下的项是可供用户自己添加的系统调用空间。

  • 在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。

  • 系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。

  • 因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

  • 内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表:
    system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。

1
call *sys_ call-table(,%eax, 4)1

由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置

3、进程的系统调用命令转换为INT 0x80中断的过程

宏定义_syscallN()见include/asm/unisted.h)用于系统调用的格式转换和参数的传递。N取0~5之间的整数。

参数个数为N的系统调用由_syscallN()负责格式转换和参数传递。系统调用号放入EAX寄存器,启动INT 0x80后,规定返回值送EAX寄存器。

4、系统调用功能模块的初始化

对系统调用的初始化也就是对INT 0x80的初始化。

系统启动时,汇编子程序setup_idt(见arch/i386/kernel/head.S)准备了1张256项的idt表,由start_kernel()(见init/main.c),trap_init()(见arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80,&system_call)(见include/asm/system.h)设置0x80号软中断的服务程序为 system_call(见arch/i386/kernel/entry.S), system.call就是所有系统调用的总入口。

5、内核如何为各种系统调用服务

当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。该命令如果已在某个头文件中由相应的_syscallN()展开,则用户程序必须包含该文件。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数 由各通用寄存器传递,然后执行INT 0x80,以内核态进入入口地址system_call。

6、ret_from_sys_call

以ret_from_sys_call入口的汇编程序段在linux进程管理中起到了十分重要的作用。

所有系统调用结束前以及大部分中断服务返回前,都会跳转至此处入口地址。 该段程序不仅仅为系统调用服务,它还处理中断嵌套、CPU调度、信号等事务。

7、内核如何为系统调用的参数传递参数

1、参数传递

除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esi和edi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。接下来许多关于系统调用处理程序的描述都是针对x86版本的。但不用担心,所有体系结构的实现都很类似。

2、参数验证

系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。

最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:

  • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。
  • 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。
  • 如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须有一个被调用。为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度(字节数)。

为了从用户空间读取数据,内核提供了copy_from_ user(),它和copy-to-User()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当出现上述错误时,系统调用返回标准-EFAULT。

注意copy_to_user()和copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

3、系统调用的返回值

系统调用(在Linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。如果一个系统调用失败,你可以读出errno的值来确定问题所在。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

errno不同数值所代表的错误消息定义在errno.h中,你也可以通过命令”man 3 errno”来察看它们。需要注意的是,errno的值只在函数发生错误时设置,如果函数不发生错误,errno的值就无定义,并不会被置为0。另外,在处理errno前最好先把它的值存入另一个变量,因为在错误处理过程中,即使像printf()这样的函数出错时也会改变errno的值。

当然,系统调用最终具有一种明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:

1
2
3
4
asmlinkage long sys_ getpid(void)
{
return current-> tgid;
}

上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则。

8、访问系统调用

1、系统调用上下文

内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。

在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。

当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。

2、系统调用访问示例

操作系统使用系统调用表将系统调用编号翻译为特定的系统调用。系统调用表包含有实现每个系统调用的函数的地址。例如,read() 系统调用函数名为sys_read。read()系统调用编号是 3,所以sys_read() 位于系统调用表的第四个条目中(因为系统调用起始编号为0)。从地址 sys_call_table + (3 * word_size) 读取数据,得到sys_read()的地址。

找到正确的系统调用地址后,它将控制权转交给那个系统调用。我们来看定义sys_read()的位置,即fs/read_write.c文件。这个函数会找到关联到 fd 编号(传递给 read() 函数的)的文件结构体。那个结构体包含指向用来读取特定类型文件数据的函数的指针。进行一些检查后,它调用与文件相关的 read() 函数,来真正从文件中读取数据并返回。与文件相关的函数是在其他地方定义的 —— 比如套接字代码、文件系统代码,或者设备驱动程序代码。这是特定内核子系统最终与内核其他部分协作的一个方面。

读取函数结束后,从sys_read()返回,它将控制权切换给 ret_from_sys。它会去检查那些在切换回用户空间之前需要完成的任务。如果没有需要做的事情,那么就恢复用户进程的状态,并将控制权交还给用户程序。

3、从用户空间直接访问系统调用

通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。值得庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷人指令。这些宏是_syscalln(),其中n的范围从0到6。代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。举个例子,open()系统调用的定义是:

1
long open(const char *filename, int flags, int mode)

而不靠库支持,直接调用此系统调用的宏的形式为:

1
2
#define NR_ open 5
syscall3(long, open, const char*,filename, int, flags, int, mode)

这样,应用程序就可以直接使用open()
对于每个宏来说,都有2+ n个参数。
第一个参数对应着系统调用的返回值类型。
第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。
NR open在<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数。由汇编语言执行前一节所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了。

3、举例kill()系统调用过程

Tips 1: 用户空间的方法xxx,对应系统调用层方法则是sys_xxx
Tips 2: unistd.h文件记录着系统调用中断号的信息。

  • 故用户空间kill方法则对应系统调用层便是sys_kill,这个方法去哪里找呢?从/kernel/include/uapi/asm-generic/unistd.h等还有很多unistd.h去慢慢查看,查看关键字sys_kill,便能看到下面几行:
1
2
/* kernel/signal.c */
__SYSCALL(__NR_kill, sys_kill)
  • 根据这个能得到一丝线索,那就是kill对应的方法sys_kill位于/kernel/signal.c文件。

Tips 3: 宏定义SYSCALL_DEFINEx(xxx,…),展开后对应的方法则是sys_xxx
Tips 4: 方法参数的个数x,对应于SYSCALL_DEFINEx。

kill(int pid, int sig)方法共两个参数,则对应方法于SYSCALL_DEFINE2(kill,...),进入signal.c文件,再次搜索关键字,便能看到方法:

1
2
3
4
5
6
7
8
9
10
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}

SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) 基本等价于 asmlinkage long sys_kill(int pid, int sig),这里用的是基本等价,往下看会解释原因。

2、Syscall流程

  • Syscall是通过中断方式实现的,ARM平台上通过swi中断来实现系统调用,实现从用户态切换到内核态,发送软中断swi时,从中断向量表中查看跳转代码,其中异常向量表定义在文件/kernelarch/arm/kernel/entry-armv.S(汇编语言文件)。当执行系统调用时会根据系统调用号从系统调用表中来查看目标函数的入口地址,在calls.S文件中声明了入口地址信息。

总体流程:kill() -> kill.S -> swi陷入内核态 -> 从sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用户态执行kill()下一行代码。 下面介绍部分核心流程:

  • 用户程序通过软中断swi指令切入内核态,执行vector_swi处的指令。vector_swi在文件/kenel/arch/arm/kernel/entry-common.S中定义,此处省略。像每一个异常处理程序一样,要做的第一件事当然就是保护现场了。紧接着是获得系统调用的系统调用号

  • 仍以kill()函数为例,来详细说说Syscall调用流程,用户空间kill()定义位于文件kill.S

1
2
3
4
5
6
7
8
9
10
11
#include <private/bionic_asm.h>
ENTRY(kill)
mov ip, r7
ldr r7, =__NR_kill
swi #0
mov r7, ip
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(kill)

当调用kill时, 系统先保存r7内容, 然后将__NR_kill值放入r7, 再执行swi软中断指令切换进内核态。

  • kill的系统调用号为__NR_kill:
1
#define __NR_kill (__NR_SYSCALL_BASE + 37)

其中__NR_SYSCALL_BASE=0,也就是__NR_kill系统调用号=37。

  • 查看系统调用表,定义在文件/kernel/arch/arm/kernel/calls.S,如下:
1
2
3
4
5
/* 35 */    CALL(sys_ni_syscall)        /* was sys_ftime */
CALL(sys_sync)
CALL(sys_kill) //此处为37号
CALL(sys_rename)
CALL(sys_mkdir)

到这里可知37号系统调用对应sys_kill(),该方法所对应的函数声明在syscalls.h文件

  • 文件/kernel/include/linux/syscalls.h中有如下声明:
1
asmlinkage long sys_kill(int pid, int sig);

asmlinkage是gcc标签,代表函数读取的参数来自于栈中,而非寄存器。

3、SYSCALL_DEFINE

sys_kill()定义在内核源码找不到直接定义,而是通过syscalls.h文件中的SYSCALL_DEFINE宏定义。前面已经讲过sys_kill是通过语句SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)来定义,下面来一层层剖开,这条宏定义的真面目:

等价 1:

syscalls.h中有大量如下宏定义:

1
2
3
4
5
6
7
8
9
#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__)

可得出原语句等价:

1
SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)
  • 注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词

等价 2:**

syscalls.h中有如下宏定义:

1
2
3
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

可得出原语句等价:

1
2
SYSCALL_METADATA(_kill, 2, pid_t, pid, int, sig)
__SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)

define __SYSCALL_DEFINEx(x, name, …)

等价 3:

syscalls.h中有如下宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define __SYSCALL_DEFINEx(x, name, ...)                    \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(SyS##name)))); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__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; \
} \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

可得出原语句等价:

1
2
3
4
5
6
7
8
9
10
asmlinkage long sys_kill(__MAP(2,__SC_DECL,__VA_ARGS__)) \ __attribute__((alias(__stringify(SyS_kill))));        \
static inline long SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__)) \ { \
long ret = SYSC_kill(__MAP(2,__SC_CAST,__VA_ARGS__)); \
__MAP(2,__SC_TEST,__VA_ARGS__); \
__PROTECT(2, ret,__MAP(2,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__))

这里__VA_ARGS__等于 pid_t, pid, int, sig

等价 4:

先说说这里涉及的宏定义

__MAP宏定义:

1
2
3
4
5
6
7
8
#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__)

相关宏定义:

1
2
3
4
5
#define __SC_DECL(t, a) t a
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

展开:

1
2
3
4
__MAP(2,__SC_DECL, pid_t, pid, int, sig) //等价于 pid_t pid, int sig
__MAP(2,__SC_LONG,__VA_ARGS__) //等价于 long pid, long sig
__MAP(2,__SC_CAST,__VA_ARGS__) //等价于 (pid_t) pid, (int)sig
__MAP(2,__SC_ARGS,__VA_ARGS__) //等价于 pid, sig

可得出原语句等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//函数声明sys_kill(),并别名指向SyS_kill
asmlinkage long sys_kill(pid_t pid, int sig) __attribute__((alias(__stringify(SyS_kill))));
static inline long SYSC_kill(pid_t pid, int sig);

//函数声明SyS_kill()
asmlinkage long SyS_kill(long pid, long sig);
asmlinkage long SyS_kill(long pid, long sig)
{
long ret = SYSC_kill((pid_t) pid, (int)sig);
BUILD_BUG_ON_ZERO(sizeof(pid_t) > sizeof(long));
BUILD_BUG_ON_ZERO(sizeof(int) > sizeof(long));
__PROTECT(2, ret, pid, sig);
return ret;
}
static inline long SYSC_kill(pid_t pid, int sig)

通过以上分析过程:

  • kill添加了sys_前缀,声明sys_kill()函数;
  • 定义SYSC_kill()函数和SyS_kill()函数;
  • sys_kill,通过别名机制等同于SyS_kill().

看到这或许很多人会觉得诧异,为何要如此复杂呢,后来查资料,发现这是由于之前64位Linux存在CVE-2009-2009的漏洞,简单说就是32位参数存放在64位寄存器,修改符号扩展可能导致产生一个非法内存地址,从而导致系统崩溃或者提升权限。 为了修复这个问题,把寄存器高位清零即可,但做起来比较困难,为了做尽可能少的修改,将调用参数统一采用使用long型来接收,再强转为相应参数。 窥见一斑,可见Linux大师们精湛的宏定义,已经用得出神入化。

如果觉得很复杂,那么可以忽略这个宏定义,只要记住SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) 基本等价于 asmlinkage long sys_kill(int pid, int sig) 就足够了。

总结

内核空间

  1. 系统调用的函数原型的指针:位于文件/kernel/arch/arm/kernel/calls.S,格式为CALL(sys_xxx),指定了目标函数的入口地址。
  2. 系统调用号的宏定义:位于文件/kernel/arch/arm/include/Uapi/asm/unistd.h,记录着内核空间的系统调用号,格式为#define__NR_xxx (__NR_SYSCALL_BASE+[num])
  3. 系统调用的函数声明:位于文件/kernel/include/linux/syscalls.h,格式为asmlinkage long sys_xxx(args ...);
  4. 系统调用的函数实现:不同函数位于不同文件,比如kill()位于/kernel/kernel/signal.c文件,格式为SYSCALL_DEFINEx(x, sname, ...)

前面这4步都是在内核空间相关的文件定义,有了这些,那么内核就可以使用相应的系统调用号。

用户空间

  1. 系统调用号的宏定义:位于文件unistd.h,记录着用户空间的系统调用号,格式为#define__NR_xxx (__NR_SYSCALL_BASE+[num])。这个文件就是由内核空间同名的头文件自动生成的,所以该文件与内核空间的系统调用号是完全一致。

  2. 汇编定义相关函数的中断调用过程:位于文件/bionic/libc/arch-arm/syscalls/xxx.S,比如kill()位于kill.S,格式为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ENTRY(xxx)
    mov ip, r7
    ldr r7, =__NR_xxx
    swi #0
    mov r7, ip
    cmn r0, #(MAX_ERRNO + 1)
    bxls lr
    neg r0, r0
    b __set_errno_internal
    END(xxx)

当然kill()方法还有函数声明,有了这些,用户空间也能在程序中使用系统调用。
参考如下:

1、CSDN博主「chosen0ne」的原创文章,原文链接:https://blog.csdn.net/chosen0ne/article/details/7721550

2、http://gityuan.com/2016/05/21/syscall/#%E4%B8%89syscall%E6%B5%81%E7%A8%8B

3、《奔跑吧 Linux内核》 笨叔 陈悦 著

4、https://zhuanlan.zhihu.com/p/267353577