zoukankan      html  css  js  c++  java
  • Linux操作系统分析 | 深入理解系统调用

    实验要求

    1、找一个系统调用,系统调用号为学号最后2位相同的系统调用

    2、通过汇编指令触发该系统调用

    3、通过gdb跟踪该系统调用的内核处理过程

    4、重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化


    实验环境及配置

    VMware® Workstation 15 Pro

    Ubuntu 16.04.3 LTS

    64位操作系统

    一、基本理论

    1、Linux 的系统调用

    当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行 system_call (entry_INT80_32 或 entry_SYSCALL_64)  汇编代码,其中根据系统调用号调用对应的内核处理函数。

    具体来说,进入内核后,开始执行对应的中断服务程序 entry_INT80_32 或者 entry_SYSCALL_64。

    2、触发系统调用的方法

    (1)使用C库函数触发系统调用

    以time系统调用为例:

    (2)使用 int &0x80 或者 syscall 汇编代码触发系统调用

    以time系统调用为例。

    32位系统:

    64位系统:

    二、通过汇编指令触发一个系统调用

    1、选择一个系统调用

    (1)步骤:

     Linux源代码中的 syscall_32.tbl 和 syscall_64.tbl 分别定义了 32位x86 和 64位x86-64的系统调用内核处理函数。

    由于我的 Linux 系统是64位的,所以进入Linux源代码中:

    ~/arch/x86/entry/syscalls/syscall_64.tbl
    

    可以查看系统调用表,如下图所示:

    我的学号最后两位为50,所以选择 50号 系统调用。

    (2)listen 函数

    a. 作用

    listen 函数用于监听来自客户端的 tcp socket 的连接请求,一般在调用 bind 函数之后、调用 accept 函数之前调用 listen 函数。

    b. 函数原型

    #include <sys/socket.h>
    int listen(int sockfd, int backlog)
    

    参数 sockfd:被 listen 函数作用的套接字

    参数 backlog:侦听队列的长度

    返回值:

    成功 失败 错误信息
    0 -1

    EADDRINUSE:另一个socket 也在监听同一个端口

    EBADF:参数sockfd为非法的文件描述符。

    ENOTSOCK:参数sockfd不是文件描述符。

    EOPNOTSUPP:套接字类型不支持listen操作

    2、通过汇编指令触发系统调用

    (1)新建服务器端程序:server.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    int main()
    {
    	int sockfd,new_fd,listen_result;
    	struct sockaddr_in my_addr;
    	struct sockaddr_in their_addr;
    	int sin_size;
    	//建立TCP套接口
    	if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
    	{
    		printf("create socket error");
    		perror("socket");
    		exit(1);
    	}
    	//初始化结构体,并绑定2323端口
    	my_addr.sin_family = AF_INET;
    	my_addr.sin_port = htons(2328);
    	my_addr.sin_addr.s_addr = INADDR_ANY;
    	bzero(&(my_addr.sin_zero),8);
    	//绑定套接口
    	if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))==-1)
    	{
    		perror("bind socket error");
    		exit(1);
    	}
    	//创建监听套接口, 监听队列长度为10
            //listen_result = listen(sockfd,10);
            asm volatile(
                "movl $0xa,%%edi
    	"    //listen函数的第二个参数
                "movl %1,%%edi
    	"      //listen函数的第一个参数
                "movl $0x32,%%eax
    	"   //将系统调用号50存入eax寄存器
                "syscall
    	"
                "movq %%rax,%0
    	"
                :"=m"(listen_result)
                :"g"(sockfd)
            );
            if(listen_result == 0)
            {
                printf("listen is being called
    ");
            }
    	if(listen_result ==-1)
    	{
    		perror("listen");
    		exit(1);
    	}
    
    	//等待连接
    	while(1)
    	{
    		sin_size = sizeof(struct sockaddr_in);
    
    		printf("server is run.
    ");
    		//如果建立连接,将产生一个全新的套接字
    		if((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,&sin_size))==-1)
    		{
    			perror("accept");
    			exit(1);
    		}
    		printf("accept success.
    ");
    		//生成一个子进程来完成和客户端的会话,父进程继续监听
    		if(!fork())
    		{
    			printf("create new thred success.
    ");
    			//读取客户端发来的信息
    			int numbytes;
    			char buff[256];
    			memset(buff,0,256);
    			if((numbytes = recv(new_fd,buff,sizeof(buff),0))==-1)
    			{
    			perror("recv");
    			exit(1);
    			}
    			printf("%s",buff);
    			//将从客户端接收到的信息再发回客户端
    			if(send(new_fd,buff,strlen(buff),0)==-1)
    				perror("send");
    			close(new_fd);
    			exit(0);
    		}
    		close(new_fd);
    	}
    	close(sockfd);
    }
    

    其中对 listen() 函数的调用采用了内嵌汇编指令的形式,即:

            asm volatile(
                "movl $0xa,%%edi
    	"    //listen函数的第二个参数
                "movl %1,%%edi
    	"      //listen函数的第一个参数
                "movl $0x32,%%eax
    	"   //将系统调用号50存入eax寄存器
                "syscall
    	"
                "movq %%rax,%0
    	"
                :"=m"(listen_result)
                :"g"(sockfd)
            );
    

     asm volatile 内联汇编格式

                asm volatile(

                     "Instruction List"

                     : Output

                     : Input

                     : Clobber/Modify

                  );

    a. asm 用来声明一个内联汇编表达式,任何内联汇编表达式都是以它开头,必不可少。

    b. volatile 是可选的,如果选用,则向GCC声明不对该内联汇编进行优化。

    c. Instruction List 是汇编指令序列,如果有多条指令时:

        可以将多条指令放在一队引号中,用 ; 或者 将它们分开;

        也可以一条指令放在一对引号中,每条指令一行。

    d. Output 用来指定内联汇编语句的输出,相当于系统函数的返回值,格式为:

        "=a"(initval)

    e. Input 用来指定当前内联汇编语句的输入,相当于系统函数的参数(当该参数为使用C语言的变量的值时,采用这种方法),格式为:

        "constraint(variable)"

    可以看到,如果使用库函数触发函数调用的话,应该是被注释掉的语句:

            listen_result = listen(sockfd,10);
    

    该函数有两个参数,分别是变量 sockfd 和 常量10,返回值为 listen_result,按照上述规定完成汇编指令触发系统调用。

    (2)新建客户端程序:client.c

    #include <stdio.h>
    #include <stdlib.h>
    
    #include <string.h>
    #include <netdb.h>
    #include <sys/types.h>
    
    #include <sys/socket.h>
    
    int main(int argc,char *argv[])
    {
    
    	int sockfd,numbytes;
     	char buf[100];
    
    	struct sockaddr_in their_addr;
    //建立一个TCP套接口 if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); printf("create socket error.建立一个TCP套接口失败"); exit(1); } //初始化结构体,连接到服务器的2323端口 their_addr.sin_family = AF_INET; their_addr.sin_port = htons(2328); // their_addr.sin_addr = *((struct in_addr *)he->h_addr); inet_aton( "127.0.0.1", &their_addr.sin_addr ); bzero(&(their_addr.sin_zero),8); //和服务器建立连接 if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))==-1) { perror("connect"); exit(1); } //向服务器发送数据 if(send(sockfd,"hello!socket.",6,0)==-1) { perror("send"); exit(1); } //接受从服务器返回的信息 if((numbytes = recv(sockfd,buf,100,0))==-1) { perror("recv"); exit(1); } buf[numbytes] = '/0'; printf("Recive from server:%s",buf); //关闭socket close(sockfd); return 0; }

    (3)对两个程序分别编译、链接

    a. 代码如下:

    gcc -o server server.c -static
    gcc -o client client.c  -static
    

    格式:gcc -o file file.c

    将文件 file.c 编译成可执行文件 file

    参数 -static:强制使用静态库链接

    参数 -m32:在64位机器上输出32位代码时,需要加上 -32

    b. 结果如下:

    执行代码前:

    可以看出文件夹中目前只有 server.c 和 client.c。

    执行代码后:

    发现文件夹中已经生成了我们想要的可执行文件 server 和 client。

    (4)执行可执行文件

    a. 启动 server,表明服务器端启动

    代码如下:

    sudo  ./server
    

    服务器端启动,结果如下:

    可以看到输出 “listen is being called”,表明我们想要调用的系统函数 listen() 已经被成功触发,即系统调用成功。

    此时服务器端就等待客户端与其建立链接并通信。

    b. 再启动一个终端充当客户端,在该终端中启动 client,表明客户端启动

    代码如下:

    sudo ./client
    

    客户端启动,结果如下:

    可以看到客户端的终端输出 ”Recive from server:hello!0",表明客户端与服务器端已成功建立连接,并且客户端收到了服务器端发回的信息。

    c. 此时,服务器端的信息为:

    服务器端继续 listen 来自客户端的信息。

    如果我们再在另外一个终端内使用 sudo ./client 启动一个客户端,服务器端也会有相应启动成功的信息生成:

    三、通过gdb跟踪该系统调用的内核处理过程

    1、环境配置

    (1)安装开发工具

    sudo apt install build-essential
    sudo apt install qemu # install QEMU
    sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
    sudo apt install axel
    

    以上工具在第一次实验时已经进行了安装。

    (2)下载内核源代码

    axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
    xz -d linux-5.4.34.tar.xz
    tar -xvf linux-5.4.34.tar
    cd linux-5.4.34
    

    (3)配置内核选项

    make defconfig # Default configuration is based on 'x86_64_defconfig'
    make menuconfig
    # 打开debug相关选项
    Kernel hacking --->
        Compile-time checks and compiler options --->
            [*] Compile the kernel with debug info
            [*] Provide GDB scripts for kernel debugging
        [*] Kernel debugging
    # 关闭KASLR,否则会导致打断点失败
    Processor type and features ---->
        [] Randomize the address of the kernel image (KASLR)
    

    (4)编译内核

    make -j$(nproc) # nproc gives the number of CPU cores/threads available
    

    (5)启动qemu

    #测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统最终会kernel panic
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage
    

    (6)制作内存根文件系统

    a. 下载解压:

    axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
    tar -jxvf busybox-1.31.1.tar.bz2
    cd busybox-1.31.1
    

    b. 配置编译、安装:

    make menuconfig
    #记得要编译成静态链接,不⽤动态链接库。
    Settings --->
        [*] Build static binary (no shared libs)
    #然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。
    make -j$(nproc) && make install
    

    c. 制作内存根文件系统镜像:

    在 linux-5.4.34 目录下创建 rootfs 文件夹

    mkdir rootfs
    cd rootfs
    cp ../busybox-1.31.1/_install/* ./ -rf
    mkdir dev proc sys home
    sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
    

    d. 准备 init 脚本文件放在根文件系统根目录下(rootfs/init):

    新建名为 init 的文档文件,添加如下内容到init文件

    #!/bin/sh
    mount -t proc none /proc
    mount -t sysfs none /sys
    echo "Wellcome Liu JianingOS!"
    echo "--------------------"
    cd home
    /bin/sh
    

        给init脚本添加可执行权限

    chmod +x init
    

    e. 打包成内存根文件系统镜像

    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
    

    f. 测试挂在根文件系统,看内核启动完成后是否执行 init 脚本

    返回到 linux-5.4.34目录下,启动qemu

    qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz
    

    结果如下:

    说明 init 脚本被执行。

    2、跟踪调试 Linux 内核

    (1)根据第二部分的内容编写利用汇编指令触发系统调用的代码

    在 rootfs/home 目录下分别创建两个名为 server.c 和 client.c 的文件,并存入第二部分相应的代码。

    (2)使用 gcc 编译成可执行文件 server 和 client

    gcc -o server server.c -static
    gcc -o client client.c -static
    

      

    (3)重新打包内存根文件系统镜像

    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
    

    (4)使用 gdb 跟踪调试

    方法:

    使用 gdb 跟踪调试内核时,在启动 qemu 命令上添加两个参数:

    a. -s

    作用:

    • 在TCP 1234 端口上创建了一个 gdb-server(如果不想使用1234端口,可以用 -gdb tcp:xxxx 来替代 -s 选项)
    • 打开另外一个窗口,用 gdb 把带符号表的内核镜像 vmlinux 加载进来
    • 然后连接 gdb server,设置断点跟踪内核

    b. -S

    作用:

    • 表示启动时暂停虚拟机,等待 gdb 执行 continue 指令(可以简写为c)。

    步骤:

    a. 使用纯命令行启动 qemu

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

    用该命令启动qemu,可以看到虚拟机一启动就暂停了,终端停留在下面的界面:

      

    参数:-nographic -append "console=ttyS0" 

    启动时不会弹出 qemu 虚拟机窗口,可以在纯命令行下启动虚拟机。

    【可以通过 killall qemu-system-x86_64 命令强制关闭虚拟机】

    b. 在打开一个终端窗口,进入 linux-5.4.34 目录下,加载内核镜像:

    gdb vmlinux
    

      

    c. 连接 gdb server,即在 gdb 中运行下方代码:

    (gdb) target remote:1234
    

      

    d. 给文章中使用的系统调用设置断点

    方法:

    (gdb) b 系统调用函数名
    

    上文可知,我选择的系统调用函数为 listen(),具体信息如下:

    代码如下:

    (gdb) b __x64_sys_listen
    

    e. 输入 (gdb) c 指令继续运行程序

    此时,第一个打开的终端的内容为:

    f. 运行编译好的可执行代码 server,使用 gdb 进行单步调试

    在第一个终端中输入如下代码:

    /home # ls
    /home # ./server

    此时第二个终端内容为:

    在第二个终端中输入:

    (gdb) n
    

    结果为: 

     

    报错:

    GDB 远程调试错误:Remote 'g' packet reply is too long

    解决方法:

    重新下载 gdb,并修改其中 remote.c 文件内容

    由 http://ftp.gnu.org/gnu/gdb/ 下载 gdb的较新版本,此处我下载的是 gdb-7.8.tar.gz,并将其放在了 /home/linux 目录下

    进入 /home/linux 目录下,对该文件进行解压缩

    tar zxvf gdb-7.8.tar.gz

    修改 gdb-7.8/gdb 目录下的 remote.c 文件内容:

     

    将如上图所以的两行原有代码注释掉,然后添加如下的代码:
    if (buf_len > 2 * rsa->sizeof_g_packet) {  
          rsa->sizeof_g_packet = buf_len ;  
          for (i = 0; i < gdbarch_num_regs (gdbarch); i++)  
          {  
             if (rsa->regs->pnum == -1)  
             continue;  
      
             if (rsa->regs->offset >= rsa->sizeof_g_packet)  
             rsa->regs->in_g_packet = 0;  
             else  
             rsa->regs->in_g_packet = 1;  
          }  
    }  

    在 gdb-7.8 目录下执行以下命令安装 gdb:

    ./configure
    make
    make install

    至此,我们再重复上述步骤就可以使用 gdb 对程序设置断点,并且进行单步调试。

    (5)使用 gdb 对程序进行单步调试

    gdb操作指令:

    (gdb) l       查看代码情况
    (gdb) n      单步执行
    (gdb) step  进入函数内部
    (gdb) bt     查看堆栈

    重新安装并调整 gdb 之后,按照步骤(4)中的 a - f 依次执行。

    a. 当第一个终端运行可执行文件server之后,即:

    /home # ./server

    第二个终端内容为:

    可以看出断点位置。

    b. 查看堆栈信息

    在第二个终端中输入命令:

    (gdb) bt

    查看当前堆栈信息,如下所示:

    c. 单步调试

    在第二个终端输入如下命令,进行单步调试:

    (gdb) n

    结果如下:

    四、分析总结

    1、使用 (gdb) bt 查看当前堆栈情况

    根据结果显示,函数调用可以分为4层:

    顶层: __x64_sys_listen       作用:开放给用户态使用的系统调用函数接口

    第二层:do_syscall_64       作用:获取系统调用号,从而调用系统函数

    第三层:entry_syscall_64   作用:保存现场工作,调用第二层的 do_syscall_64

    第四层:操作系统

    2、根据单步调试结果从顶层往下依次查看

    (1)断点定位

     断点定位为:

    /home/linux/linux-5.4.34/net/socket.c 1688

    执行以下代码,前往相应位置查看:

    cd linux/linux-5.4.34/net
    cat -n socket.c

    结果为:

     进入  __sys_listen(fd, backlog) 函数查看:

    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) {
            somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
            if ((unsigned int)backlog > somaxconn)
                backlog = somaxconn;
    
            err = security_socket_listen(sock, backlog);
            if (!err)
                err = sock->ops->listen(sock, backlog);
    
            fput_light(sock->file, fput_needed);
        }
        return err;
    }

    (2)执行 do_syscall_64 函数

     该函数定位在:

    /home/linux/linux-5.4.34/arch/x86/entry/common.c 的第300行

     (3)执行 entry_SYSCALL_64 函数

     该函数定位在:

    /home/linux/linux-5.4.34/arch/x86/entry/entry_64.S 的第184行

    3、系统调用总结

    (1)用户态的程序代码 server.c 中的内嵌汇编指令 syscall 触发系统调用

     (2)通过 MSR 寄存器找到函数入口

    中断函数入口为:

    /home/linux/linux/-5.4.34/arch/entry/entry_64.S 第145行 ENTRY(entry_SYSCALL_64) 函数,这个函数为 x86_64 系统进行系统调用的通用入口。

    ENTRY函数如下:

     a. swapgs

    使用 swapgs 指令和 下面一系列的压栈动作来保存现场。

    b. call do_syscall_64

    调用 do_syscall_64 查找系统调用表,获得所要使用的系统调用号。

    (3)跳转执行 do_syscall_64

    跳转到 /home/linux/linux-5.4.34/x86/entry/common.c 下的 do_system_64函数

     

    a. regs->ax = sys_call_table[nr](regs)

    从系统调用表中获得系统调用号,并将其存在到 ax 寄存器中,然后去执行系统调用函数。

    b. syscall_return_slowpath(regs)

    用于系统调用函数执行结束后,恢复现场

    (4)跳转执行系统系统函数 listen

     

    跳转到 /home/linux/linux-5.4.34/net/socket.c 函数,开始执行函数;

    (5)恢复现场

    函数执行完成后,需要进行现场恢复,因此再次回到:

    /home/linux/linux/-5.4.34/arch/x86/entry/entry_64.S 

    进行现场的恢复。

    至此,整个系统调用完成。

     

    参考文章:

    https://blog.csdn.net/u013920085/article/details/20574249

    https://blog.csdn.net/yangbodong22011/article/details/60399728

    https://blog.csdn.net/barry283049/article/details/42970739

  • 相关阅读:
    try catch finally中return的执行顺序
    多线程和同步
    orecle常用函数
    java如何调用接口 2
    orecle 函数
    ==和equals在比较字符串时候的区别
    orecle触发器
    java实现同步的方法
    java如何调用接口
    SMM+maven下的log4j配置打印sql
  • 原文地址:https://www.cnblogs.com/liujianing0421/p/12971722.html
Copyright © 2011-2022 走看看