深入理解系统调用

 一、实验原理

       系统调⽤的库函数就是我们使⽤的操作系统提供的 API(应⽤程序编程接⼝),API 只是函数定义。系统调⽤是通过特定的软件中断(陷阱 trap)向内核发出服务请求,int $0x80和syscall指令的执⾏就会触发⼀个系统调⽤。C库函数内部使⽤了系统调⽤的封装例程,其主要⽬的是发布系统调⽤,使程序员在写代码时不需要⽤汇编指令和寄存器传递参数来触发系统调⽤。⼀般每个系统调⽤对应⼀个系统调⽤的封装例程,函数库再⽤这些封装例程定义出给程序员调⽤的 API,这样把系统调⽤最终封装成⽅便程序员使⽤的C库函数。
       当⽤户态进程调⽤⼀个系统调⽤时,CPU切换到内核态并开始执⾏system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码,其中根据系统调⽤号调⽤对应的内核处理函数。具体来说,在Linux中通过执行int $0x80或syscall指令来触发系统调⽤的执⾏,其中这条int $0x80汇编指令是产⽣中断向量为128的编程异常(trap)。另外Intel处理器中还引⼊了sysenter指令(快速系统调⽤),因为AMD并不⽀持,在此不再详述。我们只关注int指令和syscall指令触发的系统调⽤,进⼊内核后,开始执⾏对应的中断服务程序entry_INT80_32或entry_SYSCALL_64。
      内核通过给每个系统调⽤⼀个编号来区分,即系统调⽤号,将API函数xyz()和系统调⽤内核函数sys_xyz()关联起来了。内核实现了很多不同的系统调⽤,⽤户态进程必须指明需要执⾏哪个系统调⽤,这需要使⽤EAX寄存器传递⼀个名为系统调⽤号的参数。除了系统调⽤号外,系统调⽤也可能需要传递参数。由于压栈的⽅式需要读写内存,函数调⽤速度较慢,64位x86体系结构下普通的函数调⽤和系统调⽤都是通过寄存器传递参数,RDI、RSI、RDX、RCX、R8、R9这6个寄存器⽤作函数/系统调⽤参数传递,依次对应第 1 参数到第 6 个参数。

二、实验准备

下载内核源码

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  

 配置内核编译选项并编译内核

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)

make -j$(nproc) # nproc gives the number of CPU cores/threads available
# 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统终会kernel panic 
qemu-system-x86_64 -kernel arch/x86/boot/bzImage  #  此时应该不能正常运行

配置过程如下图所示:

深入理解系统调用_第1张图片

制作根文件系统

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  

配置过程如下:

深入理解系统调用_第2张图片

制作内核根文件系统镜像

 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⽂件

 #!/bin/sh
 mount -t proc none /proc 
 mount -t sysfs none /sys
 echo "Welcome My OS!"
 echo "-------------------" 
 cd home
 /bin/sh 

给init脚本增加可执行权限

chmod +x init

输入下面代码查看qemu运行情况: 

#打包成内存根⽂件系统镜像 
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  

由下图可知qemu正常运行,可以着手开始做实验:

深入理解系统调用_第3张图片 

 三、实验过程

1.触发系统调用准备工作

本人学号末尾两位为69,故通过查看打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl可知69号调用为msgsnd,该调用用于向消息队列发送消息,对应的内核处理函数为__x64_sys_msgsnd。

深入理解系统调用_第4张图片

我们使用下面的代码触发系统调用

//test_msgsnd.c
int main()
{
  asm volatile(
     //使⽤EAX传递系统调⽤号69
  "movl $0x45,%eax\n\t" 
    //触发系统调用
  "syscall\n\t"  
   );
  return 0;
} 

写好test_msgsnd.c后,使用命令 

gcc -o test_msgsnd msgsnd.c -static 

形成可执行文件,并将文件放至rootfs/home路径下,重新执行

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

从如下结果可以看到触发代码test_msgsnd.c对应的可执行程序已经移入系统中。

深入理解系统调用_第5张图片

 2.gdb调试过程

首先使用如下命令启动qemu模拟器:

qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S –s

再打开⼀个窗⼝,启动gdb,把内核符号表加载进来,输入以下命令建⽴连接:

cd linux-5.4.34/
gdb vmlinux
(gdb) target remote:1234
(gdb) b x86_sys_msgsnd  

 执行过程如下图所示:

然后在qemu模拟器内执行test_msgsnd触发代码,之后便可以在gdb处查看断点信息:

深入理解系统调用_第6张图片

使用bt命令查看堆栈,l命令查看对应代码,由下图可知msgsnd通过syscall进行系统调用,entry_SYSCALL_64是系统调用的入口。

深入理解系统调用_第7张图片

 

在gdb中进行单步执行,可以发现保存现场信息的步骤:

深入理解系统调用_第8张图片

其中的 swapgs指令用于保存现场。这是在x86-64中引入的指令,将保存现场和恢复现场时的CPU寄存器也通过CPU内部的存储器快速保存和恢复。

继续往下执行,可以发现如下恢复现场和系统调用返回的执行过程:

深入理解系统调用_第9张图片

其中,popq %rdi 和popq %rsp恢复了寄存器和堆栈现场,而最后执行的宏USERGS_SYSRET64,做了两部分工作:swapgs——恢复现场和sysretq——系统调用返回。

3.实验总结

从上述实验过程中,可大致了解系统调用执行过程:首先,msgsnd函数底层触发系统调用,在对应的系统调用入口entry_SYSCALL_64中,执行swapgs保存现场。之后,do_syscall_64函数根据rax寄存器中的系统调用号找到对应的系统调用__x64_sys_msgsnd,在系统调用表sys_call_table中找到相应的函数进行调用并将寄存器中保存的参数取出来,作为函数参数,然后陷入内核。系统调用函数执行完毕后,通过popq %rdi 、popq %rsp和宏USERGS_SYSRET64恢复现场并返回。

你可能感兴趣的:(深入理解系统调用)