深入理解Linux系统调用
一 实验目标
1.本人学号尾号为70,找到编号为70的系统调用号,通过汇编指令触发该系统调用;
2.通过gdb跟踪该系统调用的内核处理过程;
3.重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。
二 预备知识
1.什么是系统调用?
系统调用是用户空间请求内核服务。操作系统内核提供很多服务。当程序读写文件,开始监听连接的socket , 删除或创建目录或程序结束时,都会执行系统调用。换句话说,系统调用仅仅是一些 [C] (https://en.wikipedia.org/wiki/C_%28programming_language%29) 内核空间函数,用户空间程序调用其处理一些请求。
Linux 内核提供一系列的函数并且这些函数与CPU架构相关。 例如:x86_64 提供 322 个系统调用,x86 提供 358 个不同的系统调用。 系统调用仅仅是一些函数。
2.系统调用的过程
普通应用程序运行在用户态下,其诸多操作都受到限制,比如改变特权级别、访问硬件等。特权高的代码能将自己降至低等级的级别,但反之则是不行的。而系统调用是运行在内核态的,那么运行在用户态的应用程序如何运行内核态的代码呢?操作系统一般是通过中断来从用户态切换到内核态的。学过操作系统课程的同学对中断这个词肯定都不陌生。
中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中有一个叫中断向量表的数组来映射这个关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。
中断分为硬件中断和软件中断,我们这里说的是软件中断,软件中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表你调用第0x80号的中断处理程序。
中断号是有限的,所有不会用一个中断来对应一个系统调用(系统调用有很多)。Linux下用int 0x80触发所有的系统调用,那如何区分不同的调用呢?对于每个系统调用都有一个系统调用号,在触发中断之前,会将系统调用号放入到一个固定的寄存器,0x80对应的中断处理程序会读取该寄存器的值,然后决定执行哪个系统调用的代码。
三 操作流程
首先安装开发模拟工具
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 sudo apt install axel 2 axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/ 3 linux-5.4.34.tar.xz 4 xz -d linux-5.4.34.tar.xz 5 tar -xvf linux-5.4.34.tar 6 cd linux-5.4.34
对内核相关参数进行配置
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)
编译并测试内核是否能正常运行
1 make -j$(nproc) 2 qemu-system-x86_64 -kernel arch/x86/boot/bzImage
然后制作根文件系统
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) 然后编译安装,默认会安装到源码目录下的 _install 目录中。 make -j$(nproc) && make install
制作内存根⽂件系统镜像: 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脚本
将init脚本⽂件放在根⽂件系统跟⽬录下(rootfs/init),添加如下内容到init⽂件。 #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome MengningOS!" echo "--------------------" cd home /bin/sh 给init脚本添加可执⾏权限 chmod +x init
启动内核并执行init脚本
打包成内存根⽂件系统镜像(rootfs目录下) find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz 测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本 qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
四 系统调用探究
1.找到70号系统调用函数

msgrcv函数用于从消息队列中读取msqid指定的消息。
2.测试代码编译(调试预处理)
1.首先安装GDB调试工具
如果系统自带该工具,建议先卸载再重新安装,并且修改其gdb/remote.c的文件,防止出现Remote 'g' packet reply is too long的问题。
具体的修改方法请参考如下链接:
http://blog.sina.com.cn/s/blog_79ba23780101rzk4.html
2.编写脚本用于进行代码调试
编写测试代码test.c
1 int main() 2 { 3 asm volatile( 4 "movl $0x46,%eax\n\t" //使⽤EAX传递系统调⽤号70 5 "syscall\n\t" //触发系统调⽤ 6 ); 7 return 0; 8 }
3.编译代码
使用gcc进行静态编译
gcc test.c -o test -static
4.重新制作根文件系统
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
3.使用GDB进行调试
1.首先启动qemu
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
2.启动GDB工具并设置断点
cd linux-5.4.34 # 启动 gdb vmlinux target remote:1234 # 在semctl调用处打断点 b __64x_sys_msgrcv
输入c执行下一步。

通过下图可以看到,调用了do_syscall_64和entry_SYSCALL_64两个内核函数,分别表示获得系统调用号和进入系统调用。


4.流程分析
syscall指令触发系统调用,通过MSR寄存器找到了中断函数入口,系统调用入口为entry_SYSCALL_64,其中使用了swapgs这一方法来快照式的保存现场,加快了系统调用,随后对一些相关寄存器进行压栈操作。

第一个框表示获取系统调用号。
第二个框表示进入系统调用。
第三个则是采取快照的方式现场,如堆栈信息。
五 实验总结
通过本次实验,了解了70号系统调用号__64_sys_msgrcv的使用,学习了对Linux内核进行断点调试的相关技巧,加深了对系统调用知识以及流程的理解。

浙公网安备 33010602011771号