深入理解系统调用
一、实验要求
1.找一个系统调用,系统调用号为学号最后2位相同的系统调用
2.通过汇编指令触发该系统调用
3.通过gdb跟踪该系统调用的内核处理过程,重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
4.总结分析系统调用的工作机制。
二、实验环境准备
1. 安装一些必要的开发工具
sudo apt install build-essential
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
2. 下载内核源码
sudo apt install axel (下载一个多线程工具加速内核源码下载)
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 defconig # 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)

KASLR让加载到内核时是随机一个地址,如果加载内核一直同一个地址,容易被黑客攻击,这里为了实验方便,把它关闭。
到这里,内核还不能够正常加载运⾏,因为没有⽂件系统,最终会kernel panic。
4. 制作根⽂件系统
我们这⾥为了简化实验环境,仅制作内存根⽂件系统。这⾥借助 BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序
⾸先从https://www.busybox.net下载 busybox源代码解压
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
然后制作内存根⽂件系统镜像,⼤致过程如下:
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)

给init脚本添加可执⾏权限
chmod +x init
打包成内存根⽂件系统镜像
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz
5.测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本
执行qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

6.接下来测试能不能跟踪调试
执行qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S –s
再打开⼀个窗⼝,启动gdb,把内核符号表加载进来,建⽴连接

可以看到能够使⽤gdb跟踪调试Linux内核
到这里,环境已经准备好啦!
三、实验步骤
1. 找一个系统调用,系统调用号为学号最后2位相同的系统调用
我的学号后两位是71,打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,这里我选择171号系统调用__x64_sys_setdomainname

setdomaainname是设置主机的域名,用法如下:
第一个参数是主机域名,第二个参数主机域名的长度.

需要注意的是需要在root权限下才能设置域名,返回0则设置成功,返回1则设置错误。

编写程序setdomianname.c,代码如下

编译运行结果为(注意:这里需要用静态编译):

2.通过汇编指令触发该系统调用
把程序反汇编之后查找关键代码


最终通过汇编指令触发系统调用的程序setdomainname_asm.c代码如下:

编译运行结果如下图所示:

3.通过gdb跟踪该系统调用的内核处理过程,重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。
将生成的可执行文件文件拷贝至rootfs/home文件夹下,然后重新打包成内存根⽂件系统镜像

qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S –s
⽤以上命令先启动,然后可以看到虚拟机⼀启动就暂停了
新创建一个终端开启调试,在系统调用入口加一个断点,发现运行setdomainname程序后停止在此处

bt查看此时的堆栈,可以看到__x64_sys_setdomainname系统调用函数,do_syscall_64 获取系统调用号, 前往系统调用函数,entry_syscall_64 中断入口。

端点定位到了kernel/sys.c的1358行,如下图所示

执行完这个函数,回到了do_sys_call_64 中 ,要执行的 syscall_return_slowpath 函数要为恢复现场做准备。

继续执行到entry_SYSCALL_64()

最后两个pop指令恢复rdi和rsp寄存器

系统调用完成,设置主机域名成功,结果为:

四、实验总结(分析系统调用的工作机制)
系统调用主要分为以下五步,指令触发系统调用(这里是syscall),保存现场,中断处理,恢复现场,中断返回,第一步在用户态,剩下四步在内核态。
syscall指令触发系统调⽤机制会在堆栈上保存当前执⾏程序的栈顶地址(ESP、RSP)、当时的状态字(EFlags、RFlags)、当时 的 CS:EIP/RIP 的值。同时会将当前进程内核态的栈顶地址、内核态的状态字放⼊ CPU 对应 的寄存器,并且 CS:EIP/RIP 寄存器的值会指向中断处理程序的⼊⼝,对于系统调⽤来讲是指 向系统调⽤处理的⼊⼝。系统调⽤发⽣之后进⼊中断处理程序,当中断处 理程序结束后,(中断处理结束前的最后⼀件事是恢复现场)就会执⾏ restore_all 和 INTERRUPT_RETURN(iret),负责把中断时保存的⽤户态寄存器再进⾏出栈操作到当前的 CPU ⾥⾯。最后的 iret与中断信号(包括 int 指令)发⽣时的 CPU 做的动作正好相反,之前是保存,这⾥就是恢复。

浙公网安备 33010602011771号