linux系统调用实现机制详解(内核4.14.4)

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实现函数会调用内核的统一入口函数,具体到系统调用。

86340e27c4c2b0fab0bd0667eec165877b893583

            图中逻辑为常用的系统调用。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系统调用之间的关系。针对函数细节没有进行深入,这个未来会有专项课题。

            如有错误欢迎指正,祝大家玩的愉快。

### 回答1: Linux系统调用号是指操作系统提供给用户程序调用的接口函数的编号。每个系统调用都有一个唯一的调用号,用于标识该函数。在Linux中,系统调用号是通过一个整数来表示的,不同的系统调用对应不同的整数值。用户程序可以通过系统调用号来调用相应的系统调用,从而实现操作系统的各种功能的访问和控制。常见的Linux系统调用包括open、read、write、close、fork、execve等。 ### 回答2: Linux系统调用号是一个唯一的标识符,用于标识操作系统提供给用户程序调用的各种功能和服务。通过系统调用,用户程序可以请求操作系统执行特定的操作,如打开文件、创建进程、读取网络数据等。 在Linux中,每个系统调用都有一个特定的号码,这个号码是由操作系统内核分配的。这些号码是在系统的头文件中定义的,例如unistd.h文件中包含了系统调用号的定义。 系统调用号在调用系统调用时使用,用户程序可以使用相关的系统调用接口来执行操作系统提供的功能。用户程序通常会使用C语言的库函数封装系统调用,以提供更方便的接口给开发者使用。 系统调用号的分配通常是由操作系统的开发者决定的,他们会根据不同的功能和服务进行划分和分配。在Linux中,常见的系统调用号包括打开文件(open)、读取文件(read)、写入文件(write)、关闭文件(close)等。 系统调用号的使用可以在用户程序中通过系统调用指令实现,用户程序将需要执行的系统调用号存放在相应的寄存器中,并调用int 0x80或sysenter指令触发系统调用。 总之,Linux系统调用号是一种用于标识和调用操作系统功能的机制,它允许用户程序直接访问操作系统提供的各种服务和功能。这种机制使得用户程序可以与操作系统交互,实现更加强大和灵活的应用程序开发。 ### 回答3: Linux系统调用号是用于在用户空间程序和内核空间之间进行交互的接口标识符。当用户空间程序需要执行某些操作时,如创建进程、读写文件、网络通信等,就会调用相应的系统调用系统调用号是一个整数,每个系统调用都有一个唯一的号码与之对应。Linux内核通过系统调用号来识别用户空间程序请求的具体操作。系统调用号由内核定义并存储在一个表中,用户程序通过中断指令或软中断指令触发系统调用,将调用号传递给内核。 对于不同的操作,有不同的系统调用号。例如: 1. 创建进程的系统调用号是`fork`,对应的调用号是2; 2. 打开文件的系统调用号是`open`,对应的调用号是5; 3. 写入文件的系统调用号是`write`,对应的调用号是1; 4. 进程退出的系统调用号是`exit`,对应的调用号是60。 用户程序通过指定正确的系统调用号,将自己的请求传递给内核内核收到请求后,根据调用号执行相应的操作,完成后再返回结果给用户程序。系统调用号的定义与使用遵循一定的规范,保证了用户程序与内核之间的正确通信和操作,是Linux系统中非常重要的一个概念。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值