linux系统调用实现机制详解(内核4.14.4)前言
1.1 linux系统调用介绍
linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。和普通库函数调用相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
在Linux中,每个系统调用被赋予一个系统调用号。通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。
系统调用号一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。
结合具体源码来看下实现机制。
1.2 系统调用表和调用号
具体号子分配在文件arch/x86/entry/syscalls/syscall_64.tbl中定义,如下:
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
………
30 common shmat sys_shmat
31 common shmctl sys_shmctl
32 common dup sys_dup
33 common dup2 sys_dup2
34 common pause sys_pause
35 common nanosleep sys_nanosleep
36 common getitimer sys_getitimer
37 common alarm sys_alarm
38 common setitimer sys_setitimer
39 common getpid sys_getpid
40 common sendfile sys_sendfile64
41 common socket sys_socket
…….
也可以在arch/x86/include/generated/uapi/asm/unistd_64.h文件中查找到系统调用号。
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
……
1.2 系统调用声明
在文件(include/linux/syscalls.h)中定义了系统调用函数声明,函数声明中的asmlinkage限定词,这用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。例如系统调用getpid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则.
如下:
asmlinkage long sys_kill(pid_t pid, int sig);
1.3 系统调用实现
不同的系统调用实现在不同的文件中,例如sys_read 系统调用实现在fs/read_write.c文件中,sys_socket定义在net/socket.c中。
例如sys_socket的原型如下:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
其中3表示有3个参数,用于解析参数时候使用。
查看宏SYSCALL_DEFINE3的定义,定义也在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_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#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__))
我们看到SYSCALL_DEFINE3指向SYSCALL_DEFINEx,而SYSCALL_DEFINEx指向__SYSCALL_DEFINEx,在__SYSCALL_DEFINEx宏中调用真正的原型,如sys_socket(其也定义在syscalls.h)。
所以SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 就是sys_socket函数。具体实现后续会在linux协议栈中进行介绍。
置于为什么会这么复杂,因为linux发展过程中难免碰到各种漏洞,有些则是因为修改漏洞需要,例如CVE-2009-0029漏洞
https://bugzilla.redhat.com/show_bug.cgi?id=479969
1.4 系统调用总接口
之前在arch/x86/kernel/entry_64.S中实现了system_call的系统调用总接口。根据系统参数参数号来执行具体的系统调用。
现在所有socket相关的系统调用,都会使用sys_socketcall的系统调用,如下socketcall的代码片段,根据参数进入switch…case…判断操作码,跳转至对应的系统接口:
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
……
switch (call) {
case SYS_SOCKET:
err = sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_CONNECT:
err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
break;
case SYS_LISTEN:
err = sys_listen(a0, a1);
break;
case SYS_ACCEPT:
err = sys_accept4(a0, (struct sockaddr __user *)a1,
(int __user *)a[2], 0);
break;
case SYS_GETSOCKNAME:
err =
sys_getsockname(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
break;
case SYS_GETPEERNAME:
err =
sys_getpeername(a0, (struct sockaddr __user *)a1,
(int __user *)a[2]);
这里的变量定义在文件include/uapi/linux/net.h中,如下
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */
1.5 系统调用流程
整体的系统调用的过程如下,由应用程序调用C库提供的API函数,该API实现函数会调用内核的统一入口函数,具体到系统调用。
图中逻辑为常用的系统调用。Socket相关的系统调用入口函数为sys_socketcall
如果出现错误,错误码定义在文件:
include/uapi/asm-generic/errno-base.h中。
具体看下节中的socket系统调用。
1.6 socket具体实现流程例子
Socket 的API函数 socket ()(该函数定义在/usr/include/sys/socket.h文件中)
extern int socket (int __domain, int __type, int __protocol) __THROW;
glibc库对socket系统调用进行了封装。位于文件
sysdeps/unix/sysv/linux/i386/socket.S中
其中定义了# define __socket socket,调用__socket就是调用socket函数。
该函数是对socket函数的封装,代码中主要逻辑是调用sys_socketcall系统调用,参数为socket的调用号,然后用socketcall函数来进行调用socket。
整体逻辑看上方图。
可以编译一个使用socket系统调用的应用程序,进行gdb调试,运行到socket时候进行反汇编显示如下,下面标红的一行是移动0x29到eax,而0x29就是41,就是socket系统调用的系统号:
(gdb) disass socket
Dump of assembler code for function socket:
=> 0x00007ffff78f85a0 <+0>: mov $0x29,%eax
0x00007ffff78f85a5 <+5>: syscall
0x00007ffff78f85a7 <+7>: cmp $0xfffffffffffff001,%rax
0x00007ffff78f85ad <+13>: jae 0x7ffff78f85b0 <socket+16>
0x00007ffff78f85af <+15>: retq
0x00007ffff78f85b0 <+16>: mov 0x2bb8c1(%rip),%rcx # 0x7ffff7bb3e78
0x00007ffff78f85b7 <+23>: neg %eax
0x00007ffff78f85b9 <+25>: mov %eax,%fs:(%rcx)
0x00007ffff78f85bc <+28>: or $0xffffffffffffffff,%rax
0x00007ffff78f85c0 <+32>: retq
End of assembler dump.
1.7 系统调用跟踪
编写一个代码如下:
#include <unistd.h>
#include <fcntl.h>
int main(){
int handle,bytes;
void * ptr;
handle=open("tmp/test.txt",O_RDONLY);
close(handle);
return 0;
}
编译:gcc -o hell hello.c
使用strace命令进行跟踪:
# strace -o log.txt ./hello
打开log.txt可以看到如下内容:
execve("./hello", ["./hello"], [/* 22 vars */]) = 0
brk(NULL) = 0xa1e000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc310ad000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=71985, ...}) = 0
mmap(NULL, 71985, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcc3109b000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fcc30ac0000
mprotect(0x7fcc30c80000, 2097152, PROT_NONE) = 0
mmap(0x7fcc30e80000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fcc30e80000
mmap(0x7fcc30e86000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcc30e86000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc3109a000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc31099000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc31098000
arch_prctl(ARCH_SET_FS, 0x7fcc31099700) = 0
mprotect(0x7fcc30e80000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7fcc310af000, 4096, PROT_READ) = 0
munmap(0x7fcc3109b000, 71985) = 0
open("tmp/test.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
close(-1) = -1 EBADF (Bad file descriptor)
exit_group(0) = ?
+++ exited with 0 +++
注意到最后几行就是我们程序的系统中实现的系统调用。
看到open返回的是-1(ENOENT),因为在tmp目录中不存在test.txt文件。而且我们程序中没有对文件打开与否进行判断,导致出错了应用也不知道,只能通过strace来进行跟踪。
这个也是在(文件include/uapi/asm-generic/errno-base.h中定义的:
#define ENOENT 2 /* No such file or directory */)
我们再来看一下之前的一大堆调用,这是为支持我们写的程序运行,系统进行的进程创建、内存映射等等工作。我们在代码中只写了几行,但是系统却在编译链接以及加载到内存的时候做了非常多的事情。
所以,在开发应用程序的时候还会觉得麻烦么?最麻烦的事情底层其实都已经帮我们做好了,实在是找不到借口和老板说应用程序开发很麻烦了哦。
创建一个tmp/test.txt文件,再调用发现最后三行如下:
open("tmp/test.txt", O_RDONLY) = 3
close(3) = 0
exit_group(0) = ?
说明打开正确了。后续如果要诊断程序的系统调用问题可以使用strace函数。
1.8 小结
由于网络上关于系统的调用的介绍代码引用比较分散切老旧对新步入的同学造成不同的费解,因此总结此文。
本文基于内核4.14.14代码介绍了linux系统调用,将系统调用表、调用号所在源码位置标出,同时梳理的系统调用的整个执行逻辑。最后剖析了socket用户接口和sys_socket系统调用之间的关系。针对函数细节没有进行深入,这个未来会有专项课题。
如有错误欢迎指正,祝大家玩的愉快。