本次实验要求:
请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。
Socket API编程接口:
C语言中的Socket API就是一种涉及系统调用的API,常用的函数如下:
int socket(int domain, int type, int protocol) //创建一个新的套接字,返回套接字描述符 int connect(int sockfd, struct sockaddr *server_addr, int sockaddr_len) //同远程服务器主动连接,成功时返回0,失败时返回1 int bind(int sockfd, struct sockaddr* my_addr, int addrlen) //为套接字指明一个本地端点地址,TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟悉的端口号,然后等待连接 int listen(int sockfd, int input_queue_size) //面向连接的服务器指明某个套接字,将其置为被动模式,并准备接收传入连接 int accept(int sockfd, void* addr, int* addrlen) //获取传入连接请求,返回新的连接套接字描述符,为每个新连接请求创建一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字 int sendto(int sockfd, const void* data, int data_len, unsigned int flags, struct sockaddr* remaddr,int remaddr_len) //基于UDP发送数据报,返回实际发送的数据长度,出错时返回1 int send(int sockfd, const void* data, int data_len, unsigned int flags) //在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1,将外发数据复制到OS内核中 int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,int *fromlen); //从UDP接收数据,返回实际接收的字节数,失败时返回-1 int recv(int sockfd, void* buf, int buf_len,unsigned int flags) //从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃 close(int sockfd) //撤销套接字,如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它
网络程序调用基本流程如下图所示:
socket api和系统调用关系
系统调用:
在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:
系统态(也称为管态或核心态),操作系统在系统态运行
用户态(也称为目态),应用程序只能在用户态运行。
在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令两类。
执行态切换过程:
应用程序在用户态准备好调用参数,执行 int 指令触发软中断 ,中断号为 0x80 ;
CPU 被软中断打断后,执行对应的中断处理函数 ,这时便已进入内核态 ;
系统调用处理函数准备内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);
系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;
系统调用处理函数准备返回值并从内核栈中恢复 寄存器 ;
系统调用处理函数执行 ret 指令切换回用户态 ;
socket系统调用发生流程
当用户进程使用socket API 的时候,会产生向量为0x80的编程异常,系统执行系统调用。
进程传递系统调用号到寄存器eax,指明需要哪个系统调用,同时会将系统调用需要的参数存入相关寄存器。
系统调用处理函数system_call是Linux中所有系统调用的入口点,通过进程存在eax寄存器中的系统调用号决定调用哪个系统调用。
其中,socket api有两种系统调用方式:(1)所有的socket系统调用的总入口是sys_socketcall(系统调用号102) (2)每一个独立的socket api都对应一个单独的系统调用。
初始化 MenuOS 系统的网络功能,跟踪分析 TCP 协议:
在本次实验中,我们创建的是一个利用socket的基于TCP的连接,接下来我们结合源码,接口来进行整个hello/hi的实现过程的调用分析与追踪。
上次的课程实验实现了menuos的调试环境的配置主要是在menuos中增加了replyhi和hello两条命令,其结果大致如下:
我们首先以最常用的listen()函数为例进行分析:
进入上次的MenuOS目录,更改makefile,将-S参数去除。(否则将会挂起CPU)
之后在终端编译:
make rootfs
结果如下所示:
在 gdb中给__sys_linsten打上断点:
gdb file ./vmlinux target remote:1234 break __sys_listen
结果如下所示:
说明gdb已找到这两条系统调用的函数定义所在。
我们接下来看一下接下来要分析的replyhi和hello的源码
int main() { BringUpNetInterface(); PrintMenuOS(); SetPrompt("MenuOS>>"); MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL); MenuConfig("quit","Quit from MenuOS",Quit); MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi); MenuConfig("hello", "Hello TCP Client", Hello); ExecuteMenu(); }
从上面代码中看到replyhi和hello命令需要分别调用 StartReplyhi 和 Hello函数。
下面是StartReplyhi的源码:
int StartReplyhi(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid < 0)
{
/* error occurred */
fprintf(stderr, "Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/* child process */
Replyhi();
printf("Reply hi TCP Service Started!\n");
}
else
{
/* parent process */
printf("Please input hello...\n");
}
}
从上面看出StartReplyhi又调用了
{ char szBuf[MAX_BUF_LEN] = "\0"; char szReplyMsg[MAX_BUF_LEN] = "hi\0"; InitializeService(); while (1) { ServiceStart(); RecvMsg(szBuf); SendMsg(szReplyMsg); ServiceStop(); } ShutdownService(); return 0; } #InitializeService(),ServiceStart()这些函数均定义在头文件syswrapper.h中 /* public macro */ #define InitializeService() \ PrepareSocket(IP_ADDR,PORT); \ InitServer(); #InitializeService()中分别调用了PrepareSocket和InitServer() /* private macro */ #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); #显然我们可以发现到这一步,InitializeService这个宏已经完成了socket的创建,绑定和listen #我们继续看ServiceStart(),RecvMsg(szBuf),SendMsg(szReplyMsg),ServiceStop(),ServiceStop() #define ServiceStart() \ int newfd = accept( sockfd, \ (struct sockaddr *)&clientaddr, \ &addr_len); \ if(newfd == -1) \ { \ fprintf(stderr,"Accept Error,%s:%d\n", \ __FILE__,__LINE__); \ } #这一步完成了accept #define RecvMsg(buf) \ ret = recv(newfd,buf,MAX_BUF_LEN,0); \ if(ret > 0) \ { \ printf("recv \"%s\" from %s:%d\n", \ buf, \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ } #这一步完成了recv #define SendMsg(buf) \ ret = send(newfd,buf,strlen(buf),0); \ if(ret > 0) \ { \ printf("rely \"hi\" to %s:%d\n", \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ } #这一步完成了send #define ServiceStop() close(newfd); #这一步完成了close
hello的过程与其类似,通过源码我们可以看出replayhi函数涉及到的系统调用按照调用顺序为
socket,bind,listen,accept,recv,send
于是我们在一个终端打开qemu启动MenuOS(指令中去掉 -S,在另一个终端用gdb读入linux-5.0.1的vmlinux,
通过端口1234与qemu建立连接,在相关的系统调用内核处理函数处设置断点,结果如下图所示:
接着在gdb中按c运行Menu OS
上图中可以看到,首先就等待了。然后我们在Menu OS中打开服务器,即输入replyhi:
不停地按回车,发现捕获到如下断点,一直到sys_accept4函数停止。说明此时服务器处于阻塞状态,一直在等待客户端连接。
打开qemu发现指令已经完整运行:
通过实验可知,在replyhi中,分别调用了socket、bind、listen、accept。
结果与预想一致,至此完成对replyhi/hello的追踪。