深入理解系统调用

时间:2020-05-27
本文章向大家介绍深入理解系统调用,主要包括深入理解系统调用使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、实验内容

  • 找一个系统调用,系统调用号为学号最后2位相同的系统调用
  • 通过汇编指令触发该系统调用
  • 通过gdb跟踪该系统调用的内核处理过程
  • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。

二、实验步骤

安装开发工具

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 
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)
qemu-system-x86_64 -kernel arch/x86/boot/bzImage //此时应该无法正常运行

制作根文件系统

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)
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文件

#!/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

 查阅arch/x86/entry/syscalls/syscall_64.tbl可知,28号系统调用为madvise,其代码入口为__x64_sys_madvise。

函数原型为

int madvise(void *addr, size_t length, int advice);

该函数建议内核在从 addr 指定的地址开始,长度等于 len 参数值的范围内,该区域的用户虚拟内存应遵循特定的使用模式。内核使用这些信息优化与指定范围关联的资源的处理和维护过程。

 涉及到Linux的内存映射就让我头疼。该函数不是POSIX标准。

从github上找一个benchmark来勉强运行一下吧。

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#include <time.h>
#include <sys/time.h>
#include <sys/stat.h>

#define TIMER(val) do { \
  struct timeval tm; \
  gettimeofday(&tm, NULL); \
  val = tm.tv_sec * 1000 + tm.tv_usec/1000; \
} while(0)


int FILE_LENGTH = 0;

int doprocess(char *p)
{
  long starttime, endtime;
  TIMER(starttime);
  int nSum = 0;
  int i;
  for (i = 0; i < FILE_LENGTH; i++) {
    nSum += *p;
    p++;
  }
  TIMER(endtime);
  return (endtime - starttime);
}
void readWithoutMadvise(char *path)
{
  int fd = open(path, O_RDWR | O_EXCL);
  if (fd == -1) {
    perror("open error in readWithoutMadvise");
    return;
  }
  void *p = mmap(NULL, FILE_LENGTH, PROT_READ, MAP_SHARED, fd, 0);
  if (p == MAP_FAILED) {
    perror("map error in readWithoutMadvise");
    return;
  }
  close(fd);
  fd = -1;
  int interval = doprocess((char *)p);
  printf("read without madvise: %d ms\n", interval);
  if (munmap(p, FILE_LENGTH) == -1) {
    perror("unmap error in readWithoutMadvise");
    return;
  }
}
void readWithMadvise(char *path)
{
  int fd = open(path, O_RDWR | O_EXCL);
  if (fd == -1) {
    perror("open error in readWithoutMadvise");
    return;
  }
  void *p = mmap(NULL, FILE_LENGTH, PROT_READ, MAP_SHARED, fd, 0);
  if (p == MAP_FAILED) {
    perror("map error in readWithoutMadvise");
    return;
  }
  close(fd);
  fd = -1;
  if (madvise(p, FILE_LENGTH, MADV_WILLNEED | MADV_SEQUENTIAL) == -1) {
    perror("madvise error");
    return;
  }
  int interval = doprocess((char *)p);
  printf("read with madvise: %d ms\n", interval);
  if (munmap(p, FILE_LENGTH) == -1) {
    perror("unmap error in readWithoutMadvise");
    return;
  }
}

void getFileLength(char* path) {
    struct stat st;
    stat(path, &st);

    FILE_LENGTH = (int)st.st_size;  //set global variable FILE_LENGTH

    printf("This file has %d bytes", FILE_LENGTH);
}

int main(int argc, char* argv[])
{
  char *bigFilePath = argv[1];
  long int useMadvise = strtol(argv[2], NULL, 10);

  getFileLength(bigFilePath);

  if (useMadvise == 1) {
    readWithMadvise(bigFilePath);
  } else {
    readWithoutMadvise(bigFilePath);
  }
}

 速度确实快了一点点。。。

执行下列命令,重新打包根文件系统,开启虚拟机gdb调试。

$ 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 -S -s -nographic -append "console=ttyS0"

开启新的terminal

$ cd linux-5.4.34
$ gdb vmlinux
$ target remote:1234
$ c

根据上图可以看出,系统调用的进入点为entry_SYSCALL_64,我们找到系统调用的入口并查看相应汇编代码

ENTRY(entry_SYSCALL_64)
    UNWIND_HINT_EMPTY
    /*
     * Interrupts are off on entry.
     * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
     * it is too small to ever cause noticeable irq latency.
     */

    swapgs
    /* tss.sp2 is scratch space. */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    /* Construct struct pt_regs on stack */
    pushq    $__USER_DS                /* pt_regs->ss */
    pushq    PER_CPU_VAR(cpu_tss_rw + TSS_sp2)    /* pt_regs->sp */
    pushq    %r11                    /* pt_regs->flags */
    pushq    $__USER_CS                /* pt_regs->cs */
    pushq    %rcx                    /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
    pushq    %rax                    /* pt_regs->orig_ax */

    PUSH_AND_CLEAR_REGS rax=$-ENOSYS

    TRACE_IRQS_OFF

    /* IRQs are off. */
    movq    %rax, %rdi
    movq    %rsp, %rsi
    call    do_syscall_64        /* returns with IRQs disabled */

可以看出,再执行中断函数do_syscall_64前,内核会先进行以下操作。

  1. swapgs指令切换gs寄存器从用户态到内核态(其实就是修改了运行级别,使其可以访问内核)。
  2. 保存中断上下文中的rsp寄存器的值。
  3. SWITCH_TO_KERNEL_CR3 切换到内核堆栈空间。
  4. 构建内核堆栈结构体的基本成员变量,保存通用目的寄存器到内核堆栈空间。
  5. TRACE_IRQS_OFF 关闭中断追踪
  6. 将rax系统调用号和rsp内核堆栈地址保存到rdi和rsi寄存器中。
  7. 执行do_syscall_64

我们继续查看do_syscall_64部分代码。

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    struct thread_info *ti;

    enter_from_user_mode();
    local_irq_enable();
    ti = current_thread_info();
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
        nr = syscall_trace_enter(regs);

    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
    } else if (likely((nr & __X32_SYSCALL_BIT) &&
              (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
        nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                    X32_NR_syscalls);
        regs->ax = x32_sys_call_table[nr](regs);
#endif
    }

    syscall_return_slowpath(regs);
}
#endif

do_syscall_64代码主要完成以下几个工作。

  1. 根据系统调用号查找系统调用表得到系统调用函数。
  2. 执行系统调用函数。

原文地址:https://www.cnblogs.com/gfsh/p/12976420.html