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与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket系统调用Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析