socket系统调用深度分析
Posted ethan123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了socket系统调用深度分析相关的知识,希望对你有一定的参考价值。
1、系统调用过程
1.1用户态和内核态以及系统调用机制
1、进程的地址空间
linux进程有4GB地址空间,如图所示:
3G-4G大部分是共享的,是内核态的地址空间。这里存放整个内核的代码和所有的内核模块以及内核所维护的数据。
2、特权级别
对于任何操作系统来说,创建一个进程是核心功能。创建进程要做很多工作,会消耗很多物理资源。比如分配物理内存,父子进程拷贝信息,拷贝设置页目录页表等等,这些工作得由特定的进程去做,所以就有了特权级别的概念。最关键的工作必须交给特权级最高的进程去执行,这样可以做到集中管理,减少有限资源的访问和使用冲突。inter x86架构的cpu一共有四个级别,0-3级,0级特权级最高,3级特权级最低。
3、用户态和内核态
当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。
用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入3G-4G中的内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。
4、系统调用
这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。例如fork()就是执行了一个创建新进程的系统调用。系统调用的机制和新是使用了操作系统为用户特别开放的一个中断来实现,如Linux的int 80h中断。
简单来说,系统调用就是一种特殊的接口。通过这个接口,用户可以访问内核空间。系统调用规定了用户进程进入内核的具体位置。
1.2、socket系统调用过程
1、系统调用和API的区别
api是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。有时候,某些API所提供的功能会涉及到与内核空间进行交互。
那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。
2、调用过程
如图所示,程序执行到xyz()(api),进入系统调用,当运行到int 0x80这条中断指令时,跳转到entry_INT80_32,这是liunx系统中所有系统调用的入口点。entry_INT80_32不是一段普通的函数,它是一段汇编代码,同时,我们将系统调用号用eax寄存器进行传递,entry_INT80_32通过系统调用号来查询对应的内核处理函数并跳转到相应的内核处理函数执行,完毕后再按顺序逐步返回到用户态。
3、内核初始化
int 0x80指令是如何知道entry_INT80_32的加载地址?这是在内核的初始化过程完成的。内核的初始化完成了以下的函数调用过程:start_kernel > trap_init > idt_setup_traps, 其中start_kernal是内核启动的入口函数。trap_init()使得int 0x80指令和entry_INT80_32在内核初始化的过程中完成了绑定,其内容写入idt中。在后续的执行过程中,一旦出现了int 0x80这条中断指令,就会直接跳转到entry_INT80_32去了。trap_init函数中又调用了idt_setup_traps,它完成对中断描述符表的初始化。idt将每个异常或中断向量(处理例程的入口地址)分别与它们的处理过程联系起来
4、socket系统调用
与linux socket api有关的系统调用可以分为两种,第一类:所有的与socket有关的api全都使用sys_socketcall系统调用。第二类:每一个独立的socket api都对应一个单独的系统调用,
如bind对应sys_bind,listen对应sys_listen在不同的体系结构上可能用的不是socket ---> sys_socket / bind ---> sys_bind 这样的调用方式,可能调用的是socketcall()。
2、通过gdb跟踪reply的系统调用
1、进入~/MenuOS/menu/,修改makefile文件,编译,进入menuos.
2、回到目录../linux-5.0.1下,打开另一个终端,输入如下命令:
gdb file ./vmlinux target remote:1234 break sys_socketcall
3、在menuos中输入终端命令:
replyhi
hello
捕获到sys_socketcall,对应的内核处理函数为SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
4、在net/socket.c中查看SYSCALL_DEFINE2相关的源代码:
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) { unsigned long a[AUDITSC_ARGS]; unsigned long a0, a1; int err; unsigned int len; if (call < 1 || call > SYS_SENDMMSG) return -EINVAL; call = array_index_nospec(call, SYS_SENDMMSG + 1); len = nargs[call]; if (len > sizeof(a)) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, len)) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; 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]); break; case SYS_SOCKETPAIR: err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], NULL, 0); break; case SYS_SENDTO: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: err = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: err = __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_SENDMMSG: err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], true); break; case SYS_RECVMSG: err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_RECVMMSG: if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME)) err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], (struct __kernel_timespec __user *)a[4], NULL); else err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], NULL, (struct old_timespec32 __user *)a[4]); break; case SYS_ACCEPT4: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err; }
这个函数主要的功能根据不同的call来进入不同的分支,从而调用不同的内核处理函数,如__sys_bind, __sys_listen等等内核处理函数。在replyhi的执行过程中,涉及到socket的建立、bind、listen、recv和send,这些不同的系统调用传给SYSCALL_DEFINE2的参数call是不同的。这也是内核处理函数内部通过“多态机制”对不同的网络协议进行封装的体现。
以上是关于socket系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章