实验要求:
找一个系统调用,系统调用号为学号最后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脚本被执行。
2 跟踪调试 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