• 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状态,等待客户端的连接。

  • 相关阅读:
    通过CMMI5的国内企业有几个?这个认证是不是很牛啊?
    CNUTCon 全球运维技术大会2017
    新浪微博基于Docker的混合云架构与应用实践-DockerInfo
    k8s~为服务添加ingress的实现
    springboot~HttpPut开启application/x-www-form-urlencoded
    K8s~为pod添加sidecar进行日志收集
    k8s~部署EFK框架
    springboot~Transactional注解的注意事项
    CentOS 6.4 快速安装Nginx笔记
    去掉NSString中的HTML标签
  • 原文地址:https://www.cnblogs.com/huangmengyu/p/12069834.html
Copyright © 2020-2023  润新知