实验背景:
- Socket API编程接口之上可以编写基于不同网络协议的应用程序;
- Socket接口在用户态通过系统调用机制进入内核;
- 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
- socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
前言
之前我们简单分析了用户态下封装的Socket工具与底层Socket的关系详情见这里,本次实验将针对Socket的调用过程,基于Linux提供的Socket相关接口进行其用户态到系统态的原理及过程分析,包括对Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数的详细分析。 本次将首先从简单Socket调用原理入手,讲解Socket函数调用链关系,再进行底层调用的探究实验。
首先抛出问题,用户态下的Socket怎么与底层内核建立连接的呢?
系统调用
在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:
- 系统态(也称为管态或核心态),操作系统在系统态运行
- 用户态(也称为目态),应用程序只能在用户态运行。
正常情况下,应用程序工作在用户态下,出于保护系统安全性的目的,用户态留给用户可用功能有限,所以就预留给用户一些可用内核空间,使应用程序可以通过系统调用的方法,间接调用操作系统的相关过程,取得相应的服务。当需要执行内核操作时就需要进行向内核态的转换,可以称之为系统调用。
状态的转换通过软中断进入,中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中通过维护中断向量表维护这一关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。这里涉及状态保存及返回问题,不做过多描述,嵌套的调用过程如下:
我们这里说的软中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表调用第0x80号的中断处理程序。
在此,我们以一个经典的xyz函数系统调用为例进行还原以上系统调用过程
- 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
- 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
- CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
- 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
总结下来就是用户执行带有中断指令的程序时,执行到中断调用指令int 0x80会跳转到中断处理函数,这也就是系统中断调用的接入口,通过这个介入口获取到进入内核态所需的资源,当现场保存完成、返回地址保存完成后cpu进入到内核态,并从system_call处开始指令执行(同时sys_call_table也就是上面说到的系统调用表),返回用户态时类似,具体函数调用过程如下:- start_kernel
- trap_init
- idt_setup_traps
跟踪系统调用
对系统调用有了大致了解后我们进入正题,基于上次实验qumu模拟器和gdb调试观察系统调用过程。
首先观察Replyhi函数
int Replyhi()
{
char szBuf[MAX_BUF_LEN] = " ";
char szReplyMsg[MAX_BUF_LEN] = "hi ";
InitializeService();
while (1)
{
ServiceStart();
RecvMsg(szBuf);
SendMsg(szReplyMsg);
ServiceStop();
}
ShutdownService();
return 0;
}
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!
");
}
else
{
/* parent process */
printf("Please input hello...
");
}
}
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
ExecuteMenu();
}
我们发现Replyhi函数中,依次调用了InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()以及最后的ShutdownService()函数,我们依次来看这些函数究竟是如何调用socket API的。
#ifndef _SYS_WRAPER_H_
#define _SYS_WRAPER_H_
#include<stdio.h>
#include<arpa/inet.h> /* internet socket */
#include<string.h>
//#define NDEBUG
#include<assert.h>
#define PORT 5001
#define IP_ADDR "127.0.0.1"
#define MAX_BUF_LEN 1024
/* 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
",
__FILE__,__LINE__);
close(sockfd);
return -1;
}
listen(sockfd,MAX_CONNECT_QUEUE);
#define InitClient()
int ret = connect(sockfd,
(struct sockaddr *)&serveraddr,
sizeof(struct sockaddr));
if(ret == -1)
{
fprintf(stderr,"Connect Error,%s:%d
",
__FILE__,__LINE__);
return -1;
}
/* public macro */
#define InitializeService()
PrepareSocket(IP_ADDR,PORT);
InitServer();
#define ShutdownService()
close(sockfd);
#define OpenRemoteService()
PrepareSocket(IP_ADDR,PORT);
InitClient();
int newfd = sockfd;
#define CloseRemoteService()
close(sockfd);
#define ServiceStart()
int newfd = accept( sockfd,
(struct sockaddr *)&clientaddr,
&addr_len);
if(newfd == -1)
{
fprintf(stderr,"Accept Error,%s:%d
",
__FILE__,__LINE__);
}
#define ServiceStop()
close(newfd);
#define RecvMsg(buf)
ret = recv(newfd,buf,MAX_BUF_LEN,0);
if(ret > 0)
{
printf("recv "%s" from %s:%d
",
buf,
(char*)inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port));
}
#define SendMsg(buf)
ret = send(newfd,buf,strlen(buf),0);
if(ret > 0)
{
printf("rely "hi" to %s:%d
",
(char*)inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port));
}
#endif /* _SYS_WRAPER_H_ */
综合以上代码,我们能够看到系统定义的函数首先调用InitializeService(),根据定义,依次调用socket()--->bind()--->listen(),这些是socket编程的一般步骤。然后调用ServiceStart()函数,通过宏定义,调用了accept()函数。然后是RecvMsg()和SendMsg()函数,根据宏定义,调用了recv和send函数
当我们查看socket.c源代码,能够发现,Socket的第一步,socket()函数首先进行了系统调用,也就是对入口函数sys_scoketcall的调用,通过传入用户定义的参数地址,进行系统调用的传参。
接下来我们在开始gdb跟踪之前找到系统自定义的函数宏定义标准,其结果如下(用于后面跟踪调试时查看具体是什么调用过程):
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */其中
所以接下来针对sys_scoketcall函数监视,观察系统调用过程。
首先开启qemu模拟器,执行
qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S
打开新的终端窗口,进入gdb调试,执行
file ~/LinuxKernel/linux-5.0.1/vmlinux
b sys_socketcall
target remote:1234
在qemu模拟器中继续执行,键入replyhi
,观察断点监视情况如下:
能够看到此次过程调用了4次sys_socketcall函数,其中调用的编号分别为 1、2、4、5
至此我们查看sys_define中的具体定义,在此忽略。以上过程调用过程依次对应了,__sys_socket、__sys_bind、__sys_listen、__sys_accept
函数调用,至此Socket所需资源初始化成功,我们继续进行跟踪,在qemu中键入hello
,其结果如下:
能够看到这次hello回应结束后,继续执行断点,看到调用编号分别为1、3、10、9、10、9、10、9、10、9、5
查看上面的函数宏定义分别对应函数sys_socket(2) sys_connect(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_accept(2)
这也完全对应上了上述过程,描述如下:
- 服务端创建socket
- 建立tcp连接
- 进行hello hi的四次通信过程
- 继续回到accpet状态接收消息
至此,基于qemu及gdb调试过程结束,socket如何在内核中变化定义也有了一些眉目。