深入理解系统调用
作业要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
实验步骤
一、环境准备
1、安装开发环境
sudo apt install build-essential sudo apt 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) //测试一下内核能不能正常加载运行,因为没有文件系统最终会kernel panic qemu-system-x86_64 -kernel arch/x86/boot/bzImage //此时应该无法正常运行
5、制作根文件系统
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 make menuconfig 记得要编译成静态链接,不用动态链接库。 Settings ---> [*] Build static binary (no shared libs) 然后编译安装,默认会安装到源码目录下的 _install 目录中。 make -j$(nproc) && make install
6、制作内存根文件系统镜像
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/
7、准备init脚本,放在rootfs文件夹下,添加如下内容到该文件。
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome MengningOS!" echo "--------------------" cd home /bin/sh
8、给init脚本添加可执行权限
chmod +x init
9、打包成内存根文件系统镜像
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
10、测试挂在根文件系统,看内核启动完成后是否执行init脚本
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
运行截图如下:
注意:按照上述步骤操作,linux-5.4.34文件夹、rootfs文件夹、rootfs.cpio.gz应在同一目录下。
二、找到要求的系统调用
本人学号尾号为31号。打开syscall_64.tbl,查看要选择进行实验的系统调用。
31号系统调用为shmctl,函数入口为__x64_sys_shmctl。
该系统该调用用来控制共享内存。对与共享存储区关联的各种参数进行操作,从而对共享存储区进行控制。
调用该函数使用头文件:
#include<sys/types.h> #include<sys/ipc.h> #include<sys/shm.h>
参数定义:
int shmctl(id,cmd,buf) int id,cmd; struct shmid_ds * buf;
其中:调用成功返回0,否则返回-1。id为被共享存储区的标识符。cmd规定操作的类型。规定如下:
- IPC_STAT:返回包含在指定的shmid相关数据结构中的状态信息,并且把它放置在用户存储区中的*but指针所指的数据结构中。执行此命令的进程必须有读取允许权。
- IPC_SET:对于指定的shmid,为它设置有效用户和小组标识和操作存取权。
- IPC_RMID:删除指定的shmid以及与它相关的共享存储区的数据结构。
- SHM_LOCK:在内存中锁定指定的共享存储区,必须是超级用户才可以进行此项操作。
- Buf是一个用户级数据结构地址。
shmid_ds {struct ipc_perm shm_perm; /*允许权结构*/ int shm_segsz; /*段大小*/ int padl; /*由系统使用;*/ ushort shm_lpid; /*最后操作的进程id;*/ ushort shm_cpid; /*创建者的进程id;*/ ushort shm_nattch; /*当前附界数;*/ short pad2; /*由系统使用;*/ time_t shm_atime; /*最后附接时间*/ time_t shm_dtime; /*最后段接时间*/ time_t shm_ctime; /*最后修改时间*/ }
三、通过汇编触发系统调用
编写C语言文件执行系统调用(父子进程通信共享内存应用)
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <error.h> #define SIZE 1024 int main() { int shmid ; char *shmaddr ; struct shmid_ds buf ; int flag = 0 ; int pid ; shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ; if ( shmid < 0 ) { perror("get shm ipc_id error") ; return -1 ; } pid = fork() ; if ( pid == 0 ) { shmaddr = (char *)shmat( shmid, NULL, 0 ) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } strcpy( shmaddr, "Hi, I am child process!\n") ; shmdt( shmaddr ) ; return 0; } else if ( pid > 0) { sleep(3 ) ; flag = shmctl( shmid, IPC_STAT, &buf) ; if ( flag == -1 ) { perror("shmctl shm error") ; return -1 ; } printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ; printf("parent pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ; printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ; shmaddr = (char *) shmat(shmid, NULL, 0 ) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } printf("%s", shmaddr) ; shmdt( shmaddr ) ; shmctl(shmid, IPC_RMID, NULL) ; }else{ perror("fork error") ; shmctl(shmid, IPC_RMID, NULL) ; } return 0 ; }
GCC 静态编译后运行,输出结果:
汇编改写手动触发系统调用
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <error.h> #define SIZE 1024 int main() { int shmid ; char *shmaddr ; struct shmid_ds buf ; int flag = 0 ; int pid ; shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ; if ( shmid < 0 ) { perror("get shm ipc_id error") ; return -1 ; } pid = fork() ; if ( pid == 0 ) { shmaddr = (char *)shmat( shmid, NULL, 0 ) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } strcpy( shmaddr, "Hi, I am child process!\n") ; shmdt( shmaddr ) ; return 0; } else if ( pid > 0) { sleep(3 ) ; flag = shmctl( shmid, IPC_STAT, &buf) ; asm volatile( "movq %3, %%esi\n\t" // 参数3 "movq %2, %%edx\n\t" // 参数2 "movq %1, %%eax\n\t" // 参数1 "movl $0x1F,%%edi\n\t" // 传递系统调用号31 "shmctl\n\t" // 系统调用 "movq %%eax,%0\n\t" // 结果存到flag中 :"=m"(flag) // 输出 ); if ( flag == -1 ) { perror("shmctl shm error") ; return -1 ; } printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ; printf("parent pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ; printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ; shmaddr = (char *) shmat(shmid, NULL, 0 ) ; if ( (int)shmaddr == -1 ) { perror("shmat addr error") ; return -1 ; } printf("%s", shmaddr) ; shmdt( shmaddr ) ; shmctl(shmid, IPC_RMID, NULL) ; }else{ perror("fork error") ; shmctl(shmid, IPC_RMID, NULL) ; } return 0 ; }
这里只对其中一处进行了汇编。
运行结果如下:
四、GDB调试
重新打包根文件目录,纯命令行下启动虚拟机。
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
在linux-5.4.34目录下另开一个终端启动gdb进行调试
gdb vmlinux
target remote:1234 b __x64_sys_shmctl c
然后进行GDB调试。
总结
汇编指令syscall触发系统调用,通过MSR寄存器找到了中断函数入口,通过swapgs和压栈动作保存现场。跳转到do_syscall_64函数,在ax寄存器中获取系统调用号,然后去执行系统调用内容。然后准备进行现场恢复操作,执行现场恢复。