Socket与系统调用深度分析
Posted liwj57csseblog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。
本次实验是在X86 64环境下Ubuntu18.04.3以及Linux5.0以上的内核中进行。实验将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析
实验原理:
Socket API编程接口之上可以编写基于不同网络协议的应用程序;
Socket接口在用户态通过系统调用机制进入内核;
内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
Socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法
在实验和分析之前先了解一下操作系统中的相关概念
中断:
在计算机科学中,中断指计算机CPU获知某些事,暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后再继续执行之前的程序。整个过程称为中断处理,简称中断,而引起这一过程的事件称为中断事件。中断是计算机实现并发执行的关键,也是操作系统工作的根本。
中断按事件来源分类,可以分为外部中断和内部中断。中断事件来自于CPU外部的被称为外部中断,来自于CPU内部的则为内部中断。
进一步细分,外部中断还可分为可屏蔽中断(maskable interrupt)和不可屏蔽中断(non-maskable interrupt)两种,而内部中断按事件是否正常来划分可分为软中断和异常两种。
外部中断的中断事件来源于CPU外部,必然是某个硬件产生的,所以外部中断又被称为硬件中断(hardware interrupt)。计算机的外部设备,如网卡、声卡、显卡等都能产生中断。外部设备的中断信号是通过两根信号线通知CPU的,一根是INTR,另一根是NMI。CPU从INTR收到的中断信号都是不影响系统运行的,CPU可以选择屏蔽(通过设置中断屏蔽寄存器中的IF位),而从NMI中收到的中断信号则是影响系统运行的严重错误,不可屏蔽,因为屏蔽的意义不大,系统已经无法运行。
内部中断来自于处理器内部,其中软中断是由软件主动发起的中断,常被用于系统调用(system call);而异常则是指令执行期间CPU内部产生的错误引起的。异常也和不可屏蔽中断一样不受eflags寄存器的IF位影响,区别在于不可屏蔽中断发生的事件会导致处理器无法运行(如断电、电源故障等),而异常则是影响系统正常运行的中断(如除0、越界访问等)。
中断处理流程:
系统调用:
在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:
系统态(也称为管态或核心态),操作系统在系统态运行
用户态(也称为目态),应用程序只能在用户态运行。
在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令两类。
特权指令——在系统态时运行的指令
对内存空间的访问范围基本不受限制,不仅能访问用户存储空间,也能访问系统存储空间,
特权指令只允许操作系统使用,不允许应用程序使用,否则会引起系统混乱。
非特权指令——在用户态时运行的指令
一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间。
内核中将系统调用作为一个特殊的中断来处理。系统调用是通过中断机制实现的,并且一个操作系统的所有系统调用都通过同一个中断入口来实现。在Unix/Linux系统中,系统调用像普通C函数调用那样出现在C程序中。但是一般的函数调用序列并不能把进程的状态从用户态变为核心态,而系统调用却可以做到。C语言编译程序利用一个预先确定的函数库(一般称为C库),其中有各系统调用的名字。C库中的函数都专门使用一条指令,把进程的运行状态改为核心态。Linux的系统调用是通过中断指令“INT 0x80”实现的。每个系统调用都有惟一的号码,称作系统调用号。所有的系统调用都集中在系统调用入口表中统一管理。系统调用入口表是一个函数指针数组,以系统调用号为下标在该数组中找到相应的函数指针,进而就能确定用户使用的是哪一个系统调用。不同系统中系统调用的个数是不同的,目前Linux系统中共定义了221个系统调用。另外,系统调用表中还留有一些余项,可供用户自行添加。当CPU执行到中断指令“INT 0x80”时,硬件就做出一系列响应,其动作与上述的中断响应相同。CPU穿过陷阱门,从用户空间进入系统空间。相应地,进程的上下文从用户堆栈切换到系统堆栈。接着运行内核函数system_call()。首先,进一步保存各寄存器的内容;接着调用syscall_trace( ),以系统调用号为下标检索系统调用入口表sys_call_table,从中找到相应的函数;然后转去执行该函数,完成具体的服务。执行完服务程序,核心检查是否发生错误,并作相应处理。如果本进程收到信号,则对信号作相应处理。最后进程从系统空间返回到用户空间。
Linux 通过软中断实现从用户态到内核态的切换。用户态和核心态是独立的执行流,因此在切换时,需要准备执行栈并保存寄存器 。
内核实现了很多不同的系统调用(提供不同功能),而系统调用处理函数只有一个。因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器来传递。
执行态切换过程:
应用程序在用户态准备好调用参数,执行 int 指令触发软中断 ,中断号为 0x80 ;
CPU 被软中断打断后,执行对应的中断处理函数 ,这时便已进入内核态 ;
系统调用处理函数准备内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);
系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;
系统调用处理函数准备返回值并从内核栈中恢复 寄存器 ;
系统调用处理函数执行 ret 指令切换回用户态 ;
Linux使用strace跟踪进程:
strace常用来跟踪进程执行时的系统调用和所接收的信号。 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。
strace命令参数详解:
-c 统计每一系统调用的所执行的时间,次数和出错的次数等. -d 输出strace关于标准错误的调试信息. -f 跟踪由fork调用所产生的子进程. -ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号. -F 尝试跟踪vfork调用.在-f时,vfork不被跟踪. -h 输出简要的帮助信息. -i 输出系统调用的入口指针. -q 禁止输出关于脱离的消息. -r 打印出相对时间关于,,每一个系统调用. -t 在输出中的每一行前加上时间信息. -tt 在输出中的每一行前加上时间信息,微秒级. -ttt 微秒级输出,以秒了表示时间. -T 显示每一调用所耗的时间. -v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出. -V 输出strace的版本信息. -x 以十六进制形式输出非标准字符串 -xx 所有字符串以十六进制形式输出. -a column 设置返回值的输出位置.默认 为40. -e expr 指定一个表达式,用来控制如何跟踪.格式如下: [qualifier=][!]value1[,value2]... qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如: -eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none. 注意有些shell使用!来执行历史记录里的命令,所以要使用. -e trace=set 只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all. -e trace=file 只跟踪有关文件操作的系统调用. -e trace=process 只跟踪有关进程控制的系统调用. -e trace=network 跟踪与网络有关的所有系统调用. -e strace=signal 跟踪所有与系统信号有关的 系统调用 -e trace=ipc 跟踪所有与进程通讯有关的系统调用 -e abbrev=set 设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all. -e raw=set 将指 定的系统调用的参数以十六进制显示. -e signal=set 指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号. -e read=set 输出从指定文件中读出 的数据.例如: -e read=3,5 -e write=set 输出写入到指定文件中的数据. -o filename 将strace的输出写入文件filename -p pid 跟踪指定的进程pid. -s strsize 指定输出的字符串的最大长度.默认为32.文件名一直全部输出. -u username 以username 的UID和GID执行被跟踪的命令
strace命令实例:
首先使用pidof firefox命令查询Linux的火狐浏览器的进程号。然后使用
strace -tt -T -v -f -e trace=network -o ./firefoxlog.txt -p 3193 -p 3167 -p 3125 -p 3076
命令跟踪进程并生成日志。
Linux的GDB调试:
gdb(GNU symbolic debugger)是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。
一般来说,GDB主要帮助你完成下面四个方面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。
gdb命令参数详解:
start #开始调试,停在第一行代码处,(gdb)start l #list的缩写查看源代码,(gdb)l b <lines> #b: Breakpoint的简写,设置断点。(gdb) b 8 b <func> #b: Breakpoint的简写,设置断点。(gdb) b main i breakpoints #i:info 的简写。(gdb)i breakpoints d [bpNO] #d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。(gdb)d 1 ? s #s: step执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;(gdb) s n #n: next执行一行源程序代码,此行代码中的函数调用也一并执行。(gdb) n ? r #Run的简写,运行被调试的程序。如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。(gdb) r c #Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。(gdb) c finish #函数结束 ? p [var] #Print的简写,显示指定变量(临时变量或全局变量 例如 int a)的值。(gdb) p a display [var] #display,设置想要跟踪的变量(例如 int a)。(gdb) display a undisplay [varnum] #undisplay取消对变量的跟踪,被跟踪变量用整型数标识。(gdb) undisplay 1 set args #可指定运行时参数。(gdb)set args 10 20 show args #查看运行时参数。 q #Quit的简写,退出GDB调试环境。(gdb) q help [cmd] #GDB帮助命令,提供对GDB名种命令的解释说明。如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。(gdb)help 回车 #重复前面的命令,(gdb)回车
实验过程:
本次实验使用gdb调试跟踪Linux的socket API下的bind和listen函数。具体步骤如下:
打开Ubuntu虚拟机后,进入上次实验的装有MenuOS内核的文件夹,在实验之前,首先要修改上次的menu文件夹下的Makefile文件,将上次添加的代码后面的-S去掉
然后进入menu文件,打开menuOS
make rootfs
此时切不可关闭该终端和QEMU,返回到目录../linux-5.0.1下,打开另一个终端,输入如下命令:
gdb file ./vmlinux target remote:1234 break __sys_bind break __sys_listen
此时,gdb已经给__sys_bind和__sys_listen两个Socket系统调用设定了断点,gdb响应如下:
然后开始对MenuOS的运行:
c #在gdb终端输入 replyhi #在QEMU中输入
hello#在QEMU中输入
根据gdb给我们函数定义地址的信息,在~/linux-5.0.1/net/socket.c中找到了相应的函数定义
bind函数:
bind函数定义:
/*
* Bind a name to a socket. Nothing much to do here since it‘s
* the protocol‘s responsibility to handle the local address.
*
* We move the socket address to kernel space before we call
* the protocol layer (having also checked the address is ok).
*/
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
/*
* 以fd为索引从当前进程的文件描述符表中
* 找到对应的file实例,然后从file实例的private_data中
* 获取socket实例。
*/
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
/*
* 将用户空间的地址拷贝到内核空间的缓冲区中。
*/
err = move_addr_to_kernel(umyaddr, addrlen, (struct sockaddr *)&address);
if (err >= 0) {
/*
* SELinux相关,不需要关心。
*/
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
/*
* 如果是TCP套接字,sock->ops指向的是inet_stream_ops,
* sock->ops是在inet_create()函数中初始化,所以bind接口
* 调用的是inet_bind()函数。
*/
if (!err)
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err
}
这个函数的功能是将一个进程的名字与一个socket绑定,在检查这个socket是正确的之后,将这个socket地址转入内核态去处理。
listen函数:
listen函数定义:
/* * Perform a listen. Basically, we allow the protocol to do anything * necessary for a listen, and if that works, we mark the socket as * ready for listening. */ int __sys_listen(int fd, int backlog) { struct socket *sock; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { /* * sysctl_somaxconn存储的是服务器监听时,允许每个套接字连接队列长度 * 的最大值,默认值是128 */ somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; /* * 如果指定的最大连接数超过系统限制,则使用系统当前允许的连接队列 * 中连接的最大数。 */ if ((unsigned int)backlog > somaxconn) backlog = somaxconn; err = security_socket_listen(sock, backlog); if (!err) /* * 从这里开始,socket以后所用的函数将根据TCP/UDP而视协议而定 */ err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed); } return err; } SYSCALL_DEFINE2(listen, int, fd, int, backlog) { return __sys_listen(fd, backlog); }
listen函数的作用就是将socket端口设为监听状态,是否有通信请求。
实验总结:
通过本次实验,我学会了如何使用strace跟踪特定进程,研究其中的内核子程序。学会使用gdb调试代码,跟进一步理解系统调用的原理,以及用户态和核心态之间的切换流程。对Linux内核有了更加深刻的了解。同时在实验中对Linux Socket API也有了一定的了解,尤其对bind函数和listen函数有了更深刻的认识。总而言之,这次实验让我受益匪浅。
以上是关于Socket与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章