Linux实验二:深入理解系统调用

实验要求:

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

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

1 环境配置

1.1安装开发工具

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

1.2 下载内核源代码

1 sudo apt install axel
2 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
3 xz -d linux-5.4.34.tar.xz
4 tar -xvf linux-5.4.34.tar
5 cd linux-5.4.34

1.3 配置内核选项 

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

      

1.4 编译和运行内核

1 make -j$(nproc) # nproc gives the number of CPU cores/threadsavailable //编译内核
2 # 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统最终会kernelpanic
3 qemu-system-x86_64 -kernel arch/x86/boot/bzImage //启动qemu

1.5 制作根文件系统

 

电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具, bootloader加载根⽂件系统到内存中,内核会将其挂载到根目录下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统提供基本的⽤户态可执⾏程序。

下载并解压安装包:

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

编译:

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

制作内存根文件系统镜像(rootfs文件夹放在linux-5.4.34文件夹中):

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

rootfs文件夹中新建init文件,添加如下内容到init⽂件 :

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

给init脚本添加可执行权限:

1 chmod +x init

打包内存根文件系统镜像,打包文件存放在linux-5.4.34文件下

1 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz#注意路径

测试看内核启动后是否执行 init 脚本,在 linux-5.4.34目录下,启动qemu: 

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

 看到显示“Welcom Wang LidoOS”,init脚本被执行。

跟踪调试 Linux 内核

2.1 系统调用semctl函数

实验要求找一个系统调用号为学号最后2位相同的系统调用,通过汇编指令触发该系统调用。因此我找66号系统调用。

在linux-5.4.34/arch/x86/entry/syscalls(有32与64位系统调用区别)路径,查到66号系统调用位semctl函数

semctl函数介绍:

