Socket与系统调用深度分析

Socket与系统调用深度分析

socket接口在用户态通过系统调用机制进入内核:

操作系统内核进入与退出的三种方式:系统调用、异常、中断

内核将系统调用作为一个特殊的中断来处理,即软中断(对应128号中断向量),使用int 0x80指令陷入到内核,128号中断向量对应的中断服务例程是 entry_INT80_32 。

中断向量是怎么初始化的?在内核初始化的时候(start_kernel)初始化了中断向量和对应的中断处理程序。把0x80中断与entry_INT80_32绑定,使得当用户态程序执行到int 0x80指令时程序能够跳转到entry_INT80_32。对于使用x86架构的32位系统,系统调用的初始化过程为:

start_kernel-->trap_init-->idt_setup_traps-->0x80与entry_INT80_32绑定

irq(interrupt request,中断请求)

idt(interrupt descriptor table,中断描述符表)

绑定后(初始化完),每次系统调用的底层汇编都会执行int 0x80,然后使用%eax寄存器传递系统调用号,操作系统根据系统调用号去系统调用表里找对应的服务例程。例如:用户态调用connect()函数,对应的汇编是先将362放到寄存器%eax中,然后执行int 0x80指令,进入内核,内核执行128号中断向量对应的服务例程entry_INT80_32 ,entry_INT80_32根据%eax的值362去系统调用表里找对应的服务例程为sys_connect,执行sys_connect函数,执行完毕后返回到用户态。当然这只是大致流程,具体的函数调用不会这么简单。

glibc提供的与socket有关的系统调用函数API、系统调用号及对应的内核处理函数:

系统调用号 系统调用API 对应的内核处理函数
102 socektcall sys_socketcall
359 socket sys_socket
361 bind sys_bind
362 connect sys_connect
363 listen sys_listen
6 close sys_close
364 accept4 sys_accept4
369 sendto sys_sendto
371 recvfrom sys_recvfrom

实验验证:

  1. 系统初始化过程:

断点设在:start_kernel、trap_init、idt_setup_traps,跟踪内核启动过程,

内核启动依次停在断点处,结果与分析一致。

  1. 跟踪socket系统调用直至内核处理函数

首先在sys_socketcall处设置断点:

在net/socket.c下找到该函数的定义:可以看到socketcall函数使用swith语句,根据call的值执行不同的内核处理函数,从而使不同的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]);
		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;
}

在qemu中执行命令replyhi后,服务端程序启动,使用gdb单步调试结果如下:

执行顺序为:call=1、call=2、call=4、call=5。在我们的实验环境中,socket接口的调用是通过给socket接口函数编号的方式通过112号系统调用来处理的。这些socket接口函数编号的宏定义见如下:

26#define SYS_SOCKET	1		/* sys_socket(2)		*/
27#define SYS_BIND	2		/* sys_bind(2)			*/
28#define SYS_CONNECT	3		/* sys_connect(2)		*/
29#define SYS_LISTEN	4		/* sys_listen(2)		*/
30#define SYS_ACCEPT	5		/* sys_accept(2)		*/
31#define SYS_GETSOCKNAME	6		/* sys_getsockname(2)		*/
32#define SYS_GETPEERNAME	7		/* sys_getpeername(2)		*/
33#define SYS_SOCKETPAIR	8		/* sys_socketpair(2)		*/
34#define SYS_SEND	9		/* sys_send(2)			*/
35#define SYS_RECV	10		/* sys_recv(2)			*/
36#define SYS_SENDTO	11		/* sys_sendto(2)		*/
37#define SYS_RECVFROM	12		/* sys_recvfrom(2)		*/
38#define SYS_SHUTDOWN	13		/* sys_shutdown(2)		*/
39#define SYS_SETSOCKOPT	14		/* sys_setsockopt(2)		*/
40#define SYS_GETSOCKOPT	15		/* sys_getsockopt(2)		*/
41#define SYS_SENDMSG	16		/* sys_sendmsg(2)		*/
42#define SYS_RECVMSG	17		/* sys_recvmsg(2)		*/
43#define SYS_ACCEPT4	18		/* sys_accept4(2)		*/
44#define SYS_RECVMMSG	19		/* sys_recvmmsg(2)		*/
45#define SYS_SENDMMSG	20		/* sys_sendmmsg(2)		*/

查表可知内核函数执行顺序为:SYS_SOCKET->SYS_BIND->SYS_LISTEN->SYS_ACCEPT。然后服务端处于阻塞状态,等待客户端的连接。这与理论完全一致:

然后在qemu中输入命令hello,启动客户端程序:

客户端的执行顺序为:call=1、call=3、call=10、call=9。查表可知内核函数的执行次序为:SYS_SOCKET->SYS_CONNECT->SYS_RECV->SYS_SEND。

然后服务端执行:call=10、call=9、call=5。查表可知内核函数的执行次序为:SYS_RECV->SYS_SEND->SYS_ACCEPT。服务端又阻塞在accept状态,等待客户端的连接。

原文地址:https://www.cnblogs.com/huangmengyu/p/12069834.html