Socket与系统调用深层分析
实验背景:
- 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] = "\0";
char szReplyMsg[MAX_BUF_LEN] = "hi\0";
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!\n");
}
else
{
/* parent process */
printf("Please input hello...\n");
}
}
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\n", \
__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\n", \
__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\n", \
__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\n", \
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\n", \
(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如何在内核中变化定义也有了一些眉目。
Socket与系统调用深层分析的更多相关文章
- Socket与系统调用深度分析
学习一下对Socket与系统调用的分析分析 一.介绍 我们都知道高级语言的网络编程最终的实现都是调用了系统的Socket API编程接口,在操作系统提供的socket系统接口之上可以建立不同端口之间的 ...
- Socket bind系统调用简要分析
主要查看linux kernel 源码:Socket.c 以及af_inet.c文件 1.1 bind分析 #include <sys/types.h> /* See NOTES */#i ...
- 以C语言为例完成简单的网络聊天程序以及关于socket在Linux下系统调用的分析
套接字是网络编程中的一种通信机制,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程. 端 ...
- socket相关系统调用的调用流程
最近一直在读内核网络协议栈源码,这里以ipv4/tcp为例对socket相关系统调用的流程做一个简要整理,这些相关系统调用的内部细节虽然各有不同,但其调用流程则基本一致: 调用流程: (1)系统调用 ...
- 中断与系统调用深度分析(以网络编程接口SocketAPI为例)
1.从计算机CPU与I/O设备的交互方式谈起 计算机CPU与I/O设备的交互方式有最早的程序查询(也叫轮询)方式,发展到后来的程序中断方式,DMA方式等.简单来说,最早的程序查询方式的机制是,CPU若 ...
- 出现socket:(10107)系统调用失败
在编译vue项目,npm run dev出现 socket:(10107)系统调用失败 解决方案: 以管理员身份打开cmd,使用以下命令: netsh winsock reset 重启电脑即 ...
- 通过调用C语言的库函数与在C代码中使用内联汇编两种方式来使用同一个系统调用来分析系统调用的工作机制
通过调用C语言的库函数与在C代码中使用内联汇编两种方式来使用同一个系统调用来分析系统调用的工作机制 前言说明 本篇为网易云课堂Linux内核分析课程的第四周作业,我将通过调用C语言的库函数与在C代码中 ...
- 最新 x86_64 系统调用入口分析 (基于 5.7.0)
最新 x86_64 系统调用入口分析 (基于5.7.0) 整体概览 最近的工作涉及系统调用入口,但网上的一些分析都比较老了,这里把自己的分析过程记录一下,仅供参考. x86_64位系统调用使用 SYS ...
- Windows系统调用架构分析—也谈KiFastCallEntry函数地址的获取
为什么要写这篇文章 1. 因为最近在学习<软件调试>这本书,看到书中的某个调试历程中讲了Windows的系统调用的实现机制,其中讲到了从Ring3跳转到Ring0之后直接进入了K ...
随机推荐
- Python一等函数
一等对象 一等对象的定义: (1)在运行时创建 (2)能赋值给变量或数据结构中的元素 (3)能作为参数传给函数 (4)能作为函数的返回结果 ▲ Python中,整数.字符串和字典.函数都是一等对象. ...
- 之前写的关于chromedp的文章被别人转到CSDN,很受鼓励,再来一篇golang爬虫实例
示例说明:用chromedp操作chrome,导航到baidu,然后输入“美女”,然后再翻2页,在此过程中保存cookie和所有img标签内容,并保存第一页的baidu logo为png 注释已经比较 ...
- js上传文件夹
用过浏览器的开发人员都对大文件上传与下载比较困扰,之前遇到了一个php文件夹上传下载的问题,无奈之下自己开发了一套文件上传控件,在这里分享一下.希望能对你有所帮助.此控件PC全平台支持包括mac,li ...
- python一些问题
1.对于字符变量来说不需要深度复制,字符变量是不能改变的 2.文件读取结尾的判断是通过判读 line=self.fd.readline() if not line: //结束了 不用通过判断字符长度. ...
- 【记录】springboot项目的maven的pom.xml文件第一行报错 Unknown Error
原因 : maven的插件版本的问题,造成与IDE的不兼容 解决办法 :在pom中加上 <maven-jar-plugin.version>3.1.1</maven-jar-plug ...
- HDU 4612 Warm up —— (缩点 + 求树的直径)
题意:一个无向图,问建立一条新边以后桥的最小数量. 分析:缩点以后,找出新图的树的直径,将这两点连接即可. 但是题目有个note:两点之间可能有重边!而用普通的vector保存边的话,用v!=fa的话 ...
- nvm临时版本和永久版本
nvm use 8.15.1//临时版本 nvm alias default 8.15.1//永久版本
- 求两个排序数组的交集和并集----时间复杂度O(n+m)
问题: 给你两个排序的数组,求两个数组的交集. 比如: A = 1 3 4 5 7, B = 2 3 5 8 9, 那么交集就是 3 5,n是a数组大小,m是b数组大小. 思路: (1)从b数组遍历取 ...
- 利用JDK自带工具监控JVMCPU和内存指标
特别提示:本人博客部分有参考网络其他博客,但均是本人亲手编写过并验证通过.如发现博客有错误,请及时提出以免误导其他人,谢谢!欢迎转载,但记得标明文章出处:http://www.cnblogs.com/ ...
- react-native(ios)简单配置环境(mac)
1.首先全局安装react-native-cli npm install -g react-native-cli 2.安装xcode(appStore) 3.打开xcode,检查一下是否装有某个版本的 ...