在rootfs/home 目录下分别创建seml.c文件,写入semctl函数的源代码,其中将调用semctl函数处的代码改为汇编指令代码:

  1 #include <unistd.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include <stdlib.h>
  6 #include <stdio.h>
  7 #include <string.h>
  8 #include <sys/sem.h>
  9  
 10 union semun
 11 {
 12   int val;
 13   struct semid_ds *buf;
 14   unsigned short *arry;
 15 };
 16  
 17 static int sem_id = 0;
 18 static int set_semvalue();
 19 static void del_semvalue();
 20 static int semaphore_p();
 21 static int semaphore_v();
 22  
 23 int main(int argc, char *argv[])
 24 {
 25     char message = 'X';
 26     int i = 0;
 27  
 28     // 创建信号量
 29     sem_id = semget((key_t) 1234, 1, 0666 | IPC_CREAT);
 30  
 31     if (argc > 1)
 32     {
 33         // 程序第一次被调用,初始化信号量
 34         if (!set_semvalue())
 35         {
 36             fprintf(stderr, "Failed to initialize semaphore
");
 37             exit(EXIT_FAILURE);
 38         }
 39  
 40         // 设置要输出到屏幕中的信息,即其参数的第一个字符
 41         message = argv[1][0];
 42         sleep(2);
 43     }
 44  
 45     for (i = 0; i < 10; ++i)
 46     {
 47         // 进入临界区
 48         if (!semaphore_p())
 49         {
 50             exit(EXIT_FAILURE);
 51         }
 52  
 53         // 向屏幕中输出数据
 54         printf("%c", message);
 55  
 56         // 清理缓冲区,然后休眠随机时间
 57         fflush(stdout);
 58         sleep(rand() % 3);
 59  
 60         // 离开临界区前再一次向屏幕输出数据
 61         printf("%c", message);
 62         fflush(stdout);
 63  
 64         // 离开临界区,休眠随机时间后继续循环
 65         if (!semaphore_v())
 66         {
 67             exit(EXIT_FAILURE);
 68         }
 69         sleep(rand() % 2);
 70     }
 71  
 72     sleep(10);
 73     printf("
%d - finished
", getpid());
 74  
 75     if (argc > 1)
 76     {
 77         // 如果程序是第一次被调用,则在退出前删除信号量
 78         sleep(3);
 79         del_semvalue();
 80     }
 81     exit(EXIT_SUCCESS);
 82 }
 83  
 84 static int set_semvalue()
 85 {
 86     // 用于初始化信号量,在使用信号量前必须这样做
 87     union semun sem_union;
 88  
 89     sem_union.val = 1;
 90     //int res = semctl(sem_id, 0, SETVAL, sem_union);
 91     int res;
 92     asm volatile( 
 93         "mov %4, %%rdi
	" 
 94         "mov %3, %%ecx
	" 
 95         "mov %2, %%ebx
	" 
 96         "mov %1, %%edi
	" 
 97         "movl $0x42, %%eax
	" //传递系统调用号 
 98     "syscall
	" //系统调用 
 99     "movq %%rax, %0
	" // 将函数处理结果返回给 res 变量中
100      :"=m"(res) 
101     :"a"(sem_id), "b"(0), "c"(SETVAL),"d"(sem_union)
102     );
103     if (res == -1)
104     {
105         return 0;
106     }
107     return 1;
108 }
109  
110 static void del_semvalue()
111 {
112     // 删除信号量
113     union semun sem_union;
114  
115     if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
116     {
117         fprintf(stderr, "Failed to delete semaphore
");
118     }
119 }
120  
121 static int semaphore_p()
122 {
123     // 对信号量做减1操作,即等待P(sv)
124     struct sembuf sem_b;
125     sem_b.sem_num = 0;
126     sem_b.sem_op = -1;//P()
127     sem_b.sem_flg = SEM_UNDO;
128     if (semop(sem_id, &sem_b, 1) == -1)
129     {
130         fprintf(stderr, "semaphore_p failed
");
131         return 0;
132     }
133  
134     return 1;
135 }
136  
137 static int semaphore_v()
138 {
139     // 这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
140     struct sembuf sem_b;
141     sem_b.sem_num = 0;
142     sem_b.sem_op = 1; // V()
143     sem_b.sem_flg = SEM_UNDO;
144     if (semop(sem_id, &sem_b, 1) == -1)
145     {
146         fprintf(stderr, "semaphore_v failed
");
147         return 0;
148     }

使用 gcc 编译成可执行文件 seml:

1 gcc -o server seml.c -static

在rootfs文件夹重新打包内存根文件系统镜像:

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

2.2  使用 gdb 跟踪调试

使用纯命令行启动qemu:

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

使⽤gdb跟踪调试内核,加两个参数,⼀个是-s,在TCP 1234端⼝上创建了⼀个gdbserver;

另外打开一个窗⼝,⽤gdb把带有符号表的内核镜像vmlinux加载进来,然后连接gdb server,设置断点跟踪内核。若不想使⽤1234端⼝,可以使⽤-gdb tcp:xxxx来替代-s选项),另⼀个是-S代表启动时暂停虚拟机,等待 gdb 执⾏ continue指令(可以简写为c)。

再打开一个终端窗口, linux-5.4.34 目录下,加载内核镜像:

1 gdb vmlinux

连接 gdb  TCP1234端口:

1 (gdb) target remote :1234

为系统调用设置断点:

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

 输入 c 指令,打开第一个终端窗口显示:

在第一个终端窗口输入:

/home # ./seml 0 & ./seml

 

打开第二个终端窗口:

 

 查看堆栈信息:

1 (gdb) bt

单步调试:

1 (gdb) n

 

 

3  分析

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

3.1根据堆栈结果分析

 

堆栈是自顶向下的,因此:

#0   ksys_ semctl             系统调用函数接口

#1   do_syscall_64           获取系统调用号,调用系统函数

#2   entry_SYSCALL_64  保存现场工作,调用第二层的 do_SYSCALL_64

#3   操作系统

最初我们在semctl函数调用处设置了一个断点,定位于home/linux-5.4.34/ipc/sem.c 文件的1689行

 

 前往文件相应位置查看:

1 cd linux-5.4.34/ipc
2 cat -n sem.c

1689行反回了ksys_semctl函数的调用结果,查看 ksys_semctl(semid,semnum,cmd,arg,IPC_64)函数:

 1 static long ksys_semctl(int semid, int semnum, int cmd, unsigned long arg, int version)
 2 {
 3     struct ipc_namespace *ns;
 4     void __user *p = (void __user *)arg;
 5     struct semid64_ds semid64;
 6     int err;
 7 
 8     if (semid < 0)
 9         return -EINVAL;
10 
11     ns = current->nsproxy->ipc_ns;
12 
13     switch (cmd) {
14     case IPC_INFO:
15     case SEM_INFO:
16         return semctl_info(ns, semid, cmd, p);
17     case IPC_STAT:
18     case SEM_STAT:
19     case SEM_STAT_ANY:
20         err = semctl_stat(ns, semid, cmd, &semid64);
21         if (err < 0)
22             return err;
23         if (copy_semid_to_user(p, &semid64, version))
24             err = -EFAULT;
25         return err;
26     case GETALL:
27     case GETVAL:
28     case GETPID:
29     case GETNCNT:
30     case GETZCNT:
31     case SETALL:
32         return semctl_main(ns, semid, semnum, cmd, p);
33     case SETVAL: {
34         int val;
35 #if defined(CONFIG_64BIT) && defined(__BIG_ENDIAN)
36         /* big-endian 64bit */
37         val = arg >> 32;
38 #else
39         /* 32bit or little-endian 64bit */
40         val = arg;
41 #endif
42         return semctl_setval(ns, semid, semnum, val);
43     }
44     case IPC_SET:
45         if (copy_semid_from_user(&semid64, p, version))
46             return -EFAULT;
47         /* fall through */
48     case IPC_RMID:
49         return semctl_down(ns, semid, cmd, &semid64);
50     default:
51         return -EINVAL;
52     }
53 }

根据使用(gdb)bt查看堆栈调用结果:

#0 调用ksys_semctl函数,定位在ipc/sem.c 文件第1633行,我们查找1633行,会发现就是上述ksys_semctl函数的代码

#1  调用do_syscall_64 函数,定位在arch/x86/entry/common.c 文件290行,我们打开文件查找这一行:

 

#2 entry_SYSCALL_64 函数,定位在arch/x86/entry/entry_64.S 175行

 

 CALL指令执行时,进行两步操作:(1)将程序前执行的位置IP压入堆栈中;(2)转移到调用的do_syscall_64

原文地址:https://www.cnblogs.com/lidodo/p/12976565.html