深入理解系统调用

作业要求:

  • 找一个系统调用,系统调用号为学号最后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。

原文地址:https://www.cnblogs.com/gmz-ustc/p/12971070.html