• 深入理解系统调用


    作业要求:

    • 找一个系统调用,系统调用号为学号最后2位相同的系统调用
    • 通过汇编指令触发该系统调用
    • 通过gdb跟踪该系统调用的内核处理过程
    • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

    系统调用的存在,有以下重要的意义:

    1)用户程序通过系统调用来使用硬件,而不用关心具体的硬件设备,这样大大简化了用户程序的开发。

    比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上。

    2)系统调用使得用户程序有更好的可移植性。

    只要操作系统提供的系统调用接口相同,用户程序就可在不用修改的情况下,从一个系统迁移到另一个操作系统。

    3)系统调用使得内核能更好的管理用户程序,增强了系统的稳定性。

    因为系统调用是内核实现的,内核通过系统调用来控制开放什么功能及什么权限给用户程序。

    这样可以避免用户程序不正确的使用硬件设备,从而破坏了其他程序。

    4)系统调用有效的分离了用户程序和内核的开发。

    用户程序只需关心系统调用API,通过这些API来开发自己的应用,不用关心API的具体实现。

    内核则只要关心系统调用API的实现,而不必管它们是被如何调用的。

    一、选择系统调用

    本人学号尾数为45,打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,查看要选择进行实验的系统调用。45号系统调用为recvfrom。

     

    recvfrom的功能是从套接字上接收一个消息。对于recvfrom ,可同时应用于面向连接的和无连接的套接字。recv一般只用在面向连接的套接字,几乎等同于recvfrom,只要将recvfrom的第五个参数设置NULL。当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中

    二、环境准备

    首先配置内核选项如下图所示(调试内核必须这样配置):

     

    之后使用busybox制作根文件系统,首先配置busybox使用静态链接

    之后使用命令make -j$(nproc&& make install编译安装busybox。默认安装路径为源码目录的_install下面。

    再到家目录下面新建rootfs文件夹,将_install目录中的所有文件拷贝过去。并且新建几个目录(dev proc sys home等)和文件(dev/console dev/null dev/tty*)。

     准备init脚本⽂件放在根⽂件系统跟⽬录下(rootfs/init),在init⽂件中添加这些内容:

    之后给init脚本添加可执⾏权限:chmod +x init

    打包成内存根⽂件系统镜像:

     

     启动qemu测试内核启动是否执行init

    运行结果如下

     三、汇编改写

    首先服务器运行在虚拟机上,以下为服务器的代码:

    #include <unistd.h>
    #include <string.h>
    #include <stdio.h>
    #include <iostream>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <arpa/inet.h>
    
    #define MAXLINE 4096
    #define UDPPORT 8001
    #define SERVERIP "127.0.0.1"
    
    using namespace std;
    
    int main(){
        int serverfd;
        unsigned int server_addr_length, client_addr_length;
        char recvline[MAXLINE];
        char sendline[MAXLINE];
        struct sockaddr_in serveraddr , clientaddr;
    
        // 使用函数socket(),生成套接字文件描述符;
        if( (serverfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){
            perror("socket() error");
            exit(1);
        }
    
        // 通过struct sockaddr_in 结构设置服务器地址和监听端口;
        bzero(&serveraddr,sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
        serveraddr.sin_port = htons(UDPPORT);
        server_addr_length = sizeof(serveraddr);
    
        // 使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
        if( bind(serverfd, (struct sockaddr *) &serveraddr, server_addr_length) < 0){
            perror("bind() error");
            exit(1);
        }
    
        // 接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
        client_addr_length = sizeof(sockaddr_in);
        int recv_length = 0;
        recv_length = recvfrom(serverfd, recvline, sizeof(recvline), 0, (struct sockaddr *) &clientaddr, &client_addr_length);
        cout << "recv_length = "<< recv_length <<endl;
        cout << recvline << endl;
    
        // 向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
        int send_length = 0;
        sprintf(sendline, "hello client !");
        send_length = sendto(serverfd, sendline, sizeof(sendline), 0, (struct sockaddr *) &clientaddr, client_addr_length);
        if( send_length < 0){
            perror("sendto() error");
            exit(1);
        }
        cout << "send_length = "<< send_length <<endl;
    
        //关闭套接字,使用close() 函数释放资源;
        close(serverfd);
    
        return 0;
    }

    接下来是客户端的汇编调用recvfrom系统调用代码如下:

    #include <unistd.h>
    #include <string.h>
    #include <stdio.h>
    #include <iostream>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <arpa/inet.h>
    
    #define MAXLINE 4096
    #define UDPPORT 8001
    #define SERVERIP "127.0.0.1"
    
    using namespace std;
    
    int main(){
        int confd;
        unsigned int addr_length;
        char recvline[MAXLINE];
        char sendline[MAXLINE];
        struct sockaddr_in serveraddr;
    
        // 使用socket(),生成套接字文件描述符;
        if( (confd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ){
            perror("socket() error");
            exit(1);
        }
    
        //通过struct sockaddr_in 结构设置服务器地址和监听端口;
        bzero(&serveraddr, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_addr.s_addr = inet_addr(SERVERIP);
        serveraddr.sin_port = htons(UDPPORT);
        addr_length = sizeof(serveraddr);
    
        // 向服务器发送数据,sendto() ;
        int send_length = 0;
        sprintf(sendline,"hello server!");
        send_length = sendto(confd, sendline, sizeof(sendline), 0, (struct sockaddr *) &serveraddr, addr_length);
        if(send_length < 0 ){
            perror("sendto() error");
            exit(1);
        }
        cout << "send_length = " << send_length << endl;
    
        // 接收服务器的数据,recvfrom() ;
        int recv_length = 0;
        // recv_length = recvfrom(confd, recvline, sizeof(recvline), 0, (struct sockaddr *) &serveraddr, &addr_length);
    
        asm volatile(
            "movq %1, %%rdi
    	"
            "movq %2, %%rsi
    	"
            "movq %3, %%rdx
    	"
            "movq $0x0, %%rcx
    	" 
            "movq %4, %%r8
    	" 
            "movq %5, %%r9
    	"
            "movl $0x2D,%%eax
    	"   
            "syscall
    	"          
            "movq %%rax,%0
    	"      
            :"=m"(recv_length)
            :"m"(confd), "p"(recvline), "X"(sizeof(recvline)), "p"((struct sockaddr *) &serveraddr), "p"(&addr_length)
        );
    
    
        cout << "recv_length = " << recv_length <<endl;
        cout << recvline << endl;
    
        // 关闭套接字,close() ;
        close(confd);
    
        return 0;
    }

    接下来使用

     使用gcc运行代码如下(因为client运行在内核上 所以使用静态方式):

    g++ server.cpp -o server
    
    g++ client.cpp -o client -static

    运行结果如下:

     四、gdb调试与分析

    重新打包根文件目录,纯命令⾏下启动虚拟机

    qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

    此时虚拟机会暂停在启动界面。在另一个terminal中开启gdb调试 gdb vmlinux,连接进行调试,target remote:1234。结果如下图所示:

    如下图打上断点:

    如下图所示,由于qemu无法使用网络,客户端和服务器端无法进行通信,也就无法进行下一步的操作

    虽然无法继续调试但是可以接着分析下recvfrom函数的调用。在上面的调用图中可以看见该函数位于net/socket.c文件下的第2023行

    __sys_recvfrom函数如下图:

    首先调用import_single_range,在import_single_range中,MAX_RW_COUNT是一个宏:INT_MAX & PAGE_MASK,INT_MAX是2^31,理论上每次write可写的buff大小是2^31-2^12=2147479552,然后使用了迭代器

    int import_single_range(int rw, void __user *buf, size_t len,
             struct iovec *iov, struct iov_iter *i)
    {
        if (len > MAX_RW_COUNT)
            len = MAX_RW_COUNT;
        if (unlikely(!access_ok(!rw, buf, len)))
            return -EFAULT;
    
        iov->iov_base = buf;
        iov->iov_len = len;
        iov_iter_init(i, rw, iov, 1, len);
        return 0;
    }
    EXPORT_SYMBOL(import_single_range);

    接下来用通过sockfd_lookup_light()来通过文件描述符fd来找到我们需要的结构体

    static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)
    {
        struct file *file;
        struct socket *sock;
    
        *err = -EBADF;
        file = fget_light(fd, fput_needed);//根据fd获取file结构体
        if (file) {
            sock = sock_from_file(file, err);//根据file结构体获取socket结构体
            if (sock)
                return sock;
            fput_light(file, *fput_needed);
        }
        return NULL;
    }

    可见,主要分成两个部分,一个是由fd找到file结构体,然后才是由file结构体获取socket结构体。
    先看看是如何从fd找到file结构体的。

    struct file *fget_light(unsigned int fd, int *fput_needed)
    {
        struct file *file;
        struct files_struct *files = current->files;//获取当前进程打开的文件列表
    
        *fput_needed = 0;
        //如果只有一个进程在使用,那就不需要加锁了,锁比较耗性能
        if (atomic_read(&files->count) == 1) {
            file = fcheck_files(files, fd);//根据files_struct结构获取file结构体
            if (file && (file->f_mode & FMODE_PATH))
                file = NULL;
        } else {
            rcu_read_lock();//多个进程使用,需要加锁保护
            file = fcheck_files(files, fd);
            if (file) {
                if (!(file->f_mode & FMODE_PATH) &&
                    atomic_long_inc_not_zero(&file->f_count))
                    *fput_needed = 1;
                else
                    /* Didn't get the reference, someone's freed */
                    file = NULL;
            }
            rcu_read_unlock();
        }
    
        return file;
    }
    
    static inline struct file * fcheck_files(struct files_struct *files, unsigned int fd)
    {
        struct file * file = NULL;
        struct fdtable *fdt = files_fdtable(files);//获得文件描述符位图表
    
        if (fd < fdt->max_fds)
            //根据句柄fd获取file结构体,fdt->fd可以理解为一个数组,以文件句柄fd为索引
            file = rcu_dereference_check_fdtable(files, fdt->fd[fd]);
        return file;
    }

    由此可见,进程结构体task_struct维护了一个files_struct结构体,用于记录当前进程使用的文件情况,这样也便于控制每个进程允许打开的文件个数,但这个就是另外的话题了。files_struct结构体里的fdtable变量里存放了该进程使用的所有文件句柄,并且每个文件句柄关联到了对应的file结构体。因此以fd为索引就能获取file结构体。这个赋值操作是在socket()系统调用做的,通过fd_install()函数完成fd和file结构体的关联。

    最后调用sock_recvmsg函数接收信息,如果无误就使用move_addr_to_user传递给用户,函数结尾的使用调用 fput_light() 更新文件的引用计数。

    五、系统调用栈空间分析  
    在具有函数调用的处理例程当中,各个函数的栈空间关系如下图所示: 

      
    在所有的寄存器中,%rax 通常用于存储函数调用的返回结果,%rsp 是堆栈指针寄存器,它会一直指向栈顶位置,堆栈的pop和push操作就是通过改变%rsp 的值即移动堆栈指针的位置来实现出栈和压栈操作的。%rbp 是栈帧指针,用于标识当前栈帧的起始位置,剩余的%rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数。
    在整个系统调用过程中,调用方主要做的操作有:
    1、先把参数保存在寄存器edi和esi中(通过寄存器传参数)
    2、调用callq,其中callq保存下一条指令的地址,用于函数返回继续执行,之后再跳转到子函数地址。
    3、处理返回值,函数返回值通常存放在%eax。

  • 相关阅读:
    深入理解ThreadLocal
    synchronized与Lock的区别与使用
    1亿个数中找出最小的100个数--最小堆
    B+/-Tree原理(mysql索引数据结构)
    深入理解token
    shiro(java安全框架)
    第一次项目上Linux服务器(四:CentOS6下Mysql数据库的安装与配置(转))
    第一次项目上Linux服务器(三:安装Tomcat及相关命令)
    第一次项目上Linux服务器(二:——安装jdk)
    第一次项目上Linux服务器(一:远程连接服务器)
  • 原文地址:https://www.cnblogs.com/gmz-ustc/p/12971070.html
Copyright © 2020-2023  润新知