zoukankan      html  css  js  c++  java
  • 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函数有了更深刻的认识。总而言之,这次实验让我受益匪浅。

  • 相关阅读:
    数组练习
    数组
    表达式和语句
    搜索框制作
    操作DOM
    数据类型
    javascript数组属性及方法
    javascript字符串属性及常用方法总结
    css清除浮动float
    vue项目搭建步骤
  • 原文地址:https://www.cnblogs.com/Liwj57csseblog/p/12066638.html
Copyright © 2011-2022 走看看