实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用
- 通过汇编指令触发该系统调用
- 通过gdb跟踪该系统调用的内核处理过程
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
实验内容
环境构建:
跟实验一的环境搭建类似,下载一些开发工具和Linux内核
步骤一、下载内核和开发工具
sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev sudo apt install axel axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
步骤二、解压Linux内核,并配置
#解压 xz -d linux-5.4.34.tar.xz tar -xvf linux-5.4.34.tar 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 #shift+y确定勾选 [*] Provide GDB scripts for kernel debugging [*] Kernel debugging #关闭KASLR,否则会导致打断点失败 Processor type and features ----> [] Randomize the address of the kernel image (KASLR)
步骤三、编译内核并进行测试
make -j$(nproc) # nproc gives the number of CPU cores/threads available
# 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统终会kernel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage # 此时应该不能正常运行
这一步完成后出现如下界面:
此时还不能正常工作,需要制作根文件系统
步骤四、安装并编译busybox制作根文件系统
cd .. 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
步骤五、制作根文件系统
cd .. 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脚本文件,内容如下:
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Welcome Liwuji OS!" echo "--------------------" cd home /bin/sh
然后将init文件放在rootfs目录下
并授予其运行权限
chmod +x init
步骤七、打包系统镜像,测试文件系统
#打包成内存根⽂件系统镜像 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz #测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本
cd .. qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
一切完成之后,会出现如下界面:
可以看到init脚本已经运行了。
到这里环境已经搭建完成。
系统调用实验
我的学号后两位是26,所以打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl文件,查找到系统调用号为26的是msync,函数入口为__x64_sys_msync。
根据系统调用号编写测试代码MsyncTest.c。如下:
#include<stdio.h> int main(){ asm volatile( "movl $0x1a,%eax "//使用EAX传递系统调用号26 "syscall " ); return 0; }
我们将MsyncTest.c文件放在/rootfs/home目录下,由于我们搭建的系统不支持动态链接,因此这里我们在使用gcc编译时要用-static静态编译参数)。
gcc MsyncTest.c -o MsyncTest -static
gdb调试
重新执行根文件系统(重新打包,用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
gdb vmlinux
可能会出现下面的问题:
只要照它的提示做就ok了
接着在gdb中运行:
target remote:1234
再给对应的系统调用打上断点
然后continue就完成了。
实验分析
如果一个内存区域是用来写入文件,那么被修改过的内存和文件在一段时间内将是不相同的,如果此时需要将进程写入至内存文件中,需要采用msync();函数所需头文件#include < sys/mmmap.h>
函数结构为int msync(caddr_t addr,size_t length ,int flags);
addr:内存映射时候返回的内存区域
length:内存映射时所指定的长度
flags: 指定内存和磁盘的同步方式
MS_ASYNC:被修改过的内存区域很快就会被同步。MS_ASYNC和MS_SYNC中仅有一个同时被使用;
MS_SYNC:在内存区域中被修改的页面在msync()系统调用返回前就被写入磁盘;
MS_INVALIDATE:这个选项让内核决定是否要把对内存映射区域的修改写入到磁盘中。它并不确保修改的内容不会被写入,只是告诉内核不需要保存此内容;
0:类似于MS_ASYNC 意味着如果有合适的页面需要保存的时候,内核则会将数据写入至磁盘中。
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
系统调用相关概念
在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:
系统态(也称为管态或核心态),操作系统在系统态运行
用户态(也称为目态),应用程序只能在用户态运行。
在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令两类。
特权指令——在系统态时运行的指令
对内存空间的访问范围基本不受限制,不仅能访问用户存储空间,也能访问系统存储空间,
特权指令只允许操作系统使用,不允许应用程序使用,否则会引起系统混乱。
非特权指令——在用户态时运行的指令
一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间。
内核中将系统调用作为一个特殊的中断来处理。系统调用是通过中断机制实现的,并且一个操作系统的所有系统调用都通过同一个中断入口来实现。在Unix/Linux系统中,系统调用像普通C函数调用那样出现在C程序中。但是一般的函数调用序列并不能把进程的状态从用户态变为核心态,而系统调用却可以做到。C语言编译程序利用一个预先确定的函数库(一般称为C库),其中有各系统调用的名字。C库中的函数都专门使用一条指令,把进程的运行状态改为核心态。Linux的系统调用是通过中断指令“INT 0x80”实现的。每个系统调用都有惟一的号码,称作系统调用号。所有的系统调用都集中在系统调用入口表中统一管理。系统调用入口表是一个函数指针数组,以系统调用号为下标在该数组中找到相应的函数指针,进而就能确定用户使用的是哪一个系统调用。不同系统中系统调用的个数是不同的,目前Linux系统中共定义了221个系统调用。另外,系统调用表中还留有一些余项,可供用户自行添加。当CPU执行到中断指令“INT 0x80”时,硬件就做出一系列响应,其动作与上述的中断响应相同。CPU穿过陷阱门,从用户空间进入系统空间。相应地,进程的上下文从用户堆栈切换到系统堆栈。接着运行内核函数system_call()。首先,进一步保存各寄存器的内容;接着调用syscall_trace( ),以系统调用号为下标检索系统调用入口表sys_call_table,从中找到相应的函数;然后转去执行该函数,完成具体的服务。执行完服务程序,核心检查是否发生错误,并作相应处理。如果本进程收到信号,则对信号作相应处理。最后进程从系统空间返回到用户空间。
Linux 通过软中断实现从用户态到内核态的切换。用户态和核心态是独立的执行流,因此在切换时,需要准备执行栈并保存寄存器 。
内核实现了很多不同的系统调用(提供不同功能),而系统调用处理函数只有一个。因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器来传递。
执行态切换过程:
应用程序在用户态准备好调用参数,执行 int 指令触发软中断 ,中断号为 0x80 ;
CPU 被软中断打断后,执行对应的中断处理函数 ,这时便已进入内核态 ;
系统调用处理函数准备内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);
系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;
系统调用处理函数准备返回值并从内核栈中恢复 寄存器 ;
系统调用处理函数执行 ret 指令切换回用户态 ;
实验总结
通过本次实验,我学会了使用gdb调试代码,跟进一步理解系统调用的原理,以及用户态和核心态之间的切换流程。对Linux内核有了更加深刻的了解。总而言之,这次实验让我受益匪浅。