深入理解系统调用

1. 制作根文件系统,借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序

  1.0 下载编译Linux内核

# 安装
sudo apt install build-essential
sudo apt install qemu # install QEMU 
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev

# 重新下载linux内核源码
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.1 下载 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

  1.2 安装后,开始制作

make menuconfig   # 注意编译成静态链接;  如果运行不成功,可尝试重启虚拟机
Settings  --->
    [*] Build static binary (no shared libs) 
#然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。 
make -j$(nproc) && make install

 

  1.3 制作内存根文件系统镜像

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/

  1.4 准备init脚本文件放在根文件系统根目录下(rootfs/init) 

    添加如下内容到init文件

#!/bin/sh
mount -t proc none /proc mount -t sysfs none /sys
echo "Wellcome TestOS!" echo "--------------------"
cd home
/bin/sh

 

#给init脚本添加可执行权限
chmod +x init
#打包成内存根文件系统镜像
 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

 重新启动虚拟机,可以看到成功执行了init脚本

  

 

2. 查看linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl下系统调用表,10号系统调用为 mprotect;   对应函数为__x64_sys_mprotect

 该系统调用的作用:  mprotect(const void *start, size_t len, int prot)函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值 

   注意:  所指定的内存区间必须包含整个页, 即区间地址必须和整个系统页大小对齐,且区间长度必须是页大小的整数倍

     

 

3. 通过汇编指令触发该系统调用

 创建test.c文件,进行编码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>

#define PAGESIZE 4096
int main(void)
{
    char *p;
    char c;
   
    /* Allocate a buffer; it will have the default
       protection of PROT_READ|PROT_WRITE. */
    p = malloc(1024+PAGESIZE-1);
    if (!p) {
        printf("Couldn’t malloc(1024)");
    }

    p = (char *)(((int) p + PAGESIZE-1) & ~(PAGESIZE-1));

    c = p[666];         /* Read; ok */
    p[666] = 42;        /* Write; ok */    

    /* Mark the buffer read-only. */
    int res;        // 
    asm volatile(
      "mov $0x2, %%edx\n\t"   // 将第3个参数放入 edx 寄存器, 0x2为PROT_READ
      "mov $0x400, %%esi\n\t" // 将第二个参数放入 esi 寄存器    
      "mov %1, %%rdi\n\t"     // 将第一个参数放入 rdi 寄存器
        "mov $0x0a, %%eax\n\t"  // mprotect 的系统调用号为10,将其放入 eax 寄存器
        "syscall\n\t"           // 触发系统调用
        "mov %%rax, %0\n\t"     // 将函数处理结果返回给 res 变量
        :"=m"(res)
        :"b"(p)
     );    
    printf("The return value is : %d \n", res);    
    if (res) {
        printf("Couldn’t mprotect\n");
    }

    //c = p[666];         /* Read; ok */
    //p[666] = 42;        /* Write; program dies on SIGSEGV */
    return 0;
}

  先运行test文件,查看是否成功, 结果为0,表示成功

  

  静态编译该文件得到可执行文件,  并将其放入rootfs/home/目录下,然后重新打包rootfs文件夹:  

gcc -o test test.c -static
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

 

3. 通过gdb跟踪该系统调用的内核处理过程

 运行命令qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s

 

  重新打开一个终端,在__x64_sys_mprotect处设置断点

   

  断点之后,在qemu模拟器执行 ./test,之后会在该入口点处停止,查看系统调用栈(断点处乱码,gdb错误,暂时无法跟踪)

   

 

 4.  系统调用过程分析

 4.0 系统调用

  系统调⽤是通过特定的软件中断(陷阱 trap)向内核发出服务请求,int $0x80和syscall指令的执⾏就会触发⼀个系统调⽤。

  ⼀般每个系统调⽤对应⼀个系统调⽤的封装例程,函数库再⽤这些封装例程定义出给程序员调⽤的API,这些API也就是系统调用的封装,

    在系统调用过程中,用户程序可以通过系统调用来请求内核服务,从而会由用户态转为内核态。  

    

 4.1 系统调用初始化

    在内核初始化过程中,完成了系统调用的初始化,这个过程主要包括中断初始化和系统调用初始化。

    初始化过程:  start_kernel --> trap_init -->  cpu_init --> syscall_init 

   系统调用对应的中断是软中断,向量号为0x80

   在中断初始化时候,会通过软件中断的处理程序system_call(entry_SYSCALL_64)与0x80关联,

   便于之后执行系统调用时候,通过int &0x80跳转到system_call中断处理程序入口处

   而系统调用中,还需要根据系统调用号查找对应的系统调用处理函数,因此需要系统调用的初始化,

   主要就是系统调用入口和系统调用表的初始化

   trap_init函数中调用了cpu_init(arch/x86/kernel/cpu/common.c)函数,该函数会调用

   调用相同源码文件中的syscall_init完成per-cpu 状态初始化,该函数执行系统调用入口的初始化。

   

        

 

 

 4.2 系统调用的执行过程  

  • int $0x80或syscall指令触发, 切换到内核态,并从MSR寄存器找到中断函数入口
  • 开始执⾏system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码
    • 通过swapgs 和压栈动作保存现场
    • do_syscall_64, 在eax寄存器中获取到系统调用号, 根据系统调用号执行对应的系统调用处理函数
      • Linux 内核由一个特殊的表system call table。 系统调用表是Linux内核源码文件 arch/x86/entry/syscall_64.c中定义的数组sys_call_table的对应
      • 该函数会在sys_call_table数组中找到对应的系统调用处理函数, 并将返回值保存在regs的ax中
    • syscall_return_slowpath在调用结束时执行,进行返回, 准备恢复现场,此时可以调用schedul函数做进程调度
    • 回到entry_SYSCALL_64,恢复现场
  • 系统调用返回iret/sysret, 退出系统调用,回到用户态
  • 继续执行int $0x80或syscall指令的下一条指令       
 
4.3 系统调用过程中内核堆栈的变化
  通过int $0x80或syscall指令触发,此时内核为空栈,之后通过pt_regs结构定义的堆栈数据结构进行压栈,做现场的保存
  也就是内核的栈底存放的就是pt_regs结构体内容: 从ss到ebx就是整个pt_regs结构
  恢复现场时候也是按照pt_regs结构进行恢复
  

 

 

查看mprotect对应的源码(linux-5.4.34/mm/mprotect.c),其对应的入口函数为__x64_sys_mprotect,  

 整个过程主要函数调用顺序:   entry_SYSCALL_64()  --->  do_syscall_64  --->   syscall_return_slowpath

 4.3  系统调用的入口点在entry_SYSCALL_64处,函数对应源码在arch/x86/entry/entry_64.s文件中

  主要用于保存和恢复现场ENTRY(entry_SYSCALL_64)

  

 

posted @ 2020-05-27 20:04  zhouzwt  阅读(306)  评论(0编辑  收藏  举报