Socket与系统调用深度分析
1.linux系统调用
本次实验的主要内容是从socket接口入手,通过跟踪相关函数在内核中的运行过程了解socket相关的系统调用是如何工作的。操作系统分为用户态和内核态,应用程序一般工作在用户态,而操作系统则通过系统调用为工作在其上的进程提供服务。系统调用是操作系统提供给用户程序访问内核的桥梁,通过系统调用,运行于用户态的用户程序能够调用到运行于内核态的系统内核提供的功能。系统调用一般是由软中断实现的,在Linux上该功能是由中断号为0x80的系统调用处理程序system_call提供。下面以Linux socket API为例,探究Linux中系统调用是如何进行的。
Socket与系统调用关系
Socket API编程接口之上可以编写基于不同网络协议的应用程序;
Socket接口在用户态通过系统调用机制进入内核;
内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
系统调用
(1)系统调用就是为了让应用程序可以访问系统资源,每个操作系统都会提供一套接口供应用程序使用。这些接口通常通过中断来实现,例如在windows中是0x2E号中断作为系统调用入口,linux使用0x80号中断作为系统的调用的入口。
(2)系统调用的弊端:各个操作系统的系统调用不兼容,使用不方便,系统调用比较原始。运行库解决这两个问题,它的使用统一不会随着操作系统或编译器的变化而变化。
(3)系统调用的原理
系统调用是运行在内核态的,而用户程序一般是运行在用户态的,操作系统一般通过中断从用户态切换到内核态。中断具有两个属性一个是中断号,一个是中断向量表,是一个数组,包含中断处理程序。一个中断号对应一个中断处理程序。中断分为硬件中断和软件中断,软件中断通常是一条指令,带有一个参数代表中断号。在linux中使用0x80来触发所有的系统调用。和中断一样系统调用都有一个系统调用号,系统调用号代表在系统调用表中的位置。
系统调用与一般函数调用的区别
一般的函数调用均是在用户态进行的,函数调用是通过栈来传递参数。而系统调用是通过寄存器来传递参数。
函数返回:一般函数调用的返回值如果是小字节返回则用eax带出,如果是8~15字节则用edx和eax带出。对于大字节,他会在调用方的栈帧中开辟一个空间,作为返回的临时对象,在调用函数时会将这块地址隐式的传入,调用函数的返回值拷贝到临时对象中,并将临时对象的地址用eax带出。
网络相关的主要的系统调用
1 系统调用号 函数名 系统调用 所在文件 2 41 socket sys_socket net/socket.c 3 42 connect sys_connect net/socket.c 4 43 accept sys_accept net/socket.c 5 44 sendto sys_sendto net/socket.c 6 45 recvfrom sys_recvfrom net/socket.c 7 46 sendmsg sys_sendmsg net/socket.c 8 47 recvmsg sys_recvmsg net/socket.c 9 48 shutdown sys_shutdown net/socket.c 10 49 bind sys_bind net/socket.c 11 50 listen sys_listen net/socket.c 12 51 getsockname sys_getsockname net/socket.c 13 52 getpeername sys_getpeername net/socket.c 14 53 socketpair sys_socketpair net/socket.c 15 54 setsockopt sys_setsockopt net/socket.c 16 55 getsockopt sys_getsockopt net/socket.c
lab3中的main.c文件
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi); MenuConfig("hello", "Hello TCP Client", Hello);
syswrapper.h中代码
#include/* internet socket */
/**/
#define PrepareSocket(addr,port) \ int sockfd = -1; \ struct sockaddr_in serveraddr; \ struct sockaddr_in clientaddr; \ socklen_t addr_len = sizeof(struct sockaddr); \ serveraddr.sin_family = AF_INET; \ serveraddr.sin_port = htons(port); \ serveraddr.sin_addr.s_addr = inet_addr(addr); \ memset(&serveraddr.sin_zero, 0, 8); \ sockfd = socket(PF_INET,SOCK_STREAM,0); #define InitServer() \ int ret = bind( sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Bind Error,%s:%d\n", \ __FILE__,__LINE__); \ close(sockfd); \ return -1; \ } \ listen(sockfd,MAX_CONNECT_QUEUE);
/**/
#define InitializeService() \
PrepareSocket(IP_ADDR,PORT); \
InitServer();
socket(),listen(),bind()以及close()等函数
#启动qemu 加载内核,不加-S,因为跟踪的系统调用,不是启动过程
qemu -kernel ../linux-5.4.2/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s
#打开一个新的终端
gdb
file ~/linux-5.4.2/vmlinux
b sys_socketcall
target remote:1234
c
#在qume中输入
replyhi
在sys_socketcall打了断点后,运行我们的replyhi程序,即在断点处停止了,我们查看其函数调用栈,发现其进入系统调用的顺序是 entry_SYSENTER_32() ---> do_syscall_32_irqs_on()---->sys_socketcall()。内核中为 socket 设置的总入口为 sys_socketcall(),其代码在 net/socket.c 中,而该函数实际上则调用的是SYSCALL_DEFINE2:
1 /*与socket相关的系统调用总入口。 */ 2 /* 3 *函数的第一个参数 call 即为具体的操作码,而参数 args 为指向一个数组的指针,可以根据具体操作码的不同,确定从用户空间复制参数的数量; 4 */ 5 asmlinkage long sys_socketcall(int call, unsigned long __user *args) 6 { 7 unsigned long a[6]; 8 unsigned long a0,a1; 9 int err; 10 11 if(call<1||call>SYS_RECVMSG) 12 return -EINVAL; 13 14 /* copy_from_user should be SMP safe. */ 15 if (copy_from_user(a, args, nargs[call])) 16 return -EFAULT; 17 18 a0=a[0]; 19 a1=a[1]; 20 21 switch(call) 22 { 23 case SYS_SOCKET: 24 err = sys_socket(a0,a1,a[2]); 25 break; 26 case SYS_BIND: 27 err = sys_bind(a0,(struct sockaddr __user *)a1, a[2]); 28 break; 29 case SYS_CONNECT: 30 err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); 31 break; 32 case SYS_LISTEN: 33 err = sys_listen(a0,a1); 34 break; 35 case SYS_ACCEPT: 36 err = sys_accept(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 37 break; 38 case SYS_GETSOCKNAME: 39 err = sys_getsockname(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 40 break; 41 case SYS_GETPEERNAME: 42 err = sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); 43 break; 44 case SYS_SOCKETPAIR: 45 err = sys_socketpair(a0,a1, a[2], (int __user *)a[3]); 46 break; 47 case SYS_SEND: 48 err = sys_send(a0, (void __user *)a1, a[2], a[3]); 49 break; 50 case SYS_SENDTO: 51 err = sys_sendto(a0,(void __user *)a1, a[2], a[3], 52 (struct sockaddr __user *)a[4], a[5]); 53 break; 54 case SYS_RECV: 55 err = sys_recv(a0, (void __user *)a1, a[2], a[3]); 56 break; 57 case SYS_RECVFROM: 58 err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3], 59 (struct sockaddr __user *)a[4], (int __user *)a[5]); 60 break; 61 case SYS_SHUTDOWN: 62 err = sys_shutdown(a0,a1); 63 break; 64 case SYS_SETSOCKOPT: 65 err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); 66 break; 67 case SYS_GETSOCKOPT: 68 err = sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); 69 break; 70 case SYS_SENDMSG: 71 err = sys_sendmsg(a0, (struct msghdr __user *) a1, a[2]); 72 break; 73 case SYS_RECVMSG: 74 err = sys_recvmsg(a0, (struct msghdr __user *) a1, a[2]); 75 break; 76 default: 77 err = -EINVAL; 78 break; 79 } 80 return err;
涉及到socket的系统调用都使用统一的入口sys_socketcall,再通过SYSCALL_DEFINE2进入不同的分支,调用不同的系统调用,如在本次实验中,就包括sys_socket,sys_bind,sys_listen,sys_connect,sys_accept4等等。