深入理解系统调用
1、实验要求:
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
2、搭建实验环境
2.1、首先安装环境由于实验一已经安装好了环境,所以直接进行下面步骤。
2.2、配置内核编译选项
首先执行下面的命令:
make defconfig
make menuconfig
进入图中所标的位置进行选择下面两个选项:
2.3、编译内核
make -j$(nproc)
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
由于没有文件系统,最终会 kernel panic。
2.4、利用busybox制作根系统文件
先提前通过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 make -j$(nproc) && make install
在配置选项时进入Settings,选择Build static binary (no shared libs)
接下来在根目录mykernel下制作内存根文件系统镜像:
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/
编写init脚本,并修改权限:在rootfs下新建init文件,写入下面的代码
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome GxyOS!" cd home /bin/sh
通过chmod +x init修改权限。
打包成内存根文件系统镜像,并测试挂载根文件系统:
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
运行结果如下图:
3、汇编指令触发系统调用
3.1、查询系统调用号为47的系统调用
我的学号最后两位是47,可以查出调用号为47号为recvmsg。
然后通过man recvmsg 可以查看该函数:如下图
由描述可知,该系统调用用于从套接字接收信息,也用于从相互连接的套接字中接收数据。
3.2、编写汇编调用代码
在 rootfs/home 编写recvmsg.c,如下:
int main() { asm volatile( "movl $0x2f,%eax\n\t" //使⽤EAX传递系统调⽤号47 "syscall\n\t" //触发系统调⽤ ); return 0; }
gcc 静态编译,并运行,
4. 利用gdb跟踪系统调用的内核处理过程
4.1 重新制作根文件系统,并启动qemu
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz cd .. qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
重新打开一个终端,并运行以下命令:
gdb vmlinux target remote:1234 b __x64_sys_recvmsg
输入c,继续运行:
4.2 在qemu窗口运行程序并在gdb窗口进行单步调试
qemu窗口:
gdb窗口:
出现上图错误后,卸载gdb,重新安装gdb,并找到gdb-8.0.1/gdb/remote.c源文件,将
if (buf_len > 2 * rsa->sizeof_g_packet) error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
修改为:
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; } }
然后重新编译安装:
./configure
make
sudo make install
又调用了do_syscall_64:
5、实验总结:
系统调用的执行过程主要包括如下图所示的两个阶段:用户空间到内核空间的转换阶段,以及系统调用处理程序system_call函数到系统调用服务例程的阶段。
(1)用户空间到内核空间。
如图所示,系统调用的执行需要一个用户空间到内核空间的状态转换,不同的平台具有不同的指令可以完成这种转换,这种指令也被称作操作系统陷入(operating system trap)指令。
Linux通过软中断来实现这种陷入,具体对于X86架构来说,是软中断0x80,也即int $0x80汇编指令。软中断和我们常说的中断(硬件中断)不同之处在于-它由软件指令触发而并非由硬件外设引发。
int 0x80指令被封装在C库中,对于用户应用来说,基于可移植性的考虑,不应该直接调用int $0x80指令。陷入指令的平台依赖性,也正是系统调用需要在C库进行封装的原因之一。
通过软中断0x80,系统会跳转到一个预设的内核空间地址,它指向了系统调用处理程序(不要和系统调用服务例程相混淆),即在arch/i386/kernel/entry.S文件中使用汇编语言编写的system_call函数。
(2)system_call函数到系统调用服务例程。
很显然,所有的系统调用都会统一跳转到这个地址进而执行system_call函数,到2.6.23版为止,内核提供的系统调用已经达到了325个,那么system_call函数又该如何派发它们到各自的服务例程呢?
软中断指令int 0x80执行时,系统调用号会被放入eax寄存器,同时,sys_call_table每一项占用4个字节。这样,如图所示,system_call函数可以读取eax寄存器获得当前系统调用的系统调用号,将其乘以4生成偏移地址,然后以sys_call_table为基址,基址加上偏移地址所指向的内容即是应该执行的系统调用服务例程的地址。