Socket与系统调用深度分析
- Socket API编程接口之上可以编写基于不同网络协议的应用程序;
- Socket接口在用户态通过系统调用机制进入内核;
- 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析。
- socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 32环境下Linux5.0以上的内核中进一步跟踪验证。
为了与上次实验保持一致,本次实验使用的是上次实验的linux-5.0.1下的32位X86系统。搭建过程见上文。
一、原理简介
1、系统调用介绍
为了保证系统的安全性,linux系统设置了两种控制权限。第一种是用户权限,其只能执行不涉及系统的指令;第二种是内核权限,它可以执行系统中的所有指令,于此同时,通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。
而当普通用户需要执行系统相关的指令时,就无法直接访问系统的指令,只能通过系统调用的方式,向内核发送指令产生软中断,进入内核状态,执行系统相关的函数和指令。
2、linux内核执行系统调用过程简介
Linux系统调用示意图
(1)用户态
应用程序:是一个普通程序,例如用户编写的应用。当其涉及到内核操作时,执行系统调用程序:执行软中断,Intel架构中使用的是0X80。
系统调用程序:当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数由各通用寄存器传递。然后执行INT 0X80,以核心态进入入口地址system_call。
(2)内核态
系统调用处理程序:有两个关键的文件,Entry.s和unistd.h文件。
在unistd中为每个唯一的系统调用规定了唯一的系统调用号,当得到相应的系统调用号后,转到entry.s找到相应的内容。
在entry.s中规定的system_call就是所有系统调用的总入口,entry.s中规定了系统调用表。
sys_call_table记录了各sys_name函数在表中的位子。有了这张表,很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。
3、系统调用初始化过程
对系统调用的初始化也即对INT 0X80的初始化。系统启动时,汇编子程序setup_idt(head.S)准备了张256项的idt 表start_kernel()(init/main.c)、trap_init()(traps.c)调用的C语言宏定义set_system_gate(0x80, &system_call)( system.h)设置0X80号软中断的服务程序为system_call。system_call(entry.S)就是所有系统调用的总入口。
二、网络程序系统调用
1、查看系统调用与中断的关系:
在qemu启动过程中分析system_call中系统调用和中断处理过程。
start_kernel --> trap_init --> idt_setup_traps --> entry_INT80_32
设置断点1:start_kernel是所有程序初始化的入口点,因此在init中的main进行设置start_kernel断点1。
设置断点2:系统调用实质是指令发起的软中断,trap_init中断初始化。
该程序主要定义和实现了asm.s中所引用的各个硬件异常中断处理程序。
设置断点3:系统调用中断点。
在5.0内核int0x80对应的中断服务例程是entry_INT80_32,而不是原来的名称system_call了
其对应的ENTRY(entry_INT80_32),即为使用0x80引入中断。
由此可以观察到,内核处理系统调用是将其视为一个特殊的中断进行处理的。
Qemu程序如图所示:
转到:Do_entry_INT80_32
程序继续运行
2、查看系统调用表
文件在arch/x86/entry/syscall_32目录下,系统调用号是102。
在sys_socketcall处添加断点,运行replyhi程序,找到其socket源码位置是位于net/socket位置。
部分代码:
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]); … }
研究其代码,发现了其中有很多常用的socket相关的代码,比如最常见的bind、accept、listen等,客户端采用TCP协议时的监听函数LISTEN,绑定函数BIND,接收客户端发来的消息ACCEPT等。
追踪replyhi程序调用过程,输入s单步执行查看程序调用函数:
发现继续调用了create函数。
3、重载功能的实现
以create_socket为例,socket是通过参数的不同值跳到不同的方法,实现重载功能的。
int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { int err; struct socket *sock; const struct net_proto_family *pf; /* * Check protocol is in range */ if (family < 0 || family >= NPROTO) return -EAFNOSUPPORT; if (type < 0 || type >= SOCK_MAX) return -EINVAL; /* Compatibility. This uglymoron is moved from INET layer to here to avoid deadlock in module load. */ if (family == PF_INET && type == SOCK_PACKET) { pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n", current->comm); family = PF_PACKET; } err = security_socket_create(family, type, protocol, kern); if (err) return err; ……… out_release: rcu_read_unlock(); goto out_sock_release; } EXPORT_SYMBOL(__sock_create); int sock_create(int family, int type, int protocol, struct socket **res) { return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0); } EXPORT_SYMBOL(sock_create); int sock_create_kern(struct net *net, int family, int type, int protocol, struct socket **res) { return __sock_create(net, family, type, protocol, res, 1); } EXPORT_SYMBOL(sock_create_kern);
调用具体的处理函数
int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { int err; struct socket *sock; const struct net_proto_family *pf; /* * Check protocol is in range */ if (family < 0 || family >= NPROTO) return -EAFNOSUPPORT; if (type < 0 || type >= SOCK_MAX) return -EINVAL; /* Compatibility. This uglymoron is moved from INET layer to here to avoid deadlock in module load. */ if (family == PF_INET && type == SOCK_PACKET) { pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n", current->comm); family = PF_PACKET; } err = security_socket_create(family, type, protocol, kern); if (err) return err; /*
当退出断点时,qemu程序继续执行。
至此,socket中系统调用完成。