深入理解系统调用

实验要求

  • 找一个系统调用,系统调用号为学号最后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内核有了更加深刻的了解。总而言之,这次实验让我受益匪浅。

原文地址:https://www.cnblogs.com/Liwj57csseblog/p/12968566.html