深入理解系统调用 -- 课程实验2

一、 搭建Linux内核调试环境

  本次实验的目录架构,一个主目录LinuxK,其包含3个文件夹:linux-5.4.34内核文件夹,busybox文件夹和rootfs文件夹。

  安装开发工具

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

  下载内核源码

# 在linuxK目录下
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

  配置内核选项

# 在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)

  深入理解系统调用 -- 课程实验2

  编译运行内核

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

  深入理解系统调用 -- 课程实验2

  制作根文件系统

  电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具,bootloader加载根⽂件系统到内存中,内核会将其挂载到根⽬录/下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。我们这⾥为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序。

# 在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

  注意:由于与gcc兼容性问题,不要去官网下载busybox,可用在https://github.com/mirror/busybox 下载 busybox。

make menuconfig
#记得要编译成静态链接,不⽤动态链接库。
Settings --->
     [*] Build static binary (no shared libs)
#然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中
make -j$(nproc) 
make install
#制作内存根文件系统镜像
# 在linuxK目录下
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 MyOS!"
 echo "--------------------" 
 cd home
 /bin/sh 
#在 rootfs 目录下 给init脚本添加可执⾏权限
chome +x init
# 在 rootfs目录下,打包成内存根⽂件系统镜像
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  

  深入理解系统调用 -- 课程实验2

  此时,完成内核调试环境的搭建。

二、跟踪调试Linux内核的基本方法

  本实验中启动虚拟机的方法

qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s
# 纯命令行下启动虚拟机
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

  ⽤以上命令先启动,然后可以看到虚拟机⼀启动就暂停了。加-nographic -append "console=ttyS0"参数启动不会弹出QEMU虚拟机窗⼝,可以在纯命令⾏下启动虚拟机,此时可以通过“killall qemu-systemx86_64”命令强⾏关闭虚拟机。此时不可关闭QEMU及终端。再打开另一个终端(建议在linux5.4.34目录下打开),输入如下命令:

gdb vmlinux
(gdb) target remote:1234
(gdb) b start_kernel
c、 bt、 list、 next、 step....

  系统调用概述

  现代cpu通常有多种特权级别,一般来说特权级总共有4个,编号从Ring 0(最高特权)到Ring 3(最低特权),在Linux上之用到Ring 0和RIng 3,用户态对应Ring 3,内核态对应Ring 0。普通应用程序运行在用户态下,其诸多操作都受到限制,而系统调用是运行在内核态的,操作系统一般是通过中断来从用户态切换到内核态的。
  中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中有一个叫中断向量表的数组来映射这个关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。中断分为硬件中断和软件中断,我们这里说的是软件中断,软件中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表你调用第0x80号的中断处理程序。
  对于每个系统调用都有一个系统调用号,在触发中断之前,会将系统调用号放入到一个固定的寄存器,0x80对应的中断处理程序会读取该寄存器的值,然后决定执行哪个系统调用的代码。
操作系统通过系统调用为运行于其上的进程提供服务。当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

 调用流程

  在应用程序内,调用一个系统调用的流程是怎样的呢?我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

  深入理解系统调用 -- 课程实验2

  如上图,系统调用执行的流程如下:
    1.应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
    2.库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
    3.CPU被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
    4.系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

  执行态切换

    应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

  总结起来, 执行态切换 过程如下:

    1.应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
    2.CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
    3.系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
    4.系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
    5.系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
    6.系统调用处理函数 执行 ret 指令切换回 用户态 ;

  查看系统调用表

  打开内核源码目录linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,可以看到12号系统调用是__x64_sys_brk。

  深入理解系统调用 -- 课程实验2

   汇编调用程序:

  深入理解系统调用 -- 课程实验2

  汇编结果:

  深入理解系统调用 -- 课程实验2

  重新生成文件系统:

     结果:

  

(gdb) b __x64_sys_brk
Breakpoint 1 at 0xffffffff81199a40: file mm/mmap.c, line 187.
(gdb) c
Continuing.

Breakpoint 1, __x64_sys_brk (regs=0xffffc900001b7f58) at mm/mmap.c:187
187     SYSCALL_DEFINE1(brk, unsigned long, brk)
(gdb) n
do_syscall_64 (nr=18446612682188181624, regs=0xffffc900001b7f58) at arch/x86/entry/common.c:300
300             syscall_return_slowpath(regs);
(gdb) n
301     }
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:184
184             movq    RCX(%rsp), %rcx
(gdb) n
185             movq    RIP(%rsp), %r11
(gdb) n
187             cmpq    %rcx, %r11      /* SYSRET requires RCX == RIP */
(gdb) n
188             jne     swapgs_restore_regs_and_return_to_usermode
(gdb) n
205             shl     $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
206             sar     $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
210             cmpq    %rcx, %r11
(gdb) n
211             jne     swapgs_restore_regs_and_return_to_usermode
(gdb) n
213             cmpq    $__USER_CS, CS(%rsp)            /* CS must match SYSRET */
(gdb) n
214             jne     swapgs_restore_regs_and_return_to_usermode
(gdb) n
216             movq    R11(%rsp), %r11
(gdb) n
217             cmpq    %r11, EFLAGS(%rsp)              /* R11 == RFLAGS */
(gdb) n
218             jne     swapgs_restore_regs_and_return_to_usermode
(gdb) n
238             testq   $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
(gdb) n
239             jnz     swapgs_restore_regs_and_return_to_usermode
(gdb) n
243             cmpq    $__USER_DS, SS(%rsp)            /* SS must match SYSRET */
(gdb) n
244             jne     swapgs_restore_regs_and_return_to_usermode
(gdb) n
253             POP_REGS pop_rdi=0 skip_r11rcx=1
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:259
259             movq    %rsp, %rdi
(gdb) n
260             movq    PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:262
262             pushq   RSP-RDI(%rdi)   /* RSP */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:263
263             pushq   (%rdi)          /* RDI */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:271
271             SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
(gdb) n
273             popq    %rdi
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:274
274             popq    %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:275
275             USERGS_SYSRET64
(gdb) n
0x0000000000475349 in ?? ()