Linux操作系统分析 | 深入理解系统调用

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. 函数原型

1
2
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#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\n\t"    //listen函数的第二个参数
            "movl %1,%%edi\n\t"      //listen函数的第一个参数
            "movl $0x32,%%eax\n\t"   //将系统调用号50存入eax寄存器
            "syscall\n\t"
            "movq %%rax,%0\n\t"
            :"=m"(listen_result)
            :"g"(sockfd)
        );
        if(listen_result == 0)
        {
            printf("listen is being called\n");
        }
    if(listen_result ==-1)
    {
        perror("listen");
        exit(1);
    }
 
    //等待连接
    while(1)
    {
        sin_size = sizeof(struct sockaddr_in);
 
        printf("server is run.\n");
        //如果建立连接,将产生一个全新的套接字
        if((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,&sin_size))==-1)
        {
            perror("accept");
            exit(1);
        }
        printf("accept success.\n");
        //生成一个子进程来完成和客户端的会话,父进程继续监听
        if(!fork())
        {
            printf("create new thred success.\n");
            //读取客户端发来的信息
            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() 函数的调用采用了内嵌汇编指令的形式,即:

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

 asm volatile 内联汇编格式

            asm volatile(

                 "Instruction List"

                 : Output

                 : Input

                 : Clobber/Modify

              );

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

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

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

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

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

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

    "=a"(initval)

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

    "constraint(variable)"

 

 

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

1
listen_result = listen(sockfd,10);

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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;<br>
    //建立一个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. 代码如下:

1
2
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,表明服务器端启动

代码如下:

1
sudo  ./server

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

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

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

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

代码如下:

1
sudo ./client

客户端启动,结果如下:

 

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

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

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

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

 

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

1、环境配置

(1)安装开发工具

1
2
3
4
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)下载内核源代码

 

1
2
3
4
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)配置内核选项

 

1
2
3
4
5
6
7
8
9
10
11
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)编译内核

 

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

 

(5)启动qemu

 

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

 

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

a. 下载解压:

1
2
3
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. 配置编译、安装:

 

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

 

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

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

 

1
2
3
4
5
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文件

1
2
3
4
5
6
7
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome Liu JianingOS!"
echo "--------------------"
cd home
/bin/sh

    给init脚本添加可执行权限

 

1
chmod +x init

 

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

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

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

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

1
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

 

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

 

  

 

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

 

1
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

 

1
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 目录下,加载内核镜像:

 

1
gdb vmlinux

 

  

 

 

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

 

1
(gdb) target remote:1234

 

  

 

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

方法:

1
(gdb) b 系统调用函数名

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

代码如下:

1
(gdb) b __x64_sys_listen

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

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

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

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

/home # ls
/home # ./server

此时第二个终端内容为:

在第二个终端中输入:

 

1
(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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值